neuroalpha 0.3.16 → 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.
- package/README.md +0 -1
- package/config/certs/russian_trusted_root_ca.pem +33 -0
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +4 -4
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +2 -6
- 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
- package/package.json +60 -53
- package/public/activate.html +36 -0
- package/public/activate.js +33 -0
- package/public/admin.html +126 -7
- package/public/app.js +608 -182
- package/public/index.html +57 -47
- package/public/landing.html +106 -37
- package/public/legal.html +4 -4
- package/public/styles.css +152 -36
- package/server/api/routes.js +308 -37
- package/server/auth/index.js +79 -15
- package/server/brokers/index.js +1022 -172
- package/server/build-info.js +7 -0
- package/server/config.js +13 -6
- package/server/data/moex.js +6 -4
- package/server/db.js +22 -18
- package/server/desktop-entry.js +103 -0
- package/server/hub.js +28 -14
- package/server/index.js +82 -12
- package/server/instance/agent.js +259 -0
- package/server/instance/central.js +491 -0
- package/server/instance/identity.js +79 -0
- package/server/instance/provision.js +166 -0
- package/server/instance/signing.js +71 -0
- package/server/model/edge.js +19 -0
- package/server/model/forecast.js +107 -57
- package/server/model/live.js +1 -1
- package/server/model/models.js +12 -3
- package/server/model/research.js +218 -30
- package/server/notify/index.js +51 -16
- package/server/params-meta.js +3 -3
- package/server/role.js +44 -0
- package/server/security.js +40 -0
- package/server/strategist/index.js +462 -488
- package/server/strategist/tools.js +675 -532
- package/server/tls-ca.js +32 -0
package/README.md
CHANGED
|
@@ -56,7 +56,6 @@ MOEX ISS: ~100 акций MOEXBMI + IMOEX, 10-минутные свечи + жи
|
|
|
56
56
|
- **Новости рынка** — лента новостей и корпоративных раскрытий с ИИ-оценкой тональности и влияния на конкретные тикеры.
|
|
57
57
|
3. **Управление и настройки:**
|
|
58
58
|
- **Эксперименты** *(доступен эксклюзивно в режиме «Хозяин»)* — Walk-Forward проверка ядра и управление витриной.
|
|
59
|
-
- **Издержки и новости** — настройка комиссии брокера, проскальзывания, ставки за шорт и новостного фильтра.
|
|
60
59
|
- **Подписка и уведомления** — управление тарифом, привязка ботов Telegram и MAX для подтверждения заявок в 1 клик.
|
|
61
60
|
- **Модели ИИ** — подключение провайдеров ИИ для Стратега (OpenAI-совместимые, GigaChat с сертификатом НУЦ Минцифры, Ollama).
|
|
62
61
|
|
|
@@ -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 ₽ / мес`)** —
|
|
12
|
-
* **«Профи» (`9 900 ₽ / мес`)** —
|
|
13
|
-
* **«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`)** — без ограничений по
|
|
15
|
+
* Для владельца / администратора платформы действует режим **«Хозяин» (`owner`)** — без ограничений по числу счетов и сумме капитала, а также с эксклюзивным доступом к разделу **«Эксперименты»** и консоли администратора `/admin`.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md
CHANGED
|
@@ -29,10 +29,9 @@
|
|
|
29
29
|
- **Брокерские счета** — управление всеми подключёнными счетами (боевыми и тестовыми).
|
|
30
30
|
2. **Аналитика рынка:**
|
|
31
31
|
- **Прогноз сейчас** — живой радар ожиданий по 100 акциям Мосбиржи.
|
|
32
|
-
- **Новости рынка** — свежие новости и раскрытия компаний с оценкой влияния
|
|
32
|
+
- **Новости рынка** — свежие новости и раскрытия компаний с оценкой влияния ИИ и переключателем «Блокировать сделки против важных свежих новостей».
|
|
33
33
|
3. **Управление и настройки:**
|
|
34
34
|
- **Эксперименты** *(виден и доступен эксклюзивно в режиме «Хозяин»)* — исторические Walk-Forward исследования модели.
|
|
35
|
-
- **Издержки и новости** — настройка брокерской комиссии, проскальзывания, ставки за шорт и новостного фильтра.
|
|
36
35
|
- **Подписка и уведомления** — выбор тарифа и привязка ботов **Telegram / MAX**.
|
|
37
36
|
- **Модели ИИ** — подключение и выбор языковых моделей для Стратега.
|
|
38
37
|
|
|
@@ -81,17 +80,14 @@
|
|
|
81
80
|
Ядро стратегии работает полностью автоматически (`Окно обучения = 90 дней`, `Размерность карты = 0 Авто по Марченко–Пастуру`, `Dynamic Top-K = 0`, `Conviction Sizing`). На пользовательских тарифах мощность слота и направление торговли регулируются в соответствии с выбранным планом:
|
|
82
81
|
|
|
83
82
|
- **Тариф «Инвестор» (`3 900 ₽ / мес`):**
|
|
84
|
-
- Задавленная целевая доходность стратегии: **до 60% годовых**;
|
|
85
83
|
- Торговля **только от покупки («Только в рост, без шорта»)**;
|
|
86
84
|
- **1 брокерский счёт** со стартовым лимитом капитала **до 1 000 000 ₽** (+ 100% реинвестирование всей заработанной стратегией прибыли).
|
|
87
85
|
- **Тариф «Профи» (`9 900 ₽ / мес`):**
|
|
88
|
-
- Задавленная целевая доходность стратегии: **до 120% годовых**;
|
|
89
86
|
- Торговля **в рост и в падение (с шортами)**;
|
|
90
87
|
- **До 5 брокерских счетов** со стартовым лимитом **до 1 000 000 ₽** на каждом (+ 100% реинвестирование всей заработанной прибыли).
|
|
91
88
|
- **Тариф «VIP» (по индивидуальному согласованию):**
|
|
92
|
-
- Задавленная целевая доходность стратегии: **до 240% годовых**;
|
|
93
89
|
- Торговля **в рост и в падение (с шортами)** с повышенной концентрацией слота;
|
|
94
90
|
- Количество брокерских счетов и начальная сумма торговли определяются **по индивидуальному согласованию**.
|
|
95
91
|
- **Режим «Хозяин» (`owner` — для владельца / администратора платформы):**
|
|
96
|
-
- **Без ограничений** по
|
|
92
|
+
- **Без ограничений** по размеру позиции (внутричасовой перехват `H/5` каждые 30 минут, `Conviction Sizing` до 75% капитала), числу счетов и сумме капитала;
|
|
97
93
|
- Эксклюзивный доступ к разделу **«Эксперименты»**.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Устройство и математические принципы платформы «НейроАльфа»
|
|
2
2
|
|
|
3
|
-
> Подробное руководство простым языком: как работает концепция «Рынок как язык»,
|
|
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
|
-
Параметры ядра обучения и стратегии зафиксированы в эталонном автоматическом
|
|
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.
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
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 —
|
|
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
|
|
140
|
-
<option value="pro">Тариф
|
|
141
|
-
<option value="investor">Тариф
|
|
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' : ''}>Инвестор (
|
|
274
|
-
<option value="pro"${u.plan === 'pro' ? ' selected' : ''}>Профи (
|
|
275
|
-
<option value="vip"${u.plan === 'vip' ? ' selected' : ''}>👑 VIP
|
|
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`;
|