neuroalpha 0.6.12 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +14 -10
  2. package/bin/cli.js +79 -69
  3. package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +2 -2
  4. package/package.json +4 -5
  5. package/public/activate.html +41 -19
  6. package/public/activate.js +81 -10
  7. package/public/app.js +300 -152
  8. package/public/index.html +4 -37
  9. package/server/api/routes.js +570 -906
  10. package/server/auth/index.js +576 -761
  11. package/server/brokers/index.js +1 -4
  12. package/server/config.js +73 -53
  13. package/server/index.js +186 -187
  14. package/server/llm/local-engine.js +843 -0
  15. package/server/llm/providers.js +45 -34
  16. package/server/model/dataset.js +2 -4
  17. package/server/model/forecast.js +1 -3
  18. package/server/news/index.js +2 -3
  19. package/server/notify/index.js +304 -466
  20. package/deploy/docs/01-central.md +0 -96
  21. package/deploy/docs/02-cloud-instances.md +0 -130
  22. package/deploy/docs/03-desktop-build.md +0 -92
  23. package/deploy/docs/04-client-guide.md +0 -101
  24. package/deploy/docs/05-operations.md +0 -109
  25. package/deploy/docs/06-env-reference.md +0 -91
  26. package/deploy/docs/07-stress-test.md +0 -253
  27. package/deploy/docs/pdf/01-central.pdf +0 -0
  28. package/deploy/docs/pdf/02-cloud-instances.pdf +0 -0
  29. package/deploy/docs/pdf/03-desktop-build.pdf +0 -0
  30. package/deploy/docs/pdf/04-client-guide.pdf +0 -0
  31. package/deploy/docs/pdf/05-operations.pdf +0 -0
  32. package/deploy/docs/pdf/06-env-reference.pdf +0 -0
  33. package/deploy/docs/pdf/07-stress-test.pdf +0 -0
  34. package/deploy/install.sh +0 -128
  35. package/deploy/k8s/instance.yaml +0 -147
  36. package/public/admin.html +0 -604
  37. package/public/landing.html +0 -1207
  38. package/public/legal.html +0 -134
  39. package/scripts/set-admin-password.mjs +0 -30
  40. package/server/billing/yookassa.js +0 -142
  41. package/server/instance/agent.js +0 -259
  42. package/server/instance/central.js +0 -514
  43. package/server/instance/identity.js +0 -79
  44. package/server/instance/provision.js +0 -166
  45. package/server/instance/signing.js +0 -71
  46. package/server/role.js +0 -44
package/README.md CHANGED
@@ -1,21 +1,25 @@
1
- # НейроАльфа · Облачная PaaS-платформа алгоритмической торговли на Мосбирже (`MOEX`)
1
+ # ◈ НейроАльфа · Автономный торговый терминал и веб-портал лицензирования (`MOEX`)
2
2
 
3
- Платформа **«НейроАльфа»** переносит математическую архитектуру больших языковых моделей (LLM) на поток биржевых котировок Московской биржи (`Рынок как язык`): ~100 акций индекса широкого рынка `MOEXBMI` выступают «токенами» со своими спектральными эмбеддингами (координатами на карте рынка с авто-калибровкой размерности по теореме Марченко–Пастура), недавнее состояние всех акций — «контекстом», а 12-головый механизм внимания предсказывает остаточное (очищенное от индекса `IMOEX`) движение каждой акции на адаптивном мульти-горизонте `1,5–3,5 часа`.
3
+ Проект разделён на **два полностью независимых npm-приложения**, каждое из которых запускается через `npx` и хранит свои базы данных, ключи и модели в постоянном пользовательском каталоге вне временного кэша `npx` (данные никогда не пропадают при обновлениях):
4
4
 
