neuroalpha 0.3.17 → 0.5.1

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.
Files changed (39) hide show
  1. package/config/certs/russian_trusted_root_ca.pem +33 -0
  2. package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +4 -4
  3. package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -4
  4. package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +2 -2
  5. package/package.json +60 -53
  6. package/public/activate.html +36 -0
  7. package/public/activate.js +33 -0
  8. package/public/admin.html +126 -7
  9. package/public/app.js +573 -66
  10. package/public/index.html +48 -21
  11. package/public/landing.html +106 -37
  12. package/public/legal.html +4 -4
  13. package/public/styles.css +152 -36
  14. package/server/api/routes.js +308 -37
  15. package/server/auth/index.js +79 -15
  16. package/server/brokers/index.js +1014 -168
  17. package/server/build-info.js +7 -0
  18. package/server/config.js +13 -6
  19. package/server/data/moex.js +6 -4
  20. package/server/db.js +22 -18
  21. package/server/desktop-entry.js +103 -0
  22. package/server/hub.js +28 -14
  23. package/server/index.js +82 -12
  24. package/server/instance/agent.js +259 -0
  25. package/server/instance/central.js +491 -0
  26. package/server/instance/identity.js +79 -0
  27. package/server/instance/provision.js +166 -0
  28. package/server/instance/signing.js +71 -0
  29. package/server/model/edge.js +19 -0
  30. package/server/model/forecast.js +107 -57
  31. package/server/model/live.js +1 -1
  32. package/server/model/models.js +12 -3
  33. package/server/model/research.js +218 -30
  34. package/server/notify/index.js +51 -16
  35. package/server/role.js +44 -0
  36. package/server/security.js +40 -0
  37. package/server/strategist/index.js +86 -47
  38. package/server/strategist/tools.js +231 -42
  39. package/server/tls-ca.js +32 -0
@@ -0,0 +1,33 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
3
+ PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
4
+ ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
5
+ Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
6
+ VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
7
+ YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
8
+ dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
9
+ qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
10
+ XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
11
+ zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
12
+ YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
13
+ Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
14
+ U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
15
+ 4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
16
+ G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
17
+ BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
18
+ ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
19
+ OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
20
+ BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
21
+ BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
22
+ AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
23
+ tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
24
+ W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
25
+ /3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
26
+ AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
27
+ C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
28
+ 4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
29
+ WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
30
+ D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
31
+ EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
32
+ 391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
33
+ -----END CERTIFICATE-----
@@ -8,11 +8,11 @@
8
8
 
9
9
  1. **Ваш Облачный Портал (`https://ваш-домен.ru`)**:
10
10
  * На главной странице открывается продающий **лендинг** с живой витриной стратегий и тремя коммерческими тарифами:
11
- * **«Инвестор» (`3 900 ₽ / мес`)** — задавленная доходность до 60% годовых, без шортов, 1 брокерский счёт со стартовым лимитом до 1 000 000 ₽ (+ 100% реинвестирование всей прибыли);
12
- * **«Профи» (`9 900 ₽ / мес`)** — задавленная доходность до 120% годовых, с шортами, до 5 брокерских счетов со стартовым лимитом до 1 000 000 ₽ на каждом (+ 100% реинвестирование всей прибыли);
13
- * **«VIP» (по индивидуальному согласованию)** — задавленная доходность до 240% годовых, с шортами, количество счетов и начальная сумма торговли по согласованию (активируется персональным `VIP-…` токеном от администратора).
11
+ * **«Инвестор» (`3 900 ₽ / мес`)** — без шортов, 1 брокерский счёт со стартовым лимитом до 1 000 000 ₽ (+ 100% реинвестирование всей прибыли);
12
+ * **«Профи» (`9 900 ₽ / мес`)** — с шортами, до 5 брокерских счетов со стартовым лимитом до 1 000 000 ₽ на каждом (+ 100% реинвестирование всей прибыли);
13
+ * **«VIP» (по индивидуальному согласованию)** — с шортами, количество счетов и начальная сумма торговли по согласованию (активируется персональным `VIP-…` токеном от администратора).
14
14
  * Посетитель регистрируется, принимает Публичную оферту и оплачивает тариф картой МИР или через СБП (ЮKassa), после чего подключает свои брокерские счета прямо в облачном веб-терминале (включая возможность безопасной проверки на песочнице брокера или в режиме «Только чтение»).
