neuroalpha 0.5.17 → 0.5.20

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.
@@ -1,1273 +1,1207 @@
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>НейроАльфа — ИИ-автопилот для рынка акций</title>
8
- <style>
9
- :root {
10
- color-scheme: dark;
11
- --bg: #060911;
12
- --bg2: #0b101d;
13
- --panel: rgba(17, 24, 39, 0.72);
14
- --panel-solid: #111827;
15
- --line: rgba(255, 255, 255, 0.08);
16
- --line-bright: rgba(96, 165, 250, 0.38);
17
- --text: #f8fafc;
18
- --muted: #94a3b8;
19
- --accent: #3b82f6;
20
- --accent2: #6366f1;
21
- --cyan: #06b6d4;
22
- --green: #10b981;
23
- --red: #f43f5e;
24
- }
25
- * { box-sizing: border-box; }
26
- html { scroll-behavior: smooth; }
27
- body {
28
- margin: 0;
29
- background: var(--bg);
30
- color: var(--text);
31
- font: 16px/1.6 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: 1200px; margin: 0 auto; padding: 0 24px; position: relative; z-index: 2; }
36
-
37
- /* ---------- Фоновое свечение и сетка ---------- */
38
- .ambient {
39
- position: fixed; inset: 0; pointer-events: none; z-index: 0;
40
- background:
41
- radial-gradient(650px circle at 18% 12%, rgba(59, 130, 246, 0.16), transparent 60%),
42
- radial-gradient(600px circle at 82% 22%, rgba(99, 102, 241, 0.14), transparent 60%),
43
- radial-gradient(700px circle at 50% 85%, rgba(6, 182, 212, 0.09), transparent 65%);
44
- }
45
- .grid-bg {
46
- position: fixed; inset: 0; pointer-events: none; z-index: 0;
47
- background-image:
48
- linear-gradient(to right, rgba(255,255,255,0.025) 1px, transparent 1px),
49
- linear-gradient(to bottom, rgba(255,255,255,0.025) 1px, transparent 1px);
50
- background-size: 48px 48px;
51
- mask-image: radial-gradient(ellipse at 50% 30%, #000 35%, transparent 80%);
52
- }
53
-
54
- /* ---------- Шапка и бегущая строка ---------- */
55
- header {
56
- position: sticky; top: 0; z-index: 40;
57
- background: rgba(6, 9, 17, 0.78);
58
- backdrop-filter: blur(16px);
59
- border-bottom: 1px solid var(--line);
60
- }
61
- .nav { height: 68px; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
62
- .brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 20px; letter-spacing: -0.3px; }
63
- .brand-icon {
64
- width: 34px; height: 34px; border-radius: 9px;
65
- background: linear-gradient(135deg, var(--accent), var(--accent2));
66
- display: inline-flex; align-items: center; justify-content: center;
67
- color: #fff; font-size: 18px; box-shadow: 0 4px 18px rgba(59, 130, 246, 0.4);
68
- }
69
- .nav-links { display: flex; gap: 26px; font-size: 14.5px; color: var(--muted); font-weight: 500; }
70
- .nav-links a { transition: color .15s; }
71
- .nav-links a:hover { color: #fff; }
72
- @media (max-width: 820px) { .nav-links { display: none; } }
73
-
74
- .ticker-wrap {
75
- border-bottom: 1px solid var(--line);
76
- background: rgba(11, 16, 29, 0.65);
77
- overflow: hidden; white-space: nowrap; height: 36px; display: flex; align-items: center;
78
- position: relative; z-index: 2;
79
- }
80
- .ticker-track {
81
- display: inline-flex; gap: 32px;
82
- animation: tickerMove 38s linear infinite;
83
- font-size: 12.5px; color: var(--muted);
84
- }
85
- .ticker-item { display: inline-flex; align-items: center; gap: 6px; }
86
- .ticker-item b { color: var(--text); font-weight: 600; }
87
- @keyframes tickerMove { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } }
88
-
89
- /* ---------- Кнопки ---------- */
90
- .btn {
91
- display: inline-flex; align-items: center; justify-content: center; gap: 8px;
92
- padding: 11px 22px; border-radius: 10px; border: 1px solid var(--line);
93
- background: rgba(255, 255, 255, 0.04); color: var(--text);
94
- font-weight: 600; font-size: 14.5px; cursor: pointer;
95
- transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease, background .18s ease;
96
- }
97
- .btn:hover { border-color: var(--line-bright); background: rgba(255, 255, 255, 0.08); transform: translateY(-1px); }
98
- .btn.primary {
99
- background: linear-gradient(135deg, var(--accent), var(--accent2));
100
- border-color: transparent; color: #fff;
101
- box-shadow: 0 8px 26px -4px rgba(59, 130, 246, 0.55);
102
- }
103
- .btn.primary:hover { box-shadow: 0 12px 32px -2px rgba(99, 102, 241, 0.7); transform: translateY(-2px); }
104
- .btn.lg { padding: 14px 28px; font-size: 15.5px; border-radius: 12px; }
105
-
106
- /* ---------- Hero ---------- */
107
- .hero { position: relative; padding: 64px 0 56px; overflow: hidden; }
108
- #hero-canvas {
109
- position: absolute; inset: 0; width: 100%; height: 100%;
110
- pointer-events: none; z-index: 1; opacity: 0.55;
111
- }
112
- .hero-grid {
113
- display: grid; grid-template-columns: 1.12fr 0.88fr; gap: 44px; align-items: center;
114
- position: relative; z-index: 2;
115
- }
116
- @media (max-width: 960px) { .hero-grid { grid-template-columns: 1fr; } }
117
- .pill-badge {
118
- display: inline-flex; align-items: center; gap: 8px;
119
- padding: 6px 14px; border-radius: 999px;
120
- background: rgba(59, 130, 246, 0.12); border: 1px solid rgba(59, 130, 246, 0.3);
121
- color: #93c5fd; font-size: 13px; font-weight: 500; margin-bottom: 20px;
122
- }
123
- .pulse-dot {
124
- width: 8px; height: 8px; border-radius: 50%; background: var(--green);
125
- box-shadow: 0 0 10px var(--green); animation: pulseDot 2s infinite;
126
- }
127
- @keyframes pulseDot { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .45; transform: scale(1.25); } }
128
-
129
- h1 {
130
- font-size: clamp(36px, 4.6vw, 56px); line-height: 1.08; margin: 0 0 20px;
131
- letter-spacing: -1.4px; font-weight: 800;
132
- }
133
- .grad-text {
134
- background: linear-gradient(135deg, #ffffff 20%, #93c5fd 65%, #818cf8 100%);
135
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
136
- }
137
- .hero-sub { font-size: 18px; color: var(--muted); margin: 0 0 30px; max-width: 600px; line-height: 1.6; }
138
- .hero-actions { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 36px; }
139
- .mini-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; border-top: 1px solid var(--line); padding-top: 24px; }
140
- .mini-stat .num { font-size: 26px; font-weight: 800; letter-spacing: -0.5px; }
141
- .mini-stat .lbl { font-size: 12.5px; color: var(--muted); line-height: 1.35; margin-top: 2px; }
142
- .pos { color: var(--green); } .neg { color: var(--red); }
143
-
144
- /* ---------- Живой макет терминала и мессенджера в Hero ---------- */
145
- .mockup-stack { position: relative; }
146
- .term-card {
147
- background: linear-gradient(180deg, rgba(23, 32, 52, 0.86), rgba(13, 18, 30, 0.92));
148
- border: 1px solid rgba(255, 255, 255, 0.12);
149
- border-radius: 16px; padding: 18px 20px;
150
- backdrop-filter: blur(20px);
151
- box-shadow: 0 24px 60px rgba(0, 0, 0, 0.65), 0 0 40px rgba(59, 130, 246, 0.12);
152
- }
153
- .term-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--line); font-size: 12.5px; }
154
- .dots { display: flex; gap: 6px; }
155
- .dots i { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
156
- .sig-row {
157
- display: grid; grid-template-columns: 70px 1fr 68px 72px; align-items: center; gap: 10px;
158
- padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 13px;
159
- }
160
- .sig-bar { height: 6px; background: rgba(255,255,255,0.06); border-radius: 999px; overflow: hidden; }
161
- .sig-bar div { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--accent), var(--green)); animation: barGrow 2.4s ease-out forwards; }
162
- @keyframes barGrow { from { width: 0; } }
163
- .tag-pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; text-align: center; }
164
- .tag-pill.buy { background: rgba(16, 185, 129, 0.15); color: var(--green); border: 1px solid rgba(16, 185, 129, 0.35); }
165
- .tag-pill.quar { background: rgba(244, 63, 94, 0.14); color: var(--red); border: 1px solid rgba(244, 63, 94, 0.3); }
166
-
167
- .bot-float {
168
- margin-top: 14px;
169
- background: linear-gradient(135deg, rgba(30, 41, 59, 0.95), rgba(15, 23, 42, 0.98));
170
- border: 1px solid var(--line-bright);
171
- border-radius: 14px; padding: 14px 16px;
172
- box-shadow: 0 16px 40px rgba(0, 0, 0, 0.55);
173
- animation: floatY 5s ease-in-out infinite;
174
- }
175
- @keyframes floatY { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
176
- .bot-head { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #93c5fd; margin-bottom: 6px; font-weight: 600; }
177
- .bot-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
178
- .bot-btn { padding: 7px 10px; border-radius: 8px; font-size: 12.5px; font-weight: 600; text-align: center; border: 1px solid var(--line); }
179
- .bot-btn.ok { background: rgba(16, 185, 129, 0.18); border-color: rgba(16, 185, 129, 0.45); color: #34d399; }
180
-
181
- /* ---------- Секции и Bento-карточки ---------- */
182
- section { padding: 72px 0; position: relative; z-index: 2; }
183
- .sec-head { text-align: center; max-width: 720px; margin: 0 auto 44px; }
184
- .sec-tag { color: #60a5fa; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
185
- h2 { font-size: clamp(26px, 3.5vw, 38px); margin: 0 0 12px; letter-spacing: -0.8px; line-height: 1.18; }
186
- .sec-sub { color: var(--muted); margin: 0; font-size: 16.5px; }
187
-
188
- .bento { display: grid; grid-template-columns: repeat(12, 1fr); gap: 20px; }
189
- .bcard {
190
- grid-column: span 4;
191
- background: var(--panel); border: 1px solid var(--line);
192
- border-radius: 16px; padding: 26px;
193
- backdrop-filter: blur(12px);
194
- transition: transform .22s ease, border-color .22s ease, box-shadow .22s ease;
195
- position: relative; overflow: hidden;
196
- }
197
- .bcard.wide { grid-column: span 6; }
198
- @media (max-width: 900px) { .bcard, .bcard.wide { grid-column: span 12; } }
199
- .bcard:hover {
200
- transform: translateY(-4px);
201
- border-color: var(--line-bright);
202
- box-shadow: 0 20px 44px -15px rgba(59, 130, 246, 0.25);
203
- }
204
- .bicon {
205
- width: 42px; height: 42px; border-radius: 10px;
206
- background: rgba(59, 130, 246, 0.14); border: 1px solid rgba(59, 130, 246, 0.3);
207
- display: inline-flex; align-items: center; justify-content: center;
208
- font-size: 20px; margin-bottom: 14px; color: #60a5fa;
209
- }
210
- .bcard h3 { margin: 0 0 8px; font-size: 19px; letter-spacing: -0.3px; }
211
- .bcard p { margin: 0; color: var(--muted); font-size: 14.5px; line-height: 1.55; }
212
-
213
- /* ---------- Живая витрина стратегий ---------- */
214
- .showcase-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; align-items: stretch; }
215
- @media (max-width: 960px) { .showcase-grid { grid-template-columns: 1fr; } }
216
- .sc-card {
217
- background: var(--panel); border: 1px solid var(--line);
218
- border-radius: 18px; padding: 24px;
219
- display: flex; flex-direction: column; gap: 14px;
220
- transition: border-color .2s, transform .2s;
221
- }
222
- .sc-card:hover { border-color: var(--line-bright); transform: translateY(-3px); }
223
- .sc-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
224
- .sc-ret { font-size: 30px; font-weight: 800; letter-spacing: -0.8px; line-height: 1; }
225
- .sc-kpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; background: rgba(0,0,0,0.28); border-radius: 10px; padding: 10px 12px; font-size: 12.5px; }
226
- .sc-kpis b { display: block; font-size: 14.5px; color: var(--text); }
227
-
228
- /* ---------- Тарифы ---------- */
229
- .period-wrap { display: flex; justify-content: center; margin-bottom: 32px; }
230
- .period-seg { display: inline-flex; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 4px; gap: 4px; }
231
- .period-seg button {
232
- border: 0; background: transparent; color: var(--muted);
233
- padding: 8px 18px; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 14px; transition: .15s;
234
- }
235
- .period-seg button.active { background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #fff; }
236
- .plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; align-items: stretch; }
237
- @media (max-width: 960px) { .plans { grid-template-columns: 1fr; } }
238
- .plan {
239
- background: var(--panel); border: 1px solid var(--line);
240
- border-radius: 18px; padding: 30px 26px;
241
- display: flex; flex-direction: column; position: relative;
242
- transition: transform .2s, border-color .2s;
243
- }
244
- .plan:hover { transform: translateY(-4px); }
245
- .plan.featured {
246
- background: linear-gradient(180deg, rgba(30, 58, 138, 0.32), rgba(17, 24, 39, 0.88));
247
- border-color: rgba(96, 165, 250, 0.55);
248
- box-shadow: 0 20px 50px -12px rgba(37, 99, 235, 0.35);
249
- }
250
- .plan-vip-wide {
251
- margin-top: 22px;
252
- background: linear-gradient(135deg, rgba(120, 53, 15, 0.22), rgba(17, 24, 39, 0.94));
253
- border: 1px solid rgba(245, 158, 11, 0.5);
254
- border-radius: 18px; padding: 28px 32px;
255
- display: grid; grid-template-columns: 1fr 1.3fr auto; gap: 28px; align-items: center;
256
- position: relative;
257
- box-shadow: 0 20px 50px -15px rgba(217, 119, 6, 0.25);
258
- }
259
- @media (max-width: 960px) { .plan-vip-wide { grid-template-columns: 1fr; } }
260
- .pop-ribbon {
261
- position: absolute; top: 14px; right: 16px;
262
- background: linear-gradient(135deg, var(--accent), var(--accent2));
263
- color: #fff; font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px;
264
- text-transform: uppercase; letter-spacing: 0.5px;
265
- }
266
- .plan h3, .plan-vip-wide h3 { margin: 0 0 4px; font-size: 21px; }
267
- .plan-desc { font-size: 13.5px; color: var(--muted); margin-bottom: 14px; }
268
- .price { font-size: 38px; font-weight: 800; margin: 6px 0 18px; letter-spacing: -1px; }
269
- .price span { font-size: 14px; font-weight: 400; color: var(--muted); letter-spacing: 0; }
270
- .plan ul, .plan-vip-wide ul { list-style: none; padding: 0; margin: 0 0 26px; font-size: 14.5px; color: #cbd5e1; flex: 1; }
271
- .plan-vip-wide ul { margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; }
272
- @media (max-width: 720px) { .plan-vip-wide ul { grid-template-columns: 1fr; } }
273
- .plan li, .plan-vip-wide li { margin: 10px 0; position: relative; padding-left: 22px; line-height: 1.45; }
274
- .plan-vip-wide li { margin: 0; }
275
- .plan li::before, .plan-vip-wide li::before { content: "✓"; color: var(--green); font-weight: 800; position: absolute; left: 0; top: 0; }
276
-
277
- /* ---------- Анимация появления при прокрутке ---------- */
278
- .reveal { opacity: 0; transform: translateY(22px); transition: opacity .6s ease, transform .6s ease; }
279
- .reveal.in { opacity: 1; transform: translateY(0); }
280
-
281
- footer { padding: 40px 0; border-top: 1px solid var(--line); color: var(--muted); font-size: 13px; position: relative; z-index: 2; }
282
- .modal { position: fixed; inset: 0; background: rgba(3, 6, 12, 0.78); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 16px; }
283
- .modal[hidden] { display: none; }
284
- .modal-box { background: var(--panel-solid); border: 1px solid var(--line-bright); border-radius: 16px; padding: 28px; width: 100%; max-width: 430px; box-shadow: 0 24px 60px #000a; }
285
- .modal-box h3 { margin: 0 0 16px; font-size: 21px; }
286
- .modal-box form { display: grid; gap: 12px; }
287
- .modal-box input[type=email], .modal-box input[type=password], .modal-box input[name=name] {
288
- width: 100%; padding: 11px 14px; border-radius: 10px; border: 1px solid var(--line); background: var(--bg); color: var(--text); font: inherit;
289
- }
290
- .err { color: var(--red); font-size: 13.5px; min-height: 18px; }
291
- .cli-box {
292
- background: #070b14; border: 1px solid var(--line-bright); border-radius: 14px; padding: 16px 20px;
293
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 14px; color: #e2e8f0;
294
- display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap;
295
- box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5);
296
- }
297
- .cli-box code { color: #38bdf8; word-break: break-all; }
298
- .cab-card {
299
- background: linear-gradient(135deg, rgba(30, 58, 138, 0.28), rgba(15, 23, 42, 0.92));
300
- border: 1px solid var(--line-bright); border-radius: 18px; padding: 26px 28px; margin-top: 28px;
301
- box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
302
- }
303
- .cab-grid { display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 24px; align-items: center; }
304
- @media (max-width: 860px) { .cab-grid { grid-template-columns: 1fr; } }
305
- .key-pill {
306
- display: inline-flex; align-items: center; gap: 10px; background: rgba(0, 0, 0, 0.45);
307
- border: 1px solid var(--line); border-radius: 10px; padding: 8px 14px; font-family: monospace; font-size: 14px; color: #93c5fd;
308
- }
309
- .oauth-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-bottom: 14px; }
310
- .oauth-btn {
311
- display: inline-flex; align-items: center; justify-content: center; gap: 9px;
312
- padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
313
- background: rgba(255, 255, 255, 0.04); color: var(--text); font-weight: 600; font-size: 13.5px;
314
- cursor: pointer; transition: border-color .15s, background .15s, transform .15s;
315
- }
316
- .oauth-btn:hover { border-color: var(--line-bright); background: rgba(255, 255, 255, 0.09); transform: translateY(-1px); }
317
- .oauth-btn.full { grid-column: span 2; }
318
- .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; }
319
- .or-sep { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 12px; margin: 4px 0 12px; }
320
- .or-sep::before, .or-sep::after { content: ''; flex: 1; height: 1px; background: var(--line); }
321
- </style>
322
- </head>
323
- <body>
324
- <div class="ambient"></div>
325
- <div class="grid-bg"></div>
326
-
327
- <header>
328
- <div class="wrap nav">
329
- <a class="brand" href="/landing"><span class="brand-icon">◈</span><span>НейроАльфа</span></a>
330
- <nav class="nav-links">
331
- <a href="#showcase">Результаты</a>
332
- <a href="#how">Технология</a>
333
- <a href="#paas-sec">Как работает PaaS</a>
334
- <a href="#brokers">Брокеры и мессенджеры</a>
335
- <a href="#pricing">Тарифы</a>
336
- </nav>
337
- <div style="display:flex;gap:10px" id="nav-auth">
338
- <button class="btn" id="btn-login-open">Войти</button>
339
- <a class="btn primary" href="#pricing" id="btn-nav-cta">Тарифы и подключение →</a>
340
- </div>
341
- </div>
342
- </header>
343
-
344
- <div class="ticker-wrap">
345
- <div class="ticker-track" id="ticker-track">
346
- <span class="ticker-item"><b>SBER</b> <span class="pos">+0,92%</span> · импульс от финансового кластера</span>
347
- <span class="ticker-item"><b>LKOH</b> <span class="pos">+0,74%</span> · переток за Brent и экспортёрами</span>
348
- <span class="ticker-item"><b>YDEX</b> <span class="pos">+1,18%</span> · сильнее широкого рынка</span>
349
- <span class="ticker-item"><b>PLZL</b> <span class="pos">+0,65%</span> · поддержка от макро-токена золота</span>
350
- <span class="ticker-item"><b>TATN</b> <span class="pos">+0,58%</span> · влияние LKOH, ROSN</span>
351
- <span class="ticker-item"><b>SGZH</b> <span class="neg">карантин</span> · отсеяна самообучением правил</span>
352
- <span class="ticker-item"><b>NVTK</b> <span class="pos">+0,61%</span> · возврат к среднему по карте</span>
353
- <span class="ticker-item"><b>SBER</b> <span class="pos">+0,92%</span> · импульс от финансового кластера</span>
354
- <span class="ticker-item"><b>LKOH</b> <span class="pos">+0,74%</span> · переток за Brent и экспортёрами</span>
355
- <span class="ticker-item"><b>YDEX</b> <span class="pos">+1,18%</span> · сильнее широкого рынка</span>
356
- <span class="ticker-item"><b>PLZL</b> <span class="pos">+0,65%</span> · поддержка от макро-токена золота</span>
357
- <span class="ticker-item"><b>TATN</b> <span class="pos">+0,58%</span> · влияние LKOH, ROSN</span>
358
- <span class="ticker-item"><b>SGZH</b> <span class="neg">карантин</span> · отсеяна самообучением правил</span>
359
- </div>
360
- </div>
361
-
362
- <div class="hero">
363
- <canvas id="hero-canvas"></canvas>
364
- <div class="wrap hero-grid">
365
- <div>
366
- <div class="pill-badge"><span class="pulse-dot"></span><span>Московская биржа · ~100 ликвидных акций · Т-Банк · БКС · Финам · Алор</span></div>
367
- <h1><span class="grad-text">ИИ-автопилот,</span> который читает рынок акций как единый язык</h1>
368
- <p class="hero-sub">Платформа находит опережающие волны капитала между связанными бумагами, сама адаптирует пороги под комиссию вашего брокера и присылает готовые заявки в <b>Telegram и MAX</b> с подтверждением в 1 клик.</p>
369
- <div class="hero-actions">
370
- <button class="btn primary lg" id="btn-hero-start">Попробовать бесплатно</button>
371
- <a class="btn lg" href="#pricing">Выбрать тариф PaaS →</a>
372
- </div>
373
- <div class="mini-stats">
374
- <div class="mini-stat"><div class="num pos">+95% … +422%</div><div class="lbl">на истории 2024–2026 против −23,6% у индекса Мосбиржи</div></div>
375
- <div class="mini-stat"><div class="num pos">144 из 144</div><div class="lbl">недель подряд прогноз опережал случайный выбор</div></div>
376
- <div class="mini-stat"><div class="num">1 клик</div><div class="lbl">подтверждение лимитных заявок в лотах из Telegram и MAX</div></div>
377
- </div>
378
- </div>
379
-
380
- <div class="mockup-stack">
381
- <div class="term-card">
382
- <div class="term-top">
383
- <div class="dots"><i style="background:#f43f5e"></i><i style="background:#f59e0b"></i><i style="background:#10b981"></i></div>
384
- <span style="color:var(--muted)">НейроАльфа · Прогноз на 2 часа · Самообучение: вкл.</span>
385
- <span style="font-weight:700;color:var(--muted)">пример экрана</span>
386
- </div>
387
- <div class="sig-row">
388
- <b>LKOH</b>
389
- <div class="sig-bar"><div style="width:88%"></div></div>
390
- <span class="pos" style="text-align:right;font-weight:700">+0,84%</span>
391
- <span class="tag-pill buy">купить</span>
392
- </div>
393
- <div class="sig-row">
394
- <b>SBER</b>
395
- <div class="sig-bar"><div style="width:79%"></div></div>
396
- <span class="pos" style="text-align:right;font-weight:700">+0,71%</span>
397
- <span class="tag-pill buy">купить</span>
398
- </div>
399
- <div class="sig-row">
400
- <b>YDEX</b>
401
- <div class="sig-bar"><div style="width:72%"></div></div>
402
- <span class="pos" style="text-align:right;font-weight:700">+0,64%</span>
403
- <span class="tag-pill buy">купить</span>
404
- </div>
405
- <div class="sig-row" style="border-bottom:0">
406
- <b>SGZH</b>
407
- <div class="sig-bar"><div style="width:25%;background:var(--red)"></div></div>
408
- <span class="neg" style="text-align:right">−0,52%</span>
409
- <span class="tag-pill quar">карантин</span>
410
- </div>
411
- </div>
412
-
413
- <div class="bot-float">
414
- <div class="bot-head"><span>⚡ Бот НейроАльфа · Telegram / MAX</span><span>только что</span></div>
415
- <div style="font-size:13px;color:#e2e8f0">Счёт «Т-Банк Основной»: рассчитан пакет лимитных заявок (в целых лотах Мосбиржи):<br>• <b>Купить LKOH</b> — 7 лотов по 7 142,5 ₽<br>• <b>Купить SBER</b> — 16 лотов по 284,80 ₽</div>
416
- <div class="bot-btns">
417
- <div class="bot-btn ok">✓ Исполнить заявки (2)</div>
418
- <div class="bot-btn" style="color:var(--muted)">✕ Отклонить</div>
419
- </div>
420
- </div>
421
- </div>
422
- </div>
423
- </div>
424
-
425
- <div class="wrap">
426
- <div id="cabinet-box" hidden></div>
427
-
428
- <section id="showcase" class="reveal">
429
- <div class="sec-head">
430
- <div class="sec-tag">Проверено пошагово без заглядывания в будущее</div>
431
- <h2>Живая витрина эталонных стратегий</h2>
432
- <p class="sec-sub">Каждая точка графика получена строгим скользящим обучением: модель учится на прошлом окне и торгует следующий месяц, которого ещё не видела, с учётом комиссий и дивидендов.</p>
433
- </div>
434
- <div class="showcase-grid" id="showcase-list">
435
- <div class="bcard wide"><p>Загружаем актуальные графики с сервера…</p></div>
436
- </div>
437
- </section>
438
-
439
- <section id="how" class="reveal">
440
- <div class="sec-head">
441
- <div class="sec-tag">Архитектура нового поколения</div>
442
- <h2>Почему «НейроАльфа» обыгрывает классические индикаторы</h2>
443
- <p class="sec-sub">Вместо взгляда на одну бумагу в изоляции система видит весь рынок целиком и сама подстраивает правила торговли.</p>
444
- </div>
445
- <div class="bento">
446
- <div class="bcard wide">
447
- <div class="bicon">◈</div>
448
- <h3>Принцип языковых моделей (LLM) для котировок</h3>
449
- <p>Акции выступают как слова в тексте: система строит многомерную карту рынка по совместным движениям цен и 20 фундаментальным свойствам бизнеса, улавливая переток капитала от лидеров к связанным бумагам.</p>
450
- </div>
451
- <div class="bcard wide">
452
- <div class="bicon">⚡</div>
453
- <h3>Самообучение правил и карантин «акций-убийц»</h3>
454
- <p>Вам не нужно крутить десятки настроек. Алгоритм сам калибрует порог выхода под комиссию вашего брокера и автоматически блокирует бумаги, где сигнал деградировал или идёт новостной обвал.</p>
455
- </div>
456
- <div class="bcard">
457
- <div class="bicon">🛡</div>
458
- <h3>Стресс-тест на 5 сдвигах истории</h3>
459
- <p>Любая стратегия проверяется серией сдвигов исторического окна: вы видите медиану и худший сценарий до запуска на деньгах.</p>
460
- </div>
461
- <div class="bcard">
462
- <div class="bicon">🎙</div>
463
- <h3>Голосовой ИИ-управляющий «Стратег»</h3>
464
- <p>Общайтесь с платформой голосом или текстом: Стратег сам ставит эксперименты, ведёт журнал гипотез и объясняет каждую сделку простыми словами.</p>
465
- </div>
466
- <div class="bcard">
467
- <div class="bicon">📰</div>
468
- <h3>Фильтр свежих новостей и макро-факторов</h3>
469
- <p>Учитывает нефть Brent, золото, юань, индекс гособлигаций RGBI и каждые 10 минут размечает новости компаний, блокируя входы против негатива.</p>
470
- </div>
471
- </div>
472
- </section>
473
-
474
- <section id="paas-sec" class="reveal">
475
- <div class="sec-head">
476
- <div class="sec-tag">Облачная PaaS-инфраструктура · Без установки на компьютер</div>
477
- <h2>От исследования стратегии до реальных заявок за 1 минуту</h2>
478
- <p class="sec-sub">Вся вычислительная мощность и ИИ-управляющий «Стратег» работают в защищённом облаке 24/7. Вы управляете счетами прямо из браузера или со смартфона в Telegram и MAX.</p>
479
- </div>
480
- <div class="bento">
481
- <div class="bcard">
482
- <div class="bicon">1</div>
483
- <h3>Тестируйте без ограничений</h3>
484
- <p>Даже на бесплатном тарифе вы можете проводить эксперименты на полной мощности ядра, проверять стратегии на сдвигах истории и видеть весь потенциал доходности.</p>
485
- </div>
486
- <div class="bcard">
487
- <div class="bicon">2</div>
488
- <h3>Выберите тариф</h3>
489
- <p>«Инвестор» — только покупки и один счёт; «Профи» — покупки и шорты, до 5 счетов; «VIP» — индивидуальные лимиты и установка на свой компьютер по договору.</p>
490
- </div>
491
- <div class="bcard">
492
- <div class="bicon">3</div>
493
- <h3>Торгуйте с жёсткими предохранителями</h3>
494
- <p>Подключите счёт Т-Банка в режиме «Подтверждение в 1 клик в Telegram / MAX» или «Полный автомат» с защитой стартового портфеля. БКС, Финам и Алор — в бета-режиме, только с подтверждением заявок.</p>
495
- </div>
496
- </div>
497
- </section>
498
-
499
- <section id="brokers" class="reveal">
500
- <div class="sec-head">
501
- <div class="sec-tag">Безопасность и полный контроль</div>
502
- <h2>Подключение к вашему брокеру и управление из Telegram и MAX</h2>
503
- <p class="sec-sub">Деньги всегда остаются на вашем личном счёте в Т-Банке, БКС, Финаме или Алоре. Ключи зашифрованы по стандарту AES-256-GCM.</p>
504
- </div>
505
- <div class="bento">
506
- <div class="bcard">
507
- <div class="bicon">👁</div>
508
- <h3>1. Режим «Только чтение»</h3>
509
- <p>Безопасное знакомство: платформа синхронизирует ваш реальный портфель и показывает расхождение с модельным прогнозом без отправки заявок.</p>
510
- </div>
511
- <div class="bcard">
512
- <div class="bicon">📲</div>
513
- <h3>2. Подтверждение в 1 клик в мессенджере</h3>
514
- <p>По закрытию свечи сервер сам считает целые лоты Мосбиржи и присылает карточку в Telegram и MAX. Вы нажимаете «Исполнить» прямо с телефона.</p>
515
- </div>
516
- <div class="bcard">
517
- <div class="bicon">🤖</div>
518
- <h3>3. Полный автомат с предохранителями</h3>
519
- <p>Лимитные заявки с защитным отступом цены, жёстким лимитом выделенной суммы, дневным стопом по просадке и пропуском первых/последних 10 минут сессии.</p>
520
- </div>
521
- </div>
522
- </section>
523
-
524
- <section id="pricing" class="reveal">
525
- <div class="sec-head">
526
- <div class="sec-tag">Прозрачные условия</div>
527
- <h2>Тарифные планы</h2>
528
- <p class="sec-sub">Начните с бесплатного доступа к витрине и демо-счёту или подключите полный автопилот для своих брокерских счетов.</p>
529
- </div>
530
- <div class="period-wrap">
531
- <div class="period-seg" id="months-seg">
532
- <button data-m="1" class="active">1 месяц</button>
533
- <button data-m="3">3 месяца (−10%)</button>
534
- <button data-m="12">12 месяцев (−20%)</button>
535
- </div>
536
- </div>
537
- <div class="plans" style="grid-template-columns:repeat(auto-fit,minmax(300px,1fr))">
538
- <div class="plan">
539
- <h3>Инвестор</h3>
540
- <div class="plan-desc">Консервативная реальная торговля только от покупки (без шортов)</div>
541
- <div class="price" id="price-investor">3 900 ₽ <span>/ мес</span></div>
542
- <ul>
543
- <li><b>1 брокерский счёт</b> (Т-Банк, БКС, Финам, Алор и др.)</li>
544
- <li>Лимит стартового капитала: <b>до 1 000 000 ₽</b> (+ 100% реинвестирование всей прибыли)</li>
545
- <li>Режим <b>«Только в рост (без шорта)»</b>, также доступен тестовый контур (песочница брокера)</li>
546
- <li>Дневной стоп по просадке, лимит суммы и защитные стопы</li>
547
- <li>Подтверждение заявок в 1 клик в <b>Telegram и MAX</b></li>
548
- </ul>
549
- <button class="btn primary" data-buy="investor">Оформить «Инвестор»</button>
550
- </div>
551
-
552
- <div class="plan featured">
553
- <div class="pop-ribbon">Оптимальный выбор</div>
554
- <h3>Профи</h3>
555
- <div class="plan-desc">Заработок и на росте, и на падении акций (с шортами)</div>
556
- <div class="price" id="price-pro">9 900 ₽ <span>/ мес</span></div>
557
- <ul>
558
- <li>До <b>5 брокерских счетов</b> параллельно</li>
559
- <li>Лимит стартового капитала: <b>до 1 000 000 ₽</b> на каждом счёте (+ 100% реинвестирование всей прибыли)</li>
560
- <li>Режим <b>«В рост и в падение (с шортом)»</b> с адаптацией к тренду индекса</li>
561
- <li>Защитные стоп-заявки у брокера (Т-Банк, бета)</li>
562
- <li>Поддержка боевого и тестового (песочница) режимов на каждом счёте</li>
563
- </ul>
564
- <button class="btn primary" data-buy="pro">Оформить «Профи»</button>
565
- </div>
566
- </div>
567
-
568
- <div class="plan-vip-wide">
569
- <div>
570
- <span class="pop-ribbon" style="position:static;display:inline-block;margin-bottom:8px;background:linear-gradient(135deg,#d97706,#f59e0b)">Индивидуальные условия</span>
571
- <h3 style="font-size:24px;margin-bottom:6px">👑 Тариф VIP</h3>
572
- <div class="plan-desc" style="margin-bottom:10px">Индивидуальное количество счетов и начальная сумма торговли по согласованию</div>
573
- <div style="font-size:26px;font-weight:800;color:#fbbf24">По согласованию</div>
574
- </div>
575
- <ul>
576
- <li><b>Количество брокерских счетов и начальная сумма торговли</b> — по индивидуальному согласованию</li>
577
- <li>Облачная машина или <b>установка на свой компьютер</b> (Windows, Linux, Docker) по договору</li>
578
- <li>Режим <b>«В рост и в падение (с шортом)»</b> с повышенной концентрацией слота</li>
579
- <li>Персональный VIP-токен активации от администратора платформы</li>
580
- </ul>
581
- <div style="display:flex;flex-direction:column;gap:10px;min-width:220px">
582
- <button class="btn primary" type="button" id="btn-vip-req" style="background:linear-gradient(135deg,#d97706,#f59e0b);justify-content:center">Согласовать условия VIP</button>
583
- <button class="btn" type="button" id="btn-vip-token-open" style="font-size:13px;padding:9px 14px;justify-content:center">У меня есть VIP-токен</button>
584
- </div>
585
- </div>
586
- </section>
587
-
588
- <footer>
589
- <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px">
590
- <div>© 2026 Автор и правообладатель: Перепелкин Р.А. · Оператор платформы: ООО «БИС» («Бизнес Интер Софт», г. Рязань) · Все права защищены</div>
591
- <div style="display:flex;gap:18px">
592
- <a href="/legal#offer" style="color:#93c5fd">Публичная оферта</a>
593
- <a href="/legal#privacy" style="color:#93c5fd">Политика конфиденциальности</a>
594
- <a href="/legal#risks" style="color:#93c5fd">Декларация о рисках</a>
595
- </div>
596
- </div>
597
- <div style="margin-top:8px">Проприетарное программное обеспечение (PaaS). Сервис является аналитическим программно-вычислительным комплексом и не является индивидуальной инвестиционной рекомендацией или доверительным управлением. Результаты на исторических данных не гарантируют доходности в будущем.</div>
598
- </footer>
599
- </div>
600
-
601
- <div class="modal" id="auth-modal" hidden>
602
- <div class="modal-box">
603
- <h3 id="auth-title">Вход в НейроАльфа</h3>
604
- <div id="tg-bot-login-box" hidden style="margin-bottom:8px">
605
- <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
- <span class="oauth-ico" style="background:#24a1de;color:#fff">✈</span>
607
- <span id="btn-tg-bot-lbl">Войти в 1 клик через Telegram-бота</span>
608
- </button>
609
- </div>
610
- <div id="max-bot-login-box" hidden style="margin-bottom:12px">
611
- <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">
612
- <span class="oauth-ico" style="background:#7c3aed;color:#fff">M</span>
613
- <span id="btn-max-bot-lbl">Войти в 1 клик через мессенджер MAX</span>
614
- </button>
615
- </div>
616
- <div class="oauth-grid" id="oauth-btns" hidden>
617
- <button type="button" class="oauth-btn" data-oauth="yandex" hidden>
618
- <span class="oauth-ico" style="background:#fc3f1d;color:#fff">Я</span>
619
- <span>Яндекс ID</span>
620
- </button>
621
- <button type="button" class="oauth-btn" data-oauth="sber" hidden>
622
- <span class="oauth-ico" style="background:#21a038;color:#fff">С</span>
623
- <span>Сбер ID</span>
624
- </button>
625
- <button type="button" class="oauth-btn" data-oauth="tbank" hidden>
626
- <span class="oauth-ico" style="background:#ffdd2d;color:#111">Т</span>
627
- <span>T-ID (Т-Банк)</span>
628
- </button>
629
- <button type="button" class="oauth-btn" data-oauth="google" hidden>
630
- <span class="oauth-ico" style="background:#fff;color:#2563eb">G</span>
631
- <span>Google</span>
632
- </button>
633
- <button type="button" class="oauth-btn full" data-oauth="vk" hidden>
634
- <span class="oauth-ico" style="background:#0077ff;color:#fff">VK</span>
635
- <span>Войти через VK ID</span>
636
- </button>
637
- </div>
638
- <div class="or-sep" id="oauth-or-sep" hidden>или по Email и паролю</div>
639
- <form id="auth-form">
640
- <input name="name" id="auth-name" placeholder="Ваше имя" hidden>
641
- <input name="email" type="email" required placeholder="Email">
642
- <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>
644
- <div class="err" id="auth-err"></div>
645
- <button class="btn primary" type="submit" id="auth-submit">Войти</button>
646
- <button class="btn" type="button" id="auth-toggle">Нет аккаунта? Зарегистрироваться</button>
647
- <button class="btn" type="button" id="auth-close" style="border:0;color:var(--muted)">Закрыть</button>
648
- </form>
649
- </div>
650
- </div>
651
-
652
- <!-- Модальное окно подписания Договора-оферты и Декларации о рисках перед подпиской -->
653
- <div class="modal" id="offer-modal" hidden>
654
- <div class="modal-box" style="max-width:660px">
655
- <h3>Договор публичной оферты и Декларация о рисках</h3>
656
- <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
- <p style="margin-top:0"><b>1. Предмет договора:</b> Настоящая Публичная оферта регулирует предоставление доступа к облачной аналитической платформе «НейроАльфа» (PaaS/SaaS) согласно выбранному тарифному плану («Инвестор», «Профи», «VIP»).</p>
658
- <p><b>2. Правовой статус (отсутствие ИИР и ДУ):</b> Сервис является исключительно программно-техническим средством (калькулятором статистических закономерностей). Исполнитель <b>не осуществляет инвестиционное консультирование</b>, не даёт индивидуальных инвестиционных рекомендаций (ФЗ № 39-ФЗ) и не осуществляет доверительное управление. Все сделки совершаются Пользователем самостоятельно на его личном брокерском счёте.</p>
659
- <p><b>3. Параметры тарифов</b> (тарифы различаются возможностями платформы; доходность не гарантируется и тарифом не определяется):
660
- <br>• <b>«Инвестор» (3 900 ₽/мес)</b> — 1 брокерский счёт, стартовый лимит до 1 000 000 ₽ (+ реинвестирование прибыли), торговля только от покупки («Только в рост (без шорта)»);
661
- <br>• <b>«Профи» (9 900 ₽/мес)</b> — до 5 брокерских счетов, стартовый лимит до 1 000 000 ₽ на каждом (+ реинвестирование прибыли), торговля в рост и в падение (с шортами);
662
- <br>• <b>«VIP»</b> — по индивидуальному договору: количество счетов, сумма торговли, облачная машина или установка на компьютер.
663
- </p>
664
- <p><b>4. Запрет автоследования и ретрансляции сигналов:</b> Пользователю строго запрещается подключать к платформе счета, участвующие в сервисах автоследования или копирования сделок (как в роли ведущего/автора стратегии, так и в роли подписчика), а также передавать или перепродавать сигналы третьим лицам. При обнаружении признаков автоследования счёт автоматически блокируется.</p>
665
- <p style="margin-bottom:0"><b>5. Декларация о рисках:</b> Торговля ценными бумагами сопряжена с риском потери части или всего инвестированного капитала. Результаты на исторических данных не гарантируют доходности в будущем. Полный текст: <a href="/legal" target="_blank" style="color:#93c5fd">открыть в новой вкладке →</a></p>
666
- </div>
667
- <div style="display:grid;gap:10px;font-size:13px;color:#e2e8f0;margin-bottom:16px">
668
- <label style="display:flex;gap:10px;align-items:flex-start;cursor:pointer">
669
- <input type="checkbox" id="offer-chk-1" style="width:17px;height:17px;margin-top:2px;flex:none">
670
- <span>Я прочитал(а) и безоговорочно принимаю условия <a href="/legal#offer" target="_blank" style="color:#93c5fd">Договора публичной оферты</a> и <a href="/legal#privacy" target="_blank" style="color:#93c5fd">Политики обработки персональных данных</a>.</span>
671
- </label>
672
- <label style="display:flex;gap:10px;align-items:flex-start;cursor:pointer">
673
- <input type="checkbox" id="offer-chk-2" style="width:17px;height:17px;margin-top:2px;flex:none">
674
- <span>Я ознакомлен(а) с <a href="/legal#risks" target="_blank" style="color:#93c5fd">Декларацией о рисках</a>, понимаю, что сервис не является индивидуальной инвестиционной рекомендацией, и обязуюсь <b>не использовать автоследование</b> на подключаемых счетах.</span>
675
- </label>
676
- </div>
677
- <div style="display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap">
678
- <button class="btn" type="button" id="btn-offer-decline">Отказ (Отмена)</button>
679
- <button class="btn primary" type="button" id="btn-offer-accept" disabled>Подписать оферту и продолжить →</button>
680
- </div>
681
- </div>
682
- </div>
683
-
684
- <!-- Модальное окно согласования VIP и ввода VIP-токена -->
685
- <div class="modal" id="vip-modal" hidden>
686
- <div class="modal-box" style="max-width:480px">
687
- <h3>Тариф VIP · Индивидуальные условия</h3>
688
- <p style="font-size:13.5px;color:var(--muted);margin:0 0 14px">Тариф VIP — индивидуальные условия по договору: число счетов, объём торгового капитала, облачная машина или установка на свой компьютер.</p>
689
- <form id="vip-req-form" style="margin-bottom:18px;padding-bottom:16px;border-bottom:1px solid var(--line)">
690
- <div style="font-size:13px;font-weight:600;color:#f59e0b">1. Оставить заявку на согласование условий VIP:</div>
691
- <input name="name" placeholder="Ваше имя" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
692
- <input name="contact" required placeholder="Ваш Telegram (@username) или телефон для связи" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
693
- <input name="capitalRub" placeholder="Планируемый объём капитала (например, 25 млн ₽)" style="width:100%;padding:10px 12px;border-radius:9px;border:1px solid var(--line);background:var(--bg);color:var(--text)">
694
- <button class="btn primary" type="submit" style="background:linear-gradient(135deg,#d97706,#f59e0b)">Отправить заявку администратору</button>
695
- </form>
696
- <form id="vip-token-form">
697
- <div style="font-size:13px;font-weight:600;color:#93c5fd">2. Уже получили персональный VIP-токен от администратора?</div>
698
- <div style="display:flex;gap:8px">
699
- <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)">
700
- <button class="btn primary" type="submit">Активировать VIP</button>
701
- </div>
702
- </form>
703
- <div style="margin-top:12px;text-align:right">
704
- <button class="btn" type="button" id="btn-vip-close" style="border:0;color:var(--muted)">Закрыть</button>
705
- </div>
706
- </div>
707
- </div>
708
-
709
- <script>
710
- // ---------- 1. Анимированный граф перетоков капитала на фоне Hero ----------
711
- (() => {
712
- const cv = document.getElementById('hero-canvas');
713
- if (!cv) return;
714
- const ctx = cv.getContext('2d');
715
- const tickers = ['SBER','LKOH','YDEX','ROSN','NVTK','GMKN','TATN','PLZL','CHMF','GAZP','SNGS','MOEX','T','PHOR','NLMK','ALRS'];
716
- let W = 0, H = 0;
717
- const resize = () => { W = cv.width = cv.offsetWidth; H = cv.height = cv.offsetHeight; };
718
- window.addEventListener('resize', resize);
719
- resize();
720
- const nodes = tickers.map((sym, i) => ({
721
- sym,
722
- x: (0.1 + 0.8 * ((i * 0.618) % 1)) * (W || 1200),
723
- y: (0.15 + 0.7 * (((i * 0.382) + 0.2) % 1)) * (H || 600),
724
- vx: (Math.random() - 0.5) * 0.35,
725
- vy: (Math.random() - 0.5) * 0.35,
726
- r: i < 5 ? 5 : 3.5,
727
- }));
728
- const pulses = [];
729
- setInterval(() => {
730
- if (pulses.length < 8) {
731
- const a = Math.floor(Math.random() * nodes.length);
732
- const b = (a + 1 + Math.floor(Math.random() * (nodes.length - 1))) % nodes.length;
733
- pulses.push({ a, b, t: 0 });
734
- }
735
- }, 450);
736
-
737
- function frame() {
738
- ctx.clearRect(0, 0, W, H);
739
- for (const n of nodes) {
740
- n.x += n.vx; n.y += n.vy;
741
- if (n.x < 40 || n.x > W - 40) n.vx *= -1;
742
- if (n.y < 30 || n.y > H - 30) n.vy *= -1;
743
- }
744
- for (let i = 0; i < nodes.length; i++) {
745
- for (let j = i + 1; j < nodes.length; j++) {
746
- const dx = nodes[i].x - nodes[j].x, dy = nodes[i].y - nodes[j].y;
747
- const d = Math.hypot(dx, dy);
748
- if (d < 220) {
749
- ctx.strokeStyle = `rgba(96, 165, 250, ${(1 - d / 220) * 0.22})`;
750
- ctx.lineWidth = 1;
751
- ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(nodes[j].x, nodes[j].y); ctx.stroke();
752
- }
753
- }
754
- }
755
- for (let k = pulses.length - 1; k >= 0; k--) {
756
- const p = pulses[k];
757
- p.t += 0.018;
758
- if (p.t >= 1) { pulses.splice(k, 1); continue; }
759
- const na = nodes[p.a], nb = nodes[p.b];
760
- const px = na.x + (nb.x - na.x) * p.t, py = na.y + (nb.y - na.y) * p.t;
761
- ctx.fillStyle = '#38bdf8';
762
- ctx.beginPath(); ctx.arc(px, py, 3, 0, Math.PI * 2); ctx.fill();
763
- }
764
- for (const n of nodes) {
765
- ctx.fillStyle = '#60a5fa';
766
- ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); ctx.fill();
767
- ctx.fillStyle = 'rgba(203, 213, 225, 0.55)';
768
- ctx.font = '600 11px system-ui';
769
- ctx.fillText(n.sym, n.x + 8, n.y + 4);
770
- }
771
- requestAnimationFrame(frame);
772
- }
773
- requestAnimationFrame(frame);
774
- })();
775
-
776
- // ---------- 2. Плавное появление секций при прокрутке ----------
777
- const io = new IntersectionObserver((entries) => {
778
- entries.forEach((e) => { if (e.isIntersecting) e.target.classList.add('in'); });
779
- }, { threshold: 0.12 });
780
- document.querySelectorAll('.reveal').forEach((el) => io.observe(el));
781
-
782
- // ---------- 3. Витрина, тарифы и авторизация ----------
783
- const fmt = (v, d = 1) => (v > 0 ? '+' : '') + Number(v || 0).toFixed(d).replace('.', ',');
784
- let selectedMonths = 1;
785
- let isReg = false;
786
- let pendingPlan = null;
787
- let currentUser = null;
788
-
789
- function sparkSvg(curve, mktCurve, id) {
790
- if (!curve || curve.length < 2) return '';
791
- const W = 460, H = 130;
792
- const norm = (arr) => arr.map((p) => (p.equity / arr[0].equity) * 100);
793
- const a = norm(curve), m = mktCurve?.length > 1 ? norm(mktCurve) : [];
794
- const all = [...a, ...m];
795
- const lo = Math.min(...all), hi = Math.max(...all, lo + 1);
796
- const xy = (arr) => arr.map((v, i) => [((i / (arr.length - 1)) * W), (H - 10 - ((v - lo) / (hi - lo)) * (H - 20))]);
797
- const pts = (arr) => xy(arr).map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(' ');
798
- const areaPts = `0,${H} ${pts(a)} ${W},${H}`;
799
- return `<svg viewBox="0 0 ${W} ${H}" width="100%" height="${H}" style="overflow:visible">
800
- <defs>
801
- <linearGradient id="g${id}" x1="0" y1="0" x2="0" y2="1">
802
- <stop offset="0%" stop-color="#3b82f6" stop-opacity="0.38"/>
803
- <stop offset="100%" stop-color="#3b82f6" stop-opacity="0.0"/>
804
- </linearGradient>
805
- </defs>
806
- <polygon points="${areaPts}" fill="url(#g${id})"/>
807
- ${m.length ? `<polyline points="${pts(m)}" fill="none" stroke="#64748b" stroke-width="1.6" stroke-dasharray="4 3"/>` : ''}
808
- <polyline points="${pts(a)}" fill="none" stroke="#3b82f6" stroke-width="2.8"/>
809
- </svg>`;
810
- }
811
-
812
- function renderCabinet() {
813
- const cab = document.getElementById('cabinet-box');
814
- if (!cab) return;
815
- if (!currentUser) {
816
- cab.hidden = true;
817
- return;
818
- }
819
- cab.hidden = false;
820
- const escH = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
- const untilStr = currentUser.planUntil > Date.now()
833
- ? `оплачен до ${new Date(currentUser.planUntil).toLocaleDateString('ru-RU')}`
834
- : 'бессрочно';
835
- const capLim = currentUser.limits?.maxCapitalPerAccountRub
836
- ? (currentUser.limits.maxCapitalPerAccountRub >= 100000000 ? 'без ограничений' : `до ${(currentUser.limits.maxCapitalPerAccountRub / 1000000).toLocaleString('ru-RU')} млн ₽/счёт`)
837
- : 'демо-режим';
838
- cab.innerHTML = `<div class="cab-card">
839
- <div class="cab-grid">
840
- <div>
841
- <div class="sec-tag" style="margin-bottom:4px">Личный кабинет PaaS · Аккаунт активен</div>
842
- <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:14px;font-size:14.5px;color:#cbd5e1">
844
- Тариф: <b class="pos">${currentUser.planName}</b> (${untilStr}) · Брокерских счетов: до <b>${currentUser.limits?.maxBrokerAccounts ?? 0}</b> (${capLim}) · Режим: <b>${currentUser.limits?.allowShorts ? 'в рост и в падение (с шортом)' : 'только в рост (без шорта)'}</b>.
845
- </div>
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>
849
- </div>
850
- </div>
851
- <div style="display:flex;flex-direction:column;gap:10px">
852
- <div style="display:flex;gap:10px;flex-wrap:wrap">
853
- <a class="btn" href="#pricing" style="flex:1">Сменить / продлить тариф</a>
854
- ${currentUser.role === 'admin' ? '<a class="btn" href="/admin">Консоль админа →</a>' : ''}
855
- </div>
856
- </div>
857
- </div>
858
- <div id="cab-workspace" style="margin-top:18px;padding-top:16px;border-top:1px solid var(--line)"></div>
859
- </div>`;
860
- renderWorkspace();
861
-
862
- document.getElementById('cab-promo-btn')?.addEventListener('click', () => {
863
- const code = document.getElementById('cab-promo-in')?.value.trim();
864
- if (!code) return;
865
- requireOfferAgreement(async () => {
866
- const r = await fetch('/api/auth/redeem-promo', {
867
- method: 'POST',
868
- headers: { 'Content-Type': 'application/json' },
869
- body: JSON.stringify({ code }),
870
- });
871
- const j = await r.json();
872
- if (!r.ok) return alert(j.error || 'Ошибка активации кода');
873
- currentUser = j.user;
874
- renderCabinet();
875
- alert(`Код успешно активирован! Ваш тариф: ${currentUser.planName}`);
876
- });
877
- });
878
- }
879
-
880
- // ---------- «Где работать»: облачная машина или установка на свой компьютер (VIP по договору) ----------
881
- async function renderWorkspace() {
882
- const box = document.getElementById('cab-workspace');
883
- if (!box || !currentUser) return;
884
- const escH = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
885
- let me = null;
886
- try { me = await (await fetch('/api/auth/me')).json(); } catch { /* ignore */ }
887
- const list = me?.user?.instances || [];
888
- const dl = me?.downloads || {};
889
- const paid = currentUser.plan && currentUser.plan !== 'none' && currentUser.plan !== 'free';
890
- const canDesktop = Boolean(currentUser.limits?.allowDesktop);
891
- const ago = (ts) => {
892
- if (!ts) return 'ещё не выходила на связь';
893
- const s = Math.round((Date.now() - ts) / 1000);
894
- return s < 120 ? `${s} с назад` : s < 7200 ? `${Math.round(s / 60)} мин назад` : `${Math.round(s / 3600)} ч назад`;
895
- };
896
- 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
-
898
- const clouds = list.filter((i) => i.kind === 'cloud');
899
- const cloudBody = clouds.length
900
- ? clouds.map((i) => {
901
- const state = i.online ? '<span class="pos">работает</span>'
902
- : ['pending', 'deploying', 'deployed'].includes(i.provisionState) ? 'готовится к запуску (обычно 2–5 минут)'
903
- : i.provisionState === 'error' ? 'идёт повторное развёртывание' : `нет связи (${ago(i.lastSeen)})`;
904
- return `<div style="font-size:14px;margin-bottom:8px">Адрес: <b>${escH(i.host)}</b> · ${state}</div>
905
- <a class="btn primary" href="/api/instances/open?i=${encodeURIComponent(i.id)}">Перейти в терминал →</a>
906
- <div style="font-size:12.5px;color:var(--muted);margin-top:8px">Можно добавить адрес в закладки: вход через этот кабинет выполняется автоматически.</div>`;
907
- }).join('<hr style="border:0;border-top:1px solid var(--line);margin:10px 0">')
908
- : paid
909
- ? '<div style="font-size:14px;color:var(--muted)">Машина создаётся автоматически после оплаты — обновите страницу через пару минут.</div>'
910
- : '<div style="font-size:14px;color:var(--muted)">Оформите тариф — персональная облачная машина с ИИ будет создана автоматически.</div><a class="btn" href="#pricing" style="margin-top:8px">Выбрать тариф</a>';
911
-
912
- const desks = list.filter((i) => i.kind === 'desktop');
913
- let deskBody;
914
- if (!canDesktop) {
915
- deskBody = '<div style="font-size:14px;color:var(--muted)">Установка на свой компьютер (Windows, Linux или Docker) — по индивидуальному договору на тарифе VIP.</div><a class="btn" href="#pricing" style="margin-top:8px">Обсудить VIP</a>';
916
- } else {
917
- const links = [
918
- dl.windows ? `<a class="btn" href="${escH(dl.windows)}">⬇ Windows</a>` : '',
919
- dl.linux ? `<a class="btn" href="${escH(dl.linux)}">⬇ Linux</a>` : '',
920
- ].filter(Boolean).join(' ');
921
- const bound = desks.filter((i) => i.installBound);
922
- 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>'}
923
- ${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>` : ''}
924
- ${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)}` : ''}
925
- <a class="btn" href="/api/instances/open?i=${encodeURIComponent(i.id)}" style="padding:4px 10px;font-size:12.5px">Открыть</a>
926
- <button type="button" class="btn" data-unbind="${escH(i.id)}" style="padding:4px 10px;font-size:12.5px">Отвязать</button></div>`).join('')}
927
- <button type="button" class="btn primary" id="cab-desktop-code">Получить код активации</button>
928
- <div id="cab-desktop-code-out" style="margin-top:8px;font-size:14px"></div>`;
929
- }
930
- box.innerHTML = `<div class="sec-tag" style="margin-bottom:10px">Где работать</div>
931
- <div style="display:flex;gap:14px;flex-wrap:wrap">${card('☁ В облаке', cloudBody)}${card('💻 На своём компьютере', deskBody)}</div>`;
932
-
933
- document.getElementById('cab-desktop-code')?.addEventListener('click', async () => {
934
- const out = document.getElementById('cab-desktop-code-out');
935
- try {
936
- const r = await fetch('/api/instances/desktop-code', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
937
- const j = await r.json();
938
- if (!r.ok) throw new Error(j.error || `Ошибка ${r.status}`);
939
- out.innerHTML = `Код: <b style="font-family:monospace;font-size:18px;letter-spacing:1px">${escH(j.code)}</b> — введите его в программе на компьютере в течение 10 минут.`;
940
- } catch (e) { out.textContent = e.message; }
941
- });
942
- box.querySelectorAll('[data-unbind]').forEach((b) => b.addEventListener('click', async () => {
943
- if (!confirm('Отвязать этот компьютер? Программа на нём перестанет торговать, на новом компьютере понадобится новый код активации.')) return;
944
- const r = await fetch(`/api/instances/${encodeURIComponent(b.dataset.unbind)}/unbind`, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
945
- if (!r.ok) alert((await r.json().catch(() => ({}))).error || 'Ошибка');
946
- renderWorkspace();
947
- }));
948
- }
949
-
950
- document.getElementById('oauth-btns')?.addEventListener('click', (e) => {
951
- const b = e.target.closest('button[data-oauth]');
952
- if (!b) return;
953
- const prov = b.dataset.oauth;
954
- location.href = `/api/auth/oauth/${encodeURIComponent(prov)}/start${pendingPlan ? `?plan=${encodeURIComponent(pendingPlan)}` : ''}`;
955
- });
956
-
957
- let tgPollTimer = null;
958
- async function startBotLogin(channel) {
959
- try {
960
- const res = await fetch('/api/auth/telegram/start', {
961
- method: 'POST',
962
- headers: { 'Content-Type': 'application/json' },
963
- body: JSON.stringify({ channel }),
964
- });
965
- const j = await res.json();
966
- if (!res.ok) throw new Error(j.error || 'Не удалось запустить вход через мессенджер');
967
- window.open(j.botUrl, '_blank');
968
- const lbl = document.getElementById(channel === 'max' ? 'btn-max-bot-lbl' : 'btn-tg-bot-lbl');
969
- if (lbl) lbl.textContent = `Нажмите «Начать / Запустить» в открывшемся ${channel === 'max' ? 'MAX' : 'Telegram'}…`;
970
- clearInterval(tgPollTimer);
971
- tgPollTimer = setInterval(async () => {
972
- const pr = await fetch(`/api/auth/telegram/poll?code=${encodeURIComponent(j.code)}`).then((r) => r.json()).catch(() => ({}));
973
- if (pr.status === 'done' && pr.user) {
974
- clearInterval(tgPollTimer);
975
- currentUser = pr.user;
976
- document.getElementById('auth-modal').hidden = true;
977
- document.getElementById('btn-login-open').textContent = `${currentUser.name} · ${currentUser.planName}`;
978
- renderCabinet();
979
- if (pendingPlan) {
980
- const p = pendingPlan;
981
- pendingPlan = null;
982
- await startCheckout(p);
983
- } else {
984
- document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
985
- }
986
- } else if (pr.status === 'expired') {
987
- clearInterval(tgPollTimer);
988
- if (lbl) lbl.textContent = channel === 'max' ? 'Войти в 1 клик через мессенджер MAX' : 'Войти в 1 клик через Telegram-бота';
989
- }
990
- }, 2000);
991
- } catch (e) {
992
- document.getElementById('auth-err').textContent = e.message;
993
- }
994
- }
995
- document.getElementById('btn-tg-bot-login')?.addEventListener('click', () => startBotLogin('tg'));
996
- document.getElementById('btn-max-bot-login')?.addEventListener('click', () => startBotLogin('max'));
997
-
998
- async function loadShowcase() {
999
- try {
1000
- const me = await (await fetch('/api/auth/me')).json();
1001
- currentUser = me.user;
1002
- let hasAnyFastLogin = false;
1003
- if (me.notify?.tgBotName && me.notify?.hasTgToken) {
1004
- document.getElementById('tg-bot-login-box').hidden = false;
1005
- hasAnyFastLogin = true;
1006
- }
1007
- if (me.notify?.maxBotName && me.notify?.hasMaxToken) {
1008
- document.getElementById('max-bot-login-box').hidden = false;
1009
- hasAnyFastLogin = true;
1010
- }
1011
- const oa = me.oauth || {};
1012
- let enabledOauthCount = 0;
1013
- document.querySelectorAll('#oauth-btns [data-oauth]').forEach((b) => {
1014
- const on = Boolean(oa[b.dataset.oauth]?.enabled);
1015
- b.hidden = !on;
1016
- if (on) enabledOauthCount++;
1017
- });
1018
- if (enabledOauthCount > 0) {
1019
- document.getElementById('oauth-btns').hidden = false;
1020
- hasAnyFastLogin = true;
1021
- }
1022
- document.getElementById('oauth-or-sep').hidden = !hasAnyFastLogin;
1023
-
1024
- const up = new URLSearchParams(location.search);
1025
- if (up.get('oauth_error')) {
1026
- openAuth(false);
1027
- document.getElementById('auth-err').textContent = up.get('oauth_error');
1028
- }
1029
- if (currentUser) {
1030
- document.getElementById('btn-login-open').textContent = `${currentUser.name} · ${currentUser.planName}`;
1031
- renderCabinet();
1032
- if (up.get('buy_plan')) {
1033
- startCheckout(up.get('buy_plan'));
1034
- } else if (up.get('oauth_ok') === '1') {
1035
- document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
1036
- }
1037
- }
1038
- const { runs } = await (await fetch('/api/public/showcase')).json();
1039
- const box = document.getElementById('showcase-list');
1040
- if (!runs?.length) return;
1041
- const uniqueRuns = [];
1042
- const seenRet = new Set();
1043
- for (const x of runs) {
1044
- const rk = Math.round(Number(x.metrics?.returnPct || 0));
1045
- if (seenRet.has(rk)) continue;
1046
- seenRet.add(rk);
1047
- uniqueRuns.push(x);
1048
- if (uniqueRuns.length >= 3) break;
1049
- }
1050
- uniqueRuns.sort((a, b) => Number(a.metrics?.returnPct || 0) - Number(b.metrics?.returnPct || 0));
1051
- box.innerHTML = uniqueRuns.map((r, idx) => {
1052
- const from = r.period?.from || 0, till = r.period?.till || 0;
1053
- const years = Math.max(0.5, (till - from) / (365.25 * 86400000));
1054
- const ret = Number(r.metrics.returnPct || 0);
1055
- const cagr = years >= 0.25 && ret > -99 ? ((Math.pow(1 + ret / 100, 1 / years) - 1) * 100) : (ret / years);
1056
- const mktPct = Number.isFinite(Number(r.metrics.marketPct)) ? Number(r.metrics.marketPct) : null;
1057
- const isLong = r.params.modelDirection === 'long';
1058
- const tColor = r.tierColor || (isLong ? 'blue' : 'indigo');
1059
- const tBadge = r.tierBadge || (isLong ? 'Для тарифа «Инвестор»' : 'Для тарифов «Профи» и «VIP»');
1060
- const tNote = r.tierNote || (isLong ? 'Только покупки (без шорта)' : 'Покупки и продажи без покрытия (шорт)');
1061
- // Главная цифра — реалистичная оценка стресс-теста (исполнение как в живом контуре, издержки 0,10% на сторону)
1062
- const rl = r.realistic;
1063
- const badgeStyle = tColor === 'gold'
1064
- ? 'background:rgba(245,158,11,0.18);border:1px solid rgba(245,158,11,0.5);color:#fbbf24'
1065
- : tColor === 'indigo'
1066
- ? 'background:rgba(99,102,241,0.2);border:1px solid rgba(129,140,248,0.5);color:#a5b4fc'
1067
- : 'background:rgba(59,130,246,0.16);border:1px solid rgba(96,165,250,0.45);color:#93c5fd';
1068
- const cardBorder = tColor === 'gold' ? 'border-color:rgba(245,158,11,0.4)' : '';
1069
- return `<div class="sc-card" style="${cardBorder}">
1070
- <div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:8px">
1071
- <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>
1072
- <span style="font-size:12px;color:var(--muted)">Бумаг в портфеле: ${r.params.modelTopK || 2}</span>
1073
- </div>
1074
- <div class="sc-head">
1075
- <div>
1076
- <div style="font-weight:700;font-size:17px">${r.title}</div>
1077
- <div style="font-size:12.5px;color:var(--muted);margin-top:3px">${tNote}</div>
1078
- </div>
1079
- <div style="text-align:right;flex:none">
1080
- ${rl ? `<div class="sc-ret ${rl.cagrPct >= 0 ? 'pos' : 'neg'}">${fmt(rl.cagrPct, 1)}% <span style="font-size:13px;font-weight:600">/ год</span></div>
1081
- <div style="font-size:12px;color:var(--muted)">реалистичная оценка на истории за ${years.toFixed(1).replace('.', ',')} г.</div>`
1082
- : `<div style="font-size:13px;color:var(--muted)">реалистичная оценка<br>ещё не рассчитана</div>`}
1083
- </div>
1084
- </div>
1085
- ${sparkSvg(r.curve, r.marketCurve, idx)}
1086
- <div class="sc-kpis">
1087
- <div><span class="muted">Индекс Мосбиржи</span><b class="${(mktPct ?? 0) >= 0 ? 'pos' : 'neg'}">${mktPct === null ? '—' : `${fmt(mktPct, 1)}%`}</b></div>
1088
- <div><span class="muted">Макс. просадка</span><b>${fmt(rl ? rl.maxDrawdownPct : r.metrics.maxDrawdownPct, 1).replace('+','')}%</b></div>
1089
- <div><span class="muted">На 1 ₽ убытка</span><b>${fmt(rl ? rl.profitFactor : r.metrics.profitFactor, 2).replace('+','')} ₽</b></div>
1090
- </div>
1091
- <div style="font-size:11.5px;color:var(--muted);line-height:1.45;margin:6px 0 10px">Историческая проверка (бэктест) с учётом реальных издержек 0,10% на сторону, задержки исполнения и ликвидности. График — модельный расчёт без этих поправок. Результаты в прошлом не гарантируют доходность в будущем; возможны убытки.</div>
1092
- <a class="btn" href="#pricing">Выбрать этот профиль в облаке →</a>
1093
- </div>`;
1094
- }).join('');
1095
- } catch {}
1096
- }
1097
-
1098
- function updatePrices() {
1099
- const disc = selectedMonths >= 12 ? 0.8 : selectedMonths >= 3 ? 0.9 : 1;
1100
- const inv = Math.round(3900 * selectedMonths * disc);
1101
- const pro = Math.round(9900 * selectedMonths * disc);
1102
- const lbl = selectedMonths === 1 ? '/ мес' : `за ${selectedMonths} мес.`;
1103
- document.getElementById('price-investor').innerHTML = `${inv.toLocaleString('ru-RU')} ₽ <span>${lbl}</span>`;
1104
- document.getElementById('price-pro').innerHTML = `${pro.toLocaleString('ru-RU')} ₽ <span>${lbl}</span>`;
1105
- }
1106
-
1107
- document.getElementById('months-seg').addEventListener('click', (e) => {
1108
- const b = e.target.closest('button[data-m]');
1109
- if (!b) return;
1110
- selectedMonths = Number(b.dataset.m);
1111
- document.querySelectorAll('#months-seg button').forEach((x) => x.classList.toggle('active', x === b));
1112
- updatePrices();
1113
- });
1114
-
1115
- function openAuth(reg = false) {
1116
- isReg = reg;
1117
- document.getElementById('auth-modal').hidden = false;
1118
- document.getElementById('auth-name').hidden = !reg;
1119
- document.getElementById('auth-title').textContent = reg ? 'Регистрация в НейроАльфа' : 'Вход в НейроАльфа';
1120
- document.getElementById('auth-submit').textContent = reg ? 'Создать аккаунт' : 'Войти';
1121
- document.getElementById('auth-toggle').textContent = reg ? 'Уже есть аккаунт? Войти' : 'Нет аккаунта? Зарегистрироваться';
1122
- document.getElementById('auth-err').textContent = '';
1123
- }
1124
-
1125
- document.getElementById('btn-login-open').addEventListener('click', () => openAuth(false));
1126
- document.getElementById('btn-hero-start').addEventListener('click', () => currentUser ? document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'center' }) : openAuth(true));
1127
- document.getElementById('btn-free-start')?.addEventListener('click', () => currentUser ? document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'center' }) : openAuth(true));
1128
- document.getElementById('auth-close').addEventListener('click', () => { document.getElementById('auth-modal').hidden = true; });
1129
- document.getElementById('auth-toggle').addEventListener('click', () => openAuth(!isReg));
1130
-
1131
- let onOfferAccepted = null;
1132
- const offerModal = document.getElementById('offer-modal');
1133
- const offerChk1 = document.getElementById('offer-chk-1');
1134
- const offerChk2 = document.getElementById('offer-chk-2');
1135
- const offerAcceptBtn = document.getElementById('btn-offer-accept');
1136
-
1137
- function syncOfferBtn() {
1138
- if (offerAcceptBtn) offerAcceptBtn.disabled = !(offerChk1?.checked && offerChk2?.checked);
1139
- }
1140
- offerChk1?.addEventListener('change', syncOfferBtn);
1141
- offerChk2?.addEventListener('change', syncOfferBtn);
1142
-
1143
- function requireOfferAgreement(actionFn) {
1144
- onOfferAccepted = actionFn;
1145
- if (offerChk1) offerChk1.checked = false;
1146
- if (offerChk2) offerChk2.checked = false;
1147
- syncOfferBtn();
1148
- offerModal.hidden = false;
1149
- }
1150
-
1151
- document.getElementById('btn-offer-decline')?.addEventListener('click', () => {
1152
- offerModal.hidden = true;
1153
- onOfferAccepted = null;
1154
- });
1155
- offerAcceptBtn?.addEventListener('click', async () => {
1156
- offerAcceptBtn.disabled = true;
1157
- try {
1158
- await fetch('/api/auth/accept-offer', { method: 'POST' }).catch(() => {});
1159
- offerModal.hidden = true;
1160
- if (onOfferAccepted) await onOfferAccepted();
1161
- } finally {
1162
- onOfferAccepted = null;
1163
- syncOfferBtn();
1164
- }
1165
- });
1166
-
1167
- async function startCheckout(plan) {
1168
- requireOfferAgreement(async () => {
1169
- try {
1170
- const res = await fetch('/api/billing/checkout', {
1171
- method: 'POST',
1172
- headers: { 'Content-Type': 'application/json' },
1173
- body: JSON.stringify({ plan, months: selectedMonths, returnOrigin: location.origin }),
1174
- });
1175
- const j = await res.json();
1176
- if (!res.ok) throw new Error(j.error || 'Ошибка перехода к оплате');
1177
- if (j.confirmationUrl) location.href = j.confirmationUrl;
1178
- } catch (e) {
1179
- alert(e.message);
1180
- }
1181
- });
1182
- }
1183
-
1184
- document.getElementById('btn-vip-req')?.addEventListener('click', () => {
1185
- document.getElementById('vip-modal').hidden = false;
1186
- });
1187
- document.getElementById('btn-vip-token-open')?.addEventListener('click', () => {
1188
- if (!currentUser) { openAuth(true); return; }
1189
- document.getElementById('vip-modal').hidden = false;
1190
- document.querySelector('#vip-token-form input[name=code]')?.focus();
1191
- });
1192
- document.getElementById('btn-vip-close')?.addEventListener('click', () => {
1193
- document.getElementById('vip-modal').hidden = true;
1194
- });
1195
- document.getElementById('vip-req-form')?.addEventListener('submit', async (e) => {
1196
- e.preventDefault();
1197
- const f = e.target.elements;
1198
- const res = await fetch('/api/auth/vip-request', {
1199
- method: 'POST',
1200
- headers: { 'Content-Type': 'application/json' },
1201
- body: JSON.stringify({ name: f.name.value, contact: f.contact.value, capitalRub: f.capitalRub.value }),
1202
- });
1203
- const j = await res.json();
1204
- if (!res.ok) return alert(j.error || 'Ошибка отправки заявки');
1205
- document.getElementById('vip-modal').hidden = true;
1206
- alert('Заявка на тариф VIP отправлена администратору! Мы свяжемся с вами для согласования условий и выдачи персонального VIP-токена.');
1207
- });
1208
- document.getElementById('vip-token-form')?.addEventListener('submit', (e) => {
1209
- e.preventDefault();
1210
- const code = e.target.elements.code.value.trim();
1211
- if (!code) return;
1212
- if (!currentUser) {
1213
- document.getElementById('vip-modal').hidden = true;
1214
- openAuth(true);
1215
- return;
1216
- }
1217
- document.getElementById('vip-modal').hidden = true;
1218
- requireOfferAgreement(async () => {
1219
- const r = await fetch('/api/auth/redeem-promo', {
1220
- method: 'POST',
1221
- headers: { 'Content-Type': 'application/json' },
1222
- body: JSON.stringify({ code }),
1223
- });
1224
- const j = await r.json();
1225
- if (!r.ok) return alert(j.error || 'Не удалось активировать VIP-токен');
1226
- currentUser = j.user;
1227
- renderCabinet();
1228
- alert(`Персональный VIP-токен активирован! Ваш тариф: ${currentUser.planName}`);
1229
- });
1230
- });
1231
-
1232
- document.querySelectorAll('[data-buy]').forEach((b) => b.addEventListener('click', () => {
1233
- const plan = b.dataset.buy;
1234
- if (!currentUser) {
1235
- pendingPlan = plan;
1236
- openAuth(true);
1237
- return;
1238
- }
1239
- startCheckout(plan);
1240
- }));
1241
-
1242
- document.getElementById('auth-form').addEventListener('submit', async (e) => {
1243
- e.preventDefault();
1244
- const f = e.target.elements;
1245
- const url = isReg ? '/api/auth/register' : '/api/auth/login';
1246
- try {
1247
- const res = await fetch(url, {
1248
- method: 'POST',
1249
- headers: { 'Content-Type': 'application/json' },
1250
- body: JSON.stringify({ name: f.name.value, email: f.email.value, password: f.password.value }),
1251
- });
1252
- const j = await res.json();
1253
- if (!res.ok) throw new Error(j.error || 'Ошибка авторизации');
1254
- currentUser = j.user;
1255
- document.getElementById('auth-modal').hidden = true;
1256
- document.getElementById('btn-login-open').textContent = `${currentUser.name} · ${currentUser.planName}`;
1257
- renderCabinet();
1258
- if (pendingPlan) {
1259
- const p = pendingPlan;
1260
- pendingPlan = null;
1261
- await startCheckout(p);
1262
- } else {
1263
- document.getElementById('cabinet-box')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
1264
- }
1265
- } catch (err) {
1266
- document.getElementById('auth-err').textContent = err.message;
1267
- }
1268
- });
1269
-
1270
- loadShowcase();
1271
- </script>
1272
- </body>
1273
- </html>
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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>