5
- Встроенный ИИ-управляющий **«Стратег»** помогает подключить брокерский счёт, объясняет текущие сигналы, анализирует новости и сопровождает портфель.
6
-
7
- 📖 **[Инструкция пользователя](docs/ИНСТРУКЦИЯ.md)** · 📐 **[Механика, математика и параметры системы](docs/МЕХАНИКА_И_ПАРАМЕТРЫ.md)** · 🏦 **[Подключение брокеров (API)](docs/БРОКЕРЫ_API.md)** · 🚀 **[Инструкция по деплою и ЮKassa](docs/ДЕПЛОЙ.md)** (также доступны по кнопке `?` в шапке платформы).
5
+ 1. **`neuroalpha`** *(корневой пакет, порт `3000`, данные в `~/.neuroalpha`)* — **Торговый терминал** для рынка акций Московской биржи (`MOEX`): спектральные эмбеддинги ~100 акций `MOEXBMI`, живой радар прогнозов, коннекторы брокеров РФ, активация по токену личного кабинета со скачиванием автономной подписанной `Ed25519`-лицензии и встроенный менеджер локальной ИИ-модели для «Стратега» (`llama.cpp` + `Qwen3 GGUF` с поддержкой единой памяти **AMD Ryzen AI Max+ 395 / 495**).
6
+ 2. **`neuroalpha-portal`** *(пакет в [`portal/`](portal/README.md), порт `3001`, данные в `~/.neuroalpha-portal`)* — **Веб-портал, личные кабинеты и консоль администратора**: публичная витрина, правовые документы, беспарольный вход строго по **Email + 6-значный код**, автоназначение **первого зарегистрированного пользователя Администратором**, приём оплат ЮKassa со стыковкой пакетов по длительности (`1 месяц = 31 сутки`), выпуск автономных лицензий, облачный ИИ-прокси и шлюз ботов Telegram / MAX.
8
7
 
9
8
  ---
10
9
 
11
- ## Быстрый старт (сервер платформы)
10
+ ## Быстрый запуск через `npx`
12
11
 
13
- ```powershell
14
- npm.cmd install
15
- npm.cmd start
12
+ ### 1. Торговый терминал (на ПК клиента или арендованном VPS)
13
+ ```bash
14
+ npx neuroalpha@latest
16
15
  ```
16
+ * Открывается на `http://localhost:3000` (при первом старте предложит вставить токен `na_tok_…` из личного кабинета и скачает оплаченную автономную лицензию; при доступе по сети / на VPS вход защищён токеном ЛК или локальным паролем).
17
17
 
18
- Веб-терминал открывается по адресу `http://localhost:3000`. Требуется Node.js ≥ 22.13 (встроенный `node:sqlite`). В VS Code запуск выполняется клавишей **F5**.
18
+ ### 2. Веб-портал / Личные кабинеты / Админка (`neuroalpha-portal`)
19
+ ```bash
20
+ npx neuroalpha-portal@latest
21
+ ```
22
+ * Открывается на `http://localhost:3001` (первый вошедший по Email + коду пользователь становится `admin` и получает бессрочную лицензию «Хозяин»).
19
23
 
20
24
  ---
21
25
 