15
- * Для владельца / администратора платформы действует режим **«Хозяин» (`owner`)** — без ограничений по доходности (>400% годовых), числу счетов и сумме капитала, а также с эксклюзивным доступом к разделу **«Эксперименты»** и консоли администратора `/admin`.
15
+ * Для владельца / администратора платформы действует режим **«Хозяин» (`owner`)** — без ограничений по числу счетов и сумме капитала, а также с эксклюзивным доступом к разделу **«Эксперименты»** и консоли администратора `/admin`.
16
16
 
17
17
  ---
18
18
 
@@ -80,17 +80,14 @@
80
80
  Ядро стратегии работает полностью автоматически (`Окно обучения = 90 дней`, `Размерность карты = 0 Авто по Марченко–Пастуру`, `Dynamic Top-K = 0`, `Conviction Sizing`). На пользовательских тарифах мощность слота и направление торговли регулируются в соответствии с выбранным планом:
81
81
 
82
82
  - **Тариф «Инвестор» (`3 900 ₽ / мес`):**
83
- - Задавленная целевая доходность стратегии: **до 60% годовых**;
84
83
  - Торговля **только от покупки («Только в рост, без шорта»)**;
85
84
  - **1 брокерский счёт** со стартовым лимитом капитала **до 1 000 000 ₽** (+ 100% реинвестирование всей заработанной стратегией прибыли).
86
85
  - **Тариф «Профи» (`9 900 ₽ / мес`):**
87
- - Задавленная целевая доходность стратегии: **до 120% годовых**;
88
86
  - Торговля **в рост и в падение (с шортами)**;
89
87
  - **До 5 брокерских счетов** со стартовым лимитом **до 1 000 000 ₽** на каждом (+ 100% реинвестирование всей заработанной прибыли).
90
88
  - **Тариф «VIP» (по индивидуальному согласованию):**
91
- - Задавленная целевая доходность стратегии: **до 240% годовых**;
92
89
  - Торговля **в рост и в падение (с шортами)** с повышенной концентрацией слота;
93
90
  - Количество брокерских счетов и начальная сумма торговли определяются **по индивидуальному согласованию**.
94
91
  - **Режим «Хозяин» (`owner` — для владельца / администратора платформы):**
95
- - **Без ограничений** по годовой доходности (полный форсаж ядра **>400% годовых**, внутричасовой перехват `H/5` каждые 30 минут + динамическая концентрация слота `Conviction Sizing` до 75% капитала), числу счетов и сумме капитала;
92
+ - **Без ограничений** по размеру позиции (внутричасовой перехват `H/5` каждые 30 минут, `Conviction Sizing` до 75% капитала), числу счетов и сумме капитала;
96
93
  - Эксклюзивный доступ к разделу **«Эксперименты»**.
@@ -1,6 +1,6 @@
1
1
  # Устройство и математические принципы платформы «НейроАльфа»
2
2
 
3
- > Подробное руководство простым языком: как работает концепция «Рынок как язык», откуда берётся устойчивая доходность >400% годовых (CAGR 419,6% на эталонном 1000-дневном окне), как устроена спектральная авто-калибровка Марченко–Пастура, все механики самообучения, учёт цикличности и параметры системы.
3
+ > Подробное руководство простым языком: как работает концепция «Рынок как язык», как оценивается реалистичная доходность (стресс-тест исполнения встроен в каждое исследование),6% на эталонном 1000-дневном окне), как устроена спектральная авто-калибровка Марченко–Пастура, все механики самообучения, учёт цикличности и параметры системы.
4
4
 
5
5
  ---
6
6
 
@@ -118,7 +118,7 @@ $$\text{Edge}_i = \hat{y}_i \cdot \sigma_i \cdot \sqrt{H}$$
118
118
 
119
119
  ## 6. Автоматическое ядро и настройки издержек
120
120
 
121
- Параметры ядра обучения и стратегии зафиксированы в эталонном автоматическом режиме (>400% годовых CAGR):
121
+ Параметры ядра обучения и стратегии зафиксированы в эталонном автоматическом режиме:
122
122
  * **Окно обучения (`modelTrainDays`)**: `90` торговых дней.
123
123
  * **Размерность карты рынка (`modelEmbedDim`)**: `0 (Авто)` — спектральная авто-калибровка по порогу Марченко–Пастура на каждом шаге.
124
124
  * **Период исследования (`modelHistoryDays`)**: `1000` календарных дней.