package/bin/cli.js CHANGED
@@ -1,69 +1,79 @@
1
- #!/usr/bin/env node
2
- // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import fs from 'node:fs';
6
- import { spawn } from 'node:child_process';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
- const args = process.argv.slice(2);
11
-
12
- let port = process.env.PORT || '3000';
13
- let apiKey = process.env.NEUROALPHA_API_KEY || '';
14
- let cloudUrl = process.env.CLOUD_URL || 'https://neuroalpha.odant.org';
15
- let dataDir = process.env.NEUROALPHA_DATA_DIR || '';
16
- let noBrowser = false;
17
-
18
- for (let i = 0; i < args.length; i++) {
19
- const a = args[i];
20
- if (a === '--help' || a === '-h') {
21
- console.log(`
22
- ◈ НейроАльфа — ИИ-автопилот для рынка акций (Мосбиржа)
23
-
24
- Запуск:
25
- npx neuroalpha [опции]
26
-
27
- Опции:
28
- --key, -k <na_live_...> Активировать API-ключ подписки с https://neuroalpha.odant.org
29
- --port, -p <число> Порт локального сервера (по умолчанию 3000)
30
- --data, -d <путь> Папка для локальной базы данных и ключей (~/.neuroalpha)
31
- --no-browser Не открывать браузер автоматически
32
- --help, -h Показать эту справку
33
- `);
34
- process.exit(0);
35
- }
36
- if ((a === '--key' || a === '-k') && args[i + 1]) { apiKey = args[++i]; continue; }
37
- if (a.startsWith('--key=')) { apiKey = a.slice(6); continue; }
38
- if ((a === '--port' || a === '-p') && args[i + 1]) { port = args[++i]; continue; }
39
- if (a.startsWith('--port=')) { port = a.slice(7); continue; }
40
- if ((a === '--data' || a === '-d') && args[i + 1]) { dataDir = args[++i]; continue; }
41
- if (a.startsWith('--data=')) { dataDir = a.slice(7); continue; }
42
- if (a === '--cloud' && args[i + 1]) { cloudUrl = args[++i]; continue; }
43
- if (a.startsWith('--cloud=')) { cloudUrl = a.slice(8); continue; }
44
- if (a === '--no-browser') { noBrowser = true; continue; }
45
- }
46
-
47
- // Если запуск идёт вне исходной папки разработки (например через npx), храним БД в ~/.neuroalpha
48
- const localData = path.join(ROOT, 'data');
49
- if (!dataDir) {
50
- const isDevRepo = fs.existsSync(path.join(ROOT, '.git')) || fs.existsSync(path.join(localData, 'trading.db'));
51
- dataDir = isDevRepo ? localData : path.join(os.homedir(), '.neuroalpha');
52
- }
53
-
54
- const env = {
55
- ...process.env,
56
- PORT: String(port),
57
- NEUROALPHA_DATA_DIR: path.resolve(dataDir),
58
- OPEN_BROWSER: noBrowser ? '0' : '1',
59
- ...(apiKey ? { NEUROALPHA_API_KEY: apiKey } : {}),
60
- ...(cloudUrl ? { CLOUD_URL: cloudUrl } : {}),
61
- };
62
-
63
- const child = spawn(
64
- process.execPath,
65
- ['--disable-warning=ExperimentalWarning', path.join(ROOT, 'server', 'index.js')],
66
- { stdio: 'inherit', env }
67
- );
68
-
69
- child.on('exit', (code) => process.exit(code ?? 0));
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import fs from 'node:fs';
6
+ import { spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const args = process.argv.slice(2);
11
+
12
+ let port = process.env.PORT || '3000';
13
+ let host = process.env.HOST || '';
14
+ let token = process.env.NEUROALPHA_TOKEN || process.env.NEUROALPHA_API_KEY || '';
15
+ let portalUrl = process.env.PORTAL_URL || process.env.CLOUD_URL || 'https://neuroalpha.odant.org';
16
+ let dataDir = process.env.NEUROALPHA_DATA_DIR || '';
17
+ let noBrowser = false;
18
+
19
+ for (let i = 0; i < args.length; i++) {
20
+ const a = args[i];
21
+ if (a === '--help' || a === '-h') {
22
+ console.log(`
23
+ ◈ НейроАльфа — автономный торговый терминал и ИИ-Стратег (Мосбиржа)
24
+
25
+ Запуск:
26
+ npx neuroalpha@latest [опции]
27
+
28
+ Опции:
29
+ --token, --key, -k <na_tok_...> Активировать токен из личного кабинета портала
30
+ --portal <https://...> Адрес портала лицензий (по умолчанию https://neuroalpha.odant.org)
31
+ --port, -p <число> Порт терминала (по умолчанию 3000)
32
+ --host <адрес> Адрес прослушивания (например, 0.0.0.0 на VPS)
33
+ --data, -d <путь> Постоянная папка данных и локальных моделей (~/.neuroalpha)
34
+ --no-browser Не открывать браузер автоматически
35
+ --help, -h Показать эту справку
36
+ `);
37
+ process.exit(0);
38
+ }
39
+ if ((a === '--token' || a === '--key' || a === '-k') && args[i + 1]) { token = args[++i]; continue; }
40
+ if (a.startsWith('--token=')) { token = a.slice(8); continue; }
41
+ if (a.startsWith('--key=')) { token = a.slice(6); continue; }
42
+ if ((a === '--portal' || a === '--cloud') && args[i + 1]) { portalUrl = args[++i]; continue; }
43
+ if (a.startsWith('--portal=')) { portalUrl = a.slice(9); continue; }
44
+ if (a.startsWith('--cloud=')) { portalUrl = a.slice(8); continue; }
45
+ if ((a === '--port' || a === '-p') && args[i + 1]) { port = args[++i]; continue; }
46
+ if (a.startsWith('--port=')) { port = a.slice(7); continue; }
47
+ if (a === '--host' && args[i + 1]) { host = args[++i]; continue; }
48
+ if (a.startsWith('--host=')) { host = a.slice(7); continue; }
49
+ if ((a === '--data' || a === '-d') && args[i + 1]) { dataDir = args[++i]; continue; }
50
+ if (a.startsWith('--data=')) { dataDir = a.slice(7); continue; }
51
+ if (a === '--no-browser') { noBrowser = true; continue; }
52
+ }
53
+
54
+ // Вне git-репозитория разработки (в т.ч. при любом запуске через npx) храним все данные строго в ~/.neuroalpha
55
+ const insideNodeModules = /[\\/]node_modules[\\/]|_npx/i.test(ROOT);
56
+ const localData = path.join(ROOT, 'data');
57
+ if (!dataDir) {
58
+ const isDevRepo = !insideNodeModules && (fs.existsSync(path.join(ROOT, '.git')) || fs.existsSync(path.join(localData, 'trading.db')));
59
+ dataDir = isDevRepo ? localData : path.join(os.homedir(), '.neuroalpha');
60
+ }
61
+ fs.mkdirSync(path.resolve(dataDir), { recursive: true });
62
+
63
+ const env = {
64
+ ...process.env,
65
+ PORT: String(port),
66
+ ...(host ? { HOST: host } : {}),
67
+ NEUROALPHA_DATA_DIR: path.resolve(dataDir),
68
+ OPEN_BROWSER: noBrowser ? '0' : '1',
69
+ ...(token ? { NEUROALPHA_TOKEN: token } : {}),
70
+ ...(portalUrl ? { PORTAL_URL: portalUrl, CLOUD_URL: portalUrl } : {}),
71
+ };
72
+
73
+ const child = spawn(
74
+ process.execPath,
75
+ ['--disable-warning=ExperimentalWarning', path.join(ROOT, 'server', 'index.js')],
76
+ { stdio: 'inherit', env },
77
+ );
78
+
79
+ child.on('exit', (code) => process.exit(code ?? 0));
@@ -32,8 +32,8 @@
32
32
  - **Новости рынка** — свежие новости и раскрытия компаний с оценкой влияния ИИ и переключателем «Блокировать сделки против важных свежих новостей».
33
33
  3. **Управление и настройки:**
34
34
  - **Эксперименты** *(виден и доступен эксклюзивно в режиме «Хозяин»)* — исторические Walk-Forward исследования модели.
35
- - **Подписка и уведомления** — выбор тарифа и привязка ботов **Telegram / MAX**.
36
- - **Модели ИИ** — подключение и выбор языковых моделей для Стратега.
35
+ - **Лицензия и уведомления** — активация и обновление автономной подписанной лицензии по токену личного кабинета (`na_tok_…`, со стыковкой пакетов по `31 сутки`), пароль для удалённого VPS-доступа и шлюз ботов **Telegram / MAX**.
36
+ - **Модели ИИ** — автодиагностика железа ПК и установка в 1 клик локальной модели для Стратега (`llama-server` + `Qwen3 GGUF`, включая поддержку единой памяти **AMD Ryzen AI Max+ 395 / 495** до 80B параметров), а также подключение облачного прокси, GigaChat, Ollama и внешних API.
37
37
 
38
38
  ---
39
39
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "neuroalpha",
3
- "version": "0.6.12",
4
- "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
3
+ "version": "0.7.0",
4
+ "description": "НейроАльфа — автономный торговый терминал для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор, активация токеном ЛК и автоустановка локальной ИИ-модели Стратега",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
7
7
  "repository": {
@@ -21,17 +21,16 @@
21
21
  "files": [
22
22
  "LICENSE",
23
23
  "bin/",
24
- "deploy/",
25
24
  "server/",
26
25
  "public/",
27
26
  "config/",
28
- "docs/",
29
- "scripts/set-admin-password.mjs"
27
+ "docs/"
30
28
  ],
31
29
  "scripts": {
32
30
  "start": "node --disable-warning=ExperimentalWarning server/index.js",
33
31
  "dev": "node --watch --disable-warning=ExperimentalWarning server/index.js",
34
32
  "cli": "node bin/cli.js",
33
+ "start:portal": "node --disable-warning=ExperimentalWarning portal/server/index.js",
35
34
  "build:desktop": "node scripts/build-desktop.mjs"
36
35
  },
37
36
  "keywords": [
@@ -4,31 +4,53 @@
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <title>Активация · НейроАльфа</title>
7
+ <title>Активация терминала · НейроАльфа</title>
8
8
  <link rel="stylesheet" href="styles.css">
9
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; }
10
+ body { overflow: auto; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 18px; }
11
+ .act-box { width: min(520px, 100%); background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 28px 28px 24px; }
12
+ .act-box h1 { font-size: 20px; margin: 0 0 6px; display: flex; align-items: center; gap: 8px; }
13
+ .act-box ol { padding-left: 20px; color: var(--muted); font-size: 13.5px; line-height: 1.65; margin: 14px 0 18px; }
14
+ .act-in { width: 100%; padding: 10px 12px; font-size: 14px; font-family: monospace; }
15
+ .act-btn { width: 100%; margin-top: 12px; padding: 10px; font-size: 14px; justify-content: center; }
16
+ #act-msg { margin-top: 12px; font-size: 13px; min-height: 18px; line-height: 1.45; }
17
+ .slot-pick { display: grid; gap: 8px; margin-top: 12px; }
18
+ .slot-item { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; cursor: pointer; background: var(--bg2); }
17
19
  </style>
18
20
  </head>
19
21
  <body>
20
22
  <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>
23
+ <h1><span class="logo">◈</span> <span id="act-title">Активация терминала НейроАльфа</span></h1>
24
+ <div id="act-subtitle" class="muted small">Для запуска торгового терминала введите ваш персональный токен из личного кабинета — программа скачает оплаченную автономную лицензию.</div>
25
+
26
+ <!-- Режим 1: Первичная активация токеном из личного кабинета -->
27
+ <div id="box-activate">
28
+ <ol>
29
+ <li>Откройте личный кабинет на портале: <a id="act-portal-link" href="https://neuroalpha.odant.org" target="_blank" rel="noopener">neuroalpha.odant.org</a></li>
30
+ <li>Войдите по Email + коду, убедитесь в наличии активной лицензии и скопируйте ваш токен <code>na_tok_…</code>.</li>
31
+ <li>Вставьте токен ниже. Скачанная лицензия работает <b>полностью автономно</b> весь оплаченный срок.</li>
32
+ </ol>
33
+ <form id="act-form" autocomplete="off">
34
+ <input id="act-token" class="act-in" name="token" placeholder="na_tok_…" required spellcheck="false" aria-label="Токен из личного кабинета">
35
+ <div id="slot-picker" class="slot-pick" hidden></div>
36
+ <details style="margin-top:10px;font-size:12.5px;color:var(--muted)">
37
+ <summary style="cursor:pointer">Указать другой адрес сервера лицензий</summary>
38
+ <div style="margin-top:8px">
39
+ <input id="act-portal-url" class="act-in" placeholder="https://neuroalpha.odant.org">
40
+ </div>
41
+ </details>
42
+ <button id="act-btn" type="submit" class="primary act-btn">Скачать лицензию и активировать</button>
43
+ </form>
44
+ </div>
45
+
46
+ <!-- Режим 2: Вход в уже активированный терминал по сети / на VPS -->
47
+ <div id="box-login" hidden style="margin-top:16px">
48
+ <form id="login-form" autocomplete="off">
49
+ <input id="login-secret" class="act-in secret-input" type="password" placeholder="Токен ЛК (na_tok_…) или локальный пароль" required spellcheck="false">
50
+ <button id="login-btn" type="submit" class="primary act-btn">Войти в терминал</button>
51
+ </form>
52
+ </div>
53
+
32
54
  <div id="act-msg" class="muted" role="status"></div>
33
55
  </div>
34
56
  <script src="activate.js"></script>
@@ -1,30 +1,101 @@
1
1
  // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
- // Экран активации установки на компьютер кодом из личного кабинета
2
+ // Экран первичной активации терминала токеном ЛК (na_tok_...) со скачиванием автономной лицензии
3
+ // и экран защищённого входа при обращении к терминалу по внешней сети / на VPS.
3
4
  (async () => {
4
5
  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; };
6
+ if (localStorage.getItem('theme') === 'light') document.body.classList.add('light');
7
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
8
+ const msg = (text, cls = 'muted') => {
9
+ $('#act-msg').className = cls;
10
+ $('#act-msg').textContent = text;
11
+ };
12
+
7
13
  try {
8
14
  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`;
15
+ if (st.loggedIn) {
16
+ location.href = '/';
17
+ return;
18
+ }
19
+ const pUrl = st.portalUrl || 'https://neuroalpha.odant.org';
20
+ $('#act-portal-url').value = pUrl;
21
+ $('#act-portal-link').textContent = pUrl.replace(/^https?:\/\//, '');
22
+ $('#act-portal-link').href = pUrl;
23
+
24
+ if (st.activated && !st.isLoopback) {
25
+ $('#act-title').textContent = 'Вход в удалённый терминал';
26
+ $('#act-subtitle').textContent = `Терминал активирован (${st.planName}). Для удалённого доступа по сети введите ваш токен личного кабинета${st.hasLocalPassword ? ' или локальный пароль терминала' : ''}.`;
27
+ $('#box-activate').hidden = true;
28
+ $('#box-login').hidden = false;
29
+ setTimeout(() => $('#login-secret').focus(), 40);
30
+ } else {
31
+ setTimeout(() => $('#act-token').focus(), 40);
32
+ }
12
33
  } catch { /* ignore */ }
34
+
35
+ let chosenSlotId = '';
36
+
13
37
  $('#act-form').addEventListener('submit', async (e) => {
14
38
  e.preventDefault();
15
39
  const btn = $('#act-btn');
16
40
  btn.disabled = true;
17
- msg('Проверяем код в центре…');
41
+ msg('Связываемся с порталом и скачиваем лицензию…');
18
42
  try {
43
+ const selectedRadio = document.querySelector('input[name="slot_id"]:checked');
44
+ if (selectedRadio) chosenSlotId = selectedRadio.value;
45
+
19
46
  const r = await fetch('/api/local/activate', {
20
47
  method: 'POST',
21
48
  headers: { 'Content-Type': 'application/json' },
22
- body: JSON.stringify({ code: $('#act-code').value.trim() }),
49
+ body: JSON.stringify({
50
+ token: $('#act-token').value.trim(),
51
+ portalUrl: $('#act-portal-url').value.trim(),
52
+ slotId: chosenSlotId,
53
+ }),
54
+ });
55
+ const j = await r.json().catch(() => ({}));
56
+ if (!r.ok) throw new Error(j.error || `Ошибка ${r.status}`);
57
+
58
+ if (j.chooseSlot && Array.isArray(j.slots)) {
59
+ const picker = $('#slot-picker');
60
+ picker.hidden = false;
61
+ picker.innerHTML = `<div class="small" style="font-weight:600;margin-top:4px">В вашем кабинете несколько свободных лицензий — выберите слот для этого компьютера:</div>`
62
+ + j.slots.map((s, idx) => `<label class="slot-item">
63
+ <input type="radio" name="slot_id" value="${esc(s.id)}" ${idx === 0 ? 'checked' : ''}>
64
+ <div>
65
+ <b>${esc(s.label || s.id)} · ${esc(s.planName)}</b>
66
+ <div class="muted small">${s.perpetual ? 'Бессрочная лицензия' : `Запас: ${s.totalRemainingDays || 0} сут.`}</div>
67
+ </div>
68
+ </label>`).join('');
69
+ btn.textContent = 'Привязать выбранную лицензию и войти';
70
+ btn.disabled = false;
71
+ msg('Выберите нужную лицензию выше и нажмите кнопку ещё раз.');
72
+ return;
73
+ }
74
+
75
+ const planLbl = j.state?.planName || 'активна';
76
+ msg(`✓ Автономная лицензия «${planLbl}» скачана и проверена! Открываем терминал…`, 'pos');
77
+ setTimeout(() => { location.href = '/'; }, 700);
78
+ } catch (err) {
79
+ msg(err.message, 'neg');
80
+ btn.disabled = false;
81
+ }
82
+ });
83
+
84
+ $('#login-form').addEventListener('submit', async (e) => {
85
+ e.preventDefault();
86
+ const btn = $('#login-btn');
87
+ btn.disabled = true;
88
+ msg('Проверка ключа доступа…');
89
+ try {
90
+ const r = await fetch('/api/local/login', {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({ secret: $('#login-secret').value.trim() }),
23
94
  });
24
95
  const j = await r.json().catch(() => ({}));
25
96
  if (!r.ok) throw new Error(j.error || `Ошибка ${r.status}`);
26
- msg(`✓ Программа активирована (${j.id}). Открываем терминал…`, 'pos');
27
- setTimeout(() => { location.href = '/'; }, 900);
97
+ msg('✓ Доступ подтверждён. Открываем терминал…', 'pos');
98
+ setTimeout(() => { location.href = '/'; }, 400);
28
99
  } catch (err) {
29
100
  msg(err.message, 'neg');
30
101
  btn.disabled = false;