package/package.json CHANGED
@@ -1,53 +1,60 @@
1
- {
2
- "name": "neuroalpha",
3
- "version": "0.3.17",
4
- "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
- "author": "Roman A. Perepelkin",
6
- "license": "UNLICENSED",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/proman1968/trading.git"
10
- },
11
- "homepage": "https://github.com/proman1968/trading#readme",
12
- "bugs": {
13
- "url": "https://github.com/proman1968/trading/issues"
14
- },
15
- "type": "module",
16
- "main": "server/index.js",
17
- "bin": {
18
- "neuroalpha": "bin/cli.js",
19
- "trading-consortium": "bin/cli.js"
20
- },
21
- "files": [
22
- "LICENSE",
23
- "bin/",
24
- "deploy/",
25
- "server/",
26
- "public/",
27
- "config/",
28
- "docs/"
29
- ],
30
- "scripts": {
31
- "start": "node --disable-warning=ExperimentalWarning server/index.js",
32
- "dev": "node --watch --disable-warning=ExperimentalWarning server/index.js",
33
- "cli": "node bin/cli.js"
34
- },
35
- "keywords": [
36
- "moex",
37
- "trading",
38
- "algotrading",
39
- "ai",
40
- "neuroalpha",
41
- "tbank",
42
- "finam",
43
- "alor",
44
- "bcs"
45
- ],
46
- "engines": {
47
- "node": ">=22.13"
48
- },
49
- "dependencies": {
50
- "express": "^4.21.2",
51
- "ws": "^8.18.0"
52
- }
53
- }
1
+ {
2
+ "name": "neuroalpha",
3
+ "version": "0.5.1",
4
+ "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
+ "author": "Roman A. Perepelkin",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/proman1968/trading.git"
10
+ },
11
+ "homepage": "https://github.com/proman1968/trading#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/proman1968/trading/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "server/index.js",
17
+ "bin": {
18
+ "neuroalpha": "bin/cli.js",
19
+ "trading-consortium": "bin/cli.js"
20
+ },
21
+ "files": [
22
+ "LICENSE",
23
+ "bin/",
24
+ "deploy/install.sh",
25
+ "server/",
26
+ "public/",
27
+ "config/",
28
+ "docs/"
29
+ ],
30
+ "scripts": {
31
+ "start": "node --disable-warning=ExperimentalWarning server/index.js",
32
+ "dev": "node --watch --disable-warning=ExperimentalWarning server/index.js",
33
+ "cli": "node bin/cli.js",
34
+ "build:desktop": "node scripts/build-desktop.mjs"
35
+ },
36
+ "keywords": [
37
+ "moex",
38
+ "trading",
39
+ "algotrading",
40
+ "ai",
41
+ "neuroalpha",
42
+ "tbank",
43
+ "finam",
44
+ "alor",
45
+ "bcs"
46
+ ],
47
+ "engines": {
48
+ "node": ">=22.13"
49
+ },
50
+ "dependencies": {
51
+ "express": "^4.21.2",
52
+ "ws": "^8.18.0"
53
+ },
54
+ "devDependencies": {
55
+ "esbuild": "^0.28.2",
56
+ "javascript-obfuscator": "^5.9.0",
57
+ "marked": "^18.1.1",
58
+ "postject": "^1.0.0-alpha.6"
59
+ }
60
+ }
@@ -0,0 +1,36 @@
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
+ <link rel="stylesheet" href="styles.css">
9
+ <style>
10
+ body { overflow: auto; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 16px; }
11
+ .act-box { width: min(480px, 100%); background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 26px 26px 22px; }
12
+ .act-box h1 { font-size: 19px; margin: 0 0 6px; }
13
+ .act-box ol { padding-left: 20px; color: var(--muted); font-size: 13.5px; line-height: 1.6; margin: 12px 0 16px; }
14
+ #act-code { width: 100%; font-size: 20px; letter-spacing: 2px; text-align: center; text-transform: uppercase; padding: 10px; font-family: monospace; }
15
+ #act-btn { width: 100%; margin-top: 12px; padding: 9px; font-size: 14px; }
16
+ #act-msg { margin-top: 12px; font-size: 13px; min-height: 18px; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div class="act-box">
21
+ <h1><span class="logo">◈</span> НейроАльфа на вашем компьютере</h1>
22
+ <div class="muted small">Программа установлена, но ещё не привязана к вашему договору.</div>
23
+ <ol>
24
+ <li>Откройте личный кабинет: <a id="act-central" href="#" target="_blank" rel="noopener">—</a></li>
25
+ <li>В блоке «Где работать» нажмите <b>«Получить код активации»</b>.</li>
26
+ <li>Введите код ниже. Код действует 10 минут и срабатывает один раз.</li>
27
+ </ol>
28
+ <form id="act-form" autocomplete="off">
29
+ <input id="act-code" name="code" placeholder="NA-XXXX-XXXX" maxlength="24" required spellcheck="false" aria-label="Код активации">
30
+ <button id="act-btn" type="submit" class="primary">Активировать</button>
31
+ </form>
32
+ <div id="act-msg" class="muted" role="status"></div>
33
+ </div>
34
+ <script src="activate.js"></script>
35
+ </body>
36
+ </html>
@@ -0,0 +1,33 @@
1
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
+ // Экран активации установки на компьютер кодом из личного кабинета
3
+ (async () => {
4
+ const $ = (s) => document.querySelector(s);
5
+ if (localStorage.getItem('na_theme') === 'light') document.body.classList.add('light');
6
+ const msg = (text, cls = 'muted') => { $('#act-msg').className = cls; $('#act-msg').textContent = text; };
7
+ try {
8
+ const st = await fetch('/api/local/status').then((r) => r.json());
9
+ if (st.activated) { location.href = '/'; return; }
10
+ $('#act-central').textContent = st.centralUrl;
11
+ $('#act-central').href = `${st.centralUrl}/landing?login=1`;
12
+ } catch { /* ignore */ }
13
+ $('#act-form').addEventListener('submit', async (e) => {
14
+ e.preventDefault();
15
+ const btn = $('#act-btn');
16
+ btn.disabled = true;
17
+ msg('Проверяем код в центре…');
18
+ try {
19
+ const r = await fetch('/api/local/activate', {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({ code: $('#act-code').value.trim() }),
23
+ });
24
+ const j = await r.json().catch(() => ({}));
25
+ if (!r.ok) throw new Error(j.error || `Ошибка ${r.status}`);
26
+ msg(`✓ Программа активирована (${j.id}). Открываем терминал…`, 'pos');
27
+ setTimeout(() => { location.href = '/'; }, 900);
28
+ } catch (err) {
29
+ msg(err.message, 'neg');
30
+ btn.disabled = false;
31
+ }
32
+ });
33
+ })();
package/public/admin.html CHANGED
@@ -89,6 +89,7 @@
89
89
 
90
90
  <nav class="tabs" id="adm-tabs">
91
91
  <button type="button" data-tab="users" class="active">👥 Пользователи и тарифы</button>
92
+ <button type="button" data-tab="instances">🖥 Машины клиентов</button>
92
93
  <button type="button" data-tab="billing">💳 ЮKassa, Платежи и Промокоды</button>
93
94
  <button type="button" data-tab="oauth">🔑 OAuth-вход (Яндекс / Сбер / T-ID / VK)</button>
94
95
  <button type="button" data-tab="bots">🤖 Боты платформы и Реквизиты оферты</button>
@@ -108,6 +109,30 @@
108
109
  </div>
109
110
  </section>
110
111
 
112
+ <!-- Вкладка: Машины клиентов -->
113
+ <section data-pane="instances" hidden>
114
+ <div class="card">
115
+ <h2>Арендованные машины клиентов</h2>
116
+ <p class="sub">Облачная машина — отдельный namespace в Kubernetes на поддомене <code>naXXXXXX.odant.org</code>; при включённом развёртывании (<code>PAAS_DRIVER</code>) создаётся автоматически после оплаты. Установка на компьютер (VIP по договору) активируется клиентом одноразовым кодом из кабинета и привязывается к этому компьютеру. Раз в минуту машина присылает техническое состояние и получает подписанную лицензию на 72 ч. «Пауза» — новые заявки не отправляются, защитные стопы работают; «Стоп» — запрещены любые заявки.</p>
117
+ <form id="form-instance" class="row-gap" style="flex-wrap:wrap;margin-bottom:14px">
118
+ <select name="userId" style="width:300px" required></select>
119
+ <select name="kind" style="width:200px">
120
+ <option value="cloud">☁ Облако (Kubernetes)</option>
121
+ <option value="desktop">💻 Компьютер клиента (VIP)</option>
122
+ </select>
123
+ <input name="id" placeholder="ID машины (пусто — авто naXXXXXX)" style="width:240px">
124
+ <input name="note" placeholder="Заметка (тариф, узел кластера…)" style="width:240px">
125
+ <button type="submit" class="primary">Создать машину</button>
126
+ </form>
127
+ <div id="instance-secret" hidden style="margin-bottom:14px">
128
+ <p class="sub" style="margin:0 0 6px"><b style="color:var(--yellow,#d29922)">Ключ машины показывается один раз.</b> Сохраните переменные в Secret Kubernetes (пример манифеста — <code>deploy/k8s/instance.yaml</code>):</p>
129
+ <textarea id="instance-env" readonly rows="7" style="width:100%;font-family:monospace;font-size:12px"></textarea>
130
+ <button type="button" class="btn" id="btn-copy-env" style="margin-top:6px">Скопировать</button>
131
+ </div>
132
+ <div id="instances-wrap">Загрузка…</div>
133
+ </div>
134
+ </section>
135
+
111
136
  <!-- Вкладка 2: ЮKassa и Промокоды -->
112
137
  <section data-pane="billing" hidden>
113
138
  <div class="card">
@@ -132,13 +157,13 @@
132
157
 
133
158
  <div class="card">
134
159
  <h2>Персональные VIP-токены и Промокоды</h2>
135
- <p class="sub">Сгенерируйте персональный <code>VIP-…</code> токен (тариф VIP — до 240% годовых, лимиты по согласованию) или промокод для тарифов «Инвестор» / «Профи».</p>
160
+ <p class="sub">Сгенерируйте персональный <code>VIP-…</code> токен (тариф VIP — лимиты по договору) или промокод для тарифов «Инвестор» / «Профи».</p>
136
161
  <form id="form-promo" class="row-gap" style="flex-wrap:wrap;margin-bottom:14px">
137
162
  <input name="code" placeholder="Код (или пусто для авто VIP-…)" style="width:220px">
138
163
  <select name="plan" style="width:210px">
139
- <option value="vip">👑 Тариф «VIP» (до 240%/год)</option>
140
- <option value="pro">Тариф «Профи» (до 120%/год)</option>
141
- <option value="investor">Тариф «Инвестор» (до 60%/год)</option>
164
+ <option value="vip">👑 Тариф «VIP»</option>
165
+ <option value="pro">Тариф «Профи»</option>
166
+ <option value="investor">Тариф «Инвестор»</option>
142
167
  </select>
143
168
  <input name="days" type="number" value="365" min="1" max="3650" style="width:110px" title="Длительность, дней" placeholder="Дней">
144
169
  <input name="maxUses" type="number" value="1" min="1" max="10000" style="width:120px" title="Лимит активаций" placeholder="Лимит">
@@ -270,9 +295,9 @@
270
295
  <td>
271
296
  <select data-user-plan="${u.id}">
272
297
  <option value="none"${u.plan === 'none' || u.plan === 'free' ? ' selected' : ''}>Без подписки</option>
273
- <option value="investor"${u.plan === 'investor' ? ' selected' : ''}>Инвестор (до 60%/год · 1 счёт)</option>
274
- <option value="pro"${u.plan === 'pro' ? ' selected' : ''}>Профи (до 120%/год · 5 счетов)</option>
275
- <option value="vip"${u.plan === 'vip' ? ' selected' : ''}>👑 VIP (до 240%/год)</option>
298
+ <option value="investor"${u.plan === 'investor' ? ' selected' : ''}>Инвестор (1 счёт)</option>
299
+ <option value="pro"${u.plan === 'pro' ? ' selected' : ''}>Профи (5 счетов)</option>
300
+ <option value="vip"${u.plan === 'vip' ? ' selected' : ''}>👑 VIP</option>
276
301
  <option value="owner"${u.plan === 'owner' ? ' selected' : ''}>⚡ Хозяин (Без ограничений)</option>
277
302
  </select>
278
303
  </td>
@@ -303,10 +328,104 @@
303
328
  $('#kpi-bots').textContent = fmtNum(users.filter((u) => u.tgConnected || u.maxConnected).length);
304
329
  }
305
330
 
331
+ // ---------- машины клиентов ----------
332
+ let instances = [];
333
+ const tradingLabel = { active: '<span class="pos">торгует</span>', paused: '<span style="color:#d29922">пауза</span>', halted: '<span class="neg">аварийная остановка</span>' };
334
+ function renderInstances() {
335
+ const sel = $('#form-instance').elements.userId;
336
+ sel.innerHTML = (admState?.users || []).map((u) => `<option value="${u.id}">#${u.id} · ${esc(u.name)} (${esc(u.email)}) · ${esc(u.planName)}</option>`).join('');
337
+ if (!instances.length) {
338
+ $('#instances-wrap').innerHTML = '<p class="muted small">Машин пока нет.</p>';
339
+ return;
340
+ }
341
+ $('#instances-wrap').innerHTML = `<table>
342
+ <thead><tr><th>Машина</th><th>Клиент</th><th>Связь</th><th>Версия</th><th>Состояние</th><th>Торговля</th><th>Действия</th></tr></thead>
343
+ <tbody>${instances.map((i) => {
344
+ const s = i.status || {};
345
+ const ago = i.last_seen ? Math.round((Date.now() - i.last_seen) / 1000) : null;
346
+ const prov = { pending: 'ожидает развёртывания', deploying: 'разворачивается…', deployed: 'развёрнута, ждём связь', running: '', error: 'ошибка развёртывания' }[i.provision_state] || '';
347
+ const where = i.kind === 'desktop'
348
+ ? `💻 ${i.installBound ? esc(i.hostname || 'компьютер') : '<span style="color:#d29922">не активирована</span>'}`
349
+ : `☁ <a href="${esc(i.url)}" target="_blank" rel="noopener">${esc(i.host)}</a>`;
350
+ return `<tr>
351
+ <td><b>${esc(i.id)}</b><div class="muted small">${where}${i.note ? ` · ${esc(i.note)}` : ''}</div>${prov ? `<div class="small ${i.provision_state === 'error' ? 'neg' : 'muted'}" title="${esc(i.provision_error || '')}">${prov}</div>` : ''}</td>
352
+ <td>${esc(i.userName)}<div class="muted small">${esc(i.userEmail)}</div></td>
353
+ <td class="small">${i.revoked ? '<span class="neg">аренда завершена</span>' : i.online ? '<span class="pos">онлайн</span>' : '<span class="neg">нет связи</span>'}<div class="muted small">${ago === null ? 'ни разу' : `${ago < 120 ? `${ago} с` : `${Math.round(ago / 60)} мин`} назад`}</div></td>
354
+ <td class="small">${esc(i.version || '—')}</td>
355
+ <td class="small">счетов: <b>${s.accounts ?? '—'}</b>${s.autoAccounts ? ` (авто ${s.autoAccounts})` : ''}${s.haltedAccounts ? ` · <span class="neg">стоп ${s.haltedAccounts}</span>` : ''}<div class="muted small">${s.tradingAllowed === false ? esc(s.tradingReason || 'заявки приостановлены') : s.tradingAllowed ? 'заявки разрешены' : ''}${s.rssMb ? ` · RAM ${s.rssMb} МБ` : ''}${s.dbMb ? ` · БД ${s.dbMb} МБ` : ''}</div>${(s.errors || []).length ? `<div class="neg small" title="${esc(s.errors.join('\n'))}">ошибки: ${s.errors.length}</div>` : ''}</td>
356
+ <td class="small">${tradingLabel[i.trading] || esc(i.trading)}</td>
357
+ <td class="small" style="white-space:nowrap">
358
+ ${i.trading !== 'active' ? `<button type="button" class="btn" data-inst-trading="active" data-inst="${esc(i.id)}">▶ Возобновить</button>` : ''}
359
+ ${i.trading !== 'paused' ? `<button type="button" class="btn" data-inst-trading="paused" data-inst="${esc(i.id)}">⏸ Пауза</button>` : ''}
360
+ ${i.trading !== 'halted' ? `<button type="button" class="btn" data-inst-trading="halted" data-inst="${esc(i.id)}">⛔ Стоп</button>` : ''}
361
+ ${i.kind === 'desktop'
362
+ ? (i.installBound ? `<button type="button" class="btn" data-inst-unbind="${esc(i.id)}">Отвязать компьютер</button>` : '')
363
+ : `<button type="button" class="btn" data-inst-rotate="${esc(i.id)}">🔑 Новый ключ</button>`}
364
+ <button type="button" class="btn" data-inst-revoke="${esc(i.id)}" data-revoked="${i.revoked ? 1 : 0}">${i.revoked ? 'Восстановить' : 'Завершить аренду'}</button>
365
+ </td>
366
+ </tr>`;
367
+ }).join('')}</tbody>
368
+ </table>`;
369
+ }
370
+ async function loadInstances() {
371
+ try { instances = (await api('/api/admin/instances')).instances || []; } catch (e) { instances = []; $('#instances-wrap').textContent = e.message; return; }
372
+ renderInstances();
373
+ }
374
+ function showInstanceSecret(r) {
375
+ if (!r.env) {
376
+ $('#instance-secret').hidden = true;
377
+ if (r.provisioned) toast('Ключ передан в кластер автоматически — машина перезапустится с новым ключом');
378
+ return;
379
+ }
380
+ $('#instance-secret').hidden = false;
381
+ $('#instance-env').value = r.env;
382
+ }
383
+ $('#btn-copy-env').addEventListener('click', async () => {
384
+ try { await navigator.clipboard.writeText($('#instance-env').value); toast('Скопировано'); } catch { $('#instance-env').select(); }
385
+ });
386
+ $('#form-instance').addEventListener('submit', async (e) => {
387
+ e.preventDefault();
388
+ const f = e.target.elements;
389
+ try {
390
+ const kind = f.kind.value;
391
+ const r = await api('/api/admin/instances', { method: 'POST', body: { userId: Number(f.userId.value), id: f.id.value.trim(), note: f.note.value.trim(), kind } });
392
+ // для компьютера ключ не нужен: клиент активирует установку кодом из кабинета
393
+ if (kind === 'cloud') showInstanceSecret(r); else $('#instance-secret').hidden = true;
394
+ f.id.value = ''; f.note.value = '';
395
+ await loadInstances();
396
+ toast(kind === 'desktop' ? `Установка ${r.instance.id} создана — клиент активирует её кодом из кабинета` : r.provisioned ? `Машина ${r.instance.id} разворачивается в кластере` : `Машина ${r.instance.id} создана`);
397
+ } catch (err) { toast(err.message); }
398
+ });
399
+ $('#instances-wrap').addEventListener('click', async (e) => {
400
+ const b = e.target.closest('button');
401
+ if (!b) return;
402
+ try {
403
+ if (b.dataset.instTrading) {
404
+ const t = b.dataset.instTrading;
405
+ if (t === 'halted' && !confirm(`Аварийно остановить ВСЮ торговлю на машине ${b.dataset.inst}, включая защитные стопы? Применится в течение минуты.`)) return;
406
+ await api(`/api/admin/instances/${encodeURIComponent(b.dataset.inst)}/trading`, { method: 'POST', body: { trading: t } });
407
+ toast('Команда отправится машине при следующей связи (до 1 мин)');
408
+ } else if (b.dataset.instRotate) {
409
+ if (!confirm(`Выпустить новый ключ для ${b.dataset.instRotate}? Старый перестанет работать сразу — обновите Secret в Kubernetes.`)) return;
410
+ showInstanceSecret(await api(`/api/admin/instances/${encodeURIComponent(b.dataset.instRotate)}/rotate-key`, { method: 'POST' }));
411
+ } else if (b.dataset.instUnbind) {
412
+ if (!confirm(`Отвязать компьютер от ${b.dataset.instUnbind}? Установка на нём сразу перестанет получать лицензию.`)) return;
413
+ await api(`/api/admin/instances/${encodeURIComponent(b.dataset.instUnbind)}/unbind`, { method: 'POST' });
414
+ } else if (b.dataset.instRevoke) {
415
+ const revoke = b.dataset.revoked !== '1';
416
+ if (revoke && !confirm(`Завершить аренду ${b.dataset.instRevoke}? Новые заявки будут остановлены, защитные стопы останутся.`)) return;
417
+ await api(`/api/admin/instances/${encodeURIComponent(b.dataset.instRevoke)}/revoke`, { method: 'POST', body: { revoked: revoke } });
418
+ }
419
+ await loadInstances();
420
+ } catch (err) { toast(err.message); }
421
+ });
422
+ setInterval(() => { if (!document.querySelector('section[data-pane="instances"]').hidden) loadInstances(); }, 30000);
423
+
306
424
  async function loadAdminState() {
307
425
  admState = await api('/api/admin/state');
308
426
  updateKpis();
309
427
  renderUsers();
428
+ loadInstances();
310
429
 
311
430
  // Billing
312
431
  $('#yoo-webhook').textContent = `${location.origin}/api/billing/yookassa/webhook`;