work-license-server 1.0.21 → 1.0.23

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.
@@ -764,6 +764,164 @@ html[data-theme="dark"] .sec-nav-item.active {
764
764
  .sec-item:first-child { padding-top: 2px; }
765
765
  .sec-item:last-child { border-bottom: 0; padding-bottom: 2px; }
766
766
 
767
+ /* ---------- Раскрывающиеся панели (аккордеоны) клиентов и франчайзеров в /admin ---------- */
768
+ .adm-acc-list {
769
+ display: flex;
770
+ flex-direction: column;
771
+ gap: 10px;
772
+ }
773
+ .adm-acc-card {
774
+ background: var(--panel-solid);
775
+ border: 1px solid var(--line-strong);
776
+ border-radius: 12px;
777
+ overflow: hidden;
778
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
779
+ transition: border-color .15s ease, box-shadow .15s ease;
780
+ }
781
+ .adm-acc-card.open {
782
+ border-color: color-mix(in srgb, #3b82f6 50%, var(--line-strong));
783
+ box-shadow: 0 3px 10px rgba(15, 23, 42, 0.06);
784
+ }
785
+ .adm-acc-head {
786
+ display: flex;
787
+ align-items: center;
788
+ justify-content: space-between;
789
+ gap: 14px;
790
+ padding: 13px 18px;
791
+ cursor: pointer;
792
+ user-select: none;
793
+ transition: background .15s ease;
794
+ flex-wrap: wrap;
795
+ }
796
+ .adm-acc-head:hover {
797
+ background: color-mix(in srgb, var(--brand) 4%, transparent);
798
+ }
799
+ .adm-acc-ident {
800
+ display: flex;
801
+ align-items: center;
802
+ gap: 12px;
803
+ min-width: 0;
804
+ }
805
+ .adm-acc-chev {
806
+ display: inline-flex;
807
+ align-items: center;
808
+ justify-content: center;
809
+ width: 22px;
810
+ height: 22px;
811
+ border-radius: 6px;
812
+ background: color-mix(in srgb, var(--brand) 8%, transparent);
813
+ color: var(--text-2);
814
+ font-size: 11px;
815
+ font-weight: 700;
816
+ transition: transform .18s ease, background .15s ease, color .15s ease;
817
+ flex-shrink: 0;
818
+ }
819
+ .adm-acc-card.open .adm-acc-chev {
820
+ transform: rotate(90deg);
821
+ background: rgba(59, 130, 246, 0.14);
822
+ color: #2563eb;
823
+ }
824
+ html[data-theme="dark"] .adm-acc-card.open .adm-acc-chev {
825
+ color: #60a5fa;
826
+ }
827
+ .adm-acc-email {
828
+ font-weight: 700;
829
+ font-size: 14.5px;
830
+ color: var(--text);
831
+ word-break: break-all;
832
+ }
833
+ .adm-acc-sub {
834
+ font-size: 12.5px;
835
+ color: var(--muted);
836
+ margin-top: 2px;
837
+ }
838
+ .adm-acc-badges {
839
+ display: flex;
840
+ align-items: center;
841
+ gap: 8px;
842
+ flex-wrap: wrap;
843
+ }
844
+ .adm-acc-body {
845
+ display: none;
846
+ border-top: 1px solid var(--line);
847
+ padding: 14px 18px 16px;
848
+ }
849
+ .adm-acc-card.open .adm-acc-body {
850
+ display: block;
851
+ }
852
+ .adm-profile-strip {
853
+ display: flex;
854
+ align-items: center;
855
+ justify-content: space-between;
856
+ gap: 12px;
857
+ flex-wrap: wrap;
858
+ padding: 10px 14px;
859
+ margin-bottom: 12px;
860
+ border-radius: 10px;
861
+ border: 1px solid var(--line);
862
+ background: color-mix(in srgb, var(--brand) 3%, var(--panel));
863
+ font-size: 12.5px;
864
+ }
865
+ .adm-profile-meta {
866
+ display: flex;
867
+ align-items: center;
868
+ gap: 14px;
869
+ flex-wrap: wrap;
870
+ color: var(--text-2);
871
+ }
872
+ .adm-profile-actions {
873
+ display: flex;
874
+ align-items: center;
875
+ gap: 8px;
876
+ flex-wrap: wrap;
877
+ }
878
+
879
+ /* ---------- Горизонтальные формы настроек в /admin (SMTP, ЮKassa, Боты, Реквизиты оферты) ---------- */
880
+ .adm-form-grid {
881
+ display: flex;
882
+ flex-direction: column;
883
+ gap: 12px;
884
+ }
885
+ .adm-form-row {
886
+ display: grid;
887
+ grid-template-columns: 220px minmax(0, 1fr);
888
+ align-items: center;
889
+ gap: 16px;
890
+ }
891
+ @media (max-width: 720px) {
892
+ .adm-form-row {
893
+ grid-template-columns: 1fr;
894
+ gap: 6px;
895
+ }
896
+ }
897
+ .adm-form-label {
898
+ font-size: 13.5px;
899
+ color: var(--text-2);
900
+ font-weight: 500;
901
+ }
902
+ .adm-preset-bar {
903
+ display: flex;
904
+ align-items: center;
905
+ gap: 8px;
906
+ flex-wrap: wrap;
907
+ margin-bottom: 6px;
908
+ }
909
+ .adm-preset-btn {
910
+ padding: 5px 12px;
911
+ border-radius: 8px;
912
+ border: 1px solid var(--line-strong);
913
+ background: var(--panel);
914
+ color: var(--text);
915
+ font-size: 12.5px;
916
+ font-weight: 600;
917
+ cursor: pointer;
918
+ transition: border-color .15s ease, background .15s ease;
919
+ }
920
+ .adm-preset-btn:hover {
921
+ border-color: #3b82f6;
922
+ background: color-mix(in srgb, #3b82f6 8%, var(--panel));
923
+ }
924
+
767
925
  /* ---------- Оферта и вход администратора ---------- */
768
926
  .oferta-banner {
769
927
  padding: 16px 22px;
package/bin/server.mjs CHANGED
@@ -2,16 +2,75 @@
2
2
  /**
3
3
  * Точка запуска лицензионного сервера и лендинга ПАК WORK (work.odant.org).
4
4
  */
5
- import { fileURLToPath } from 'node:url';
5
+ import fs from 'node:fs';
6
+ import net from 'node:net';
7
+ import path from 'node:path';
8
+ import * as childProcess from 'node:child_process';
6
9
  import { createLicenseServerApp } from '../src/server.js';
10
+ import { resolvePersistentDataDir } from '../src/store.js';
7
11
 
8
12
  const port = Number(process.env.PORT || process.env.WORK_LICENSE_PORT || 8020);
9
- const defaultDataDir = fileURLToPath(new URL('../data', import.meta.url));
10
- const dataDir = process.env.WORK_LICENSE_DATA_DIR || defaultDataDir;
13
+ const dataDir = resolvePersistentDataDir();
11
14
 
12
- const app = createLicenseServerApp({ dataDir });
15
+ async function releasePreviousInstance() {
16
+ const pidFile = path.join(dataDir, 'run.pid');
17
+ try {
18
+ if (fs.existsSync(pidFile)) {
19
+ const oldPid = Number(fs.readFileSync(pidFile, 'utf8').trim());
20
+ if (Number.isInteger(oldPid) && oldPid > 0 && oldPid !== process.pid) {
21
+ try {
22
+ process.kill(oldPid);
23
+ for (let i = 0; i < 15; i++) {
24
+ await new Promise(r => setTimeout(r, 100));
25
+ try { process.kill(oldPid, 0); }
26
+ catch { break; }
27
+ }
28
+ }
29
+ catch { /* процесс уже завершён */ }
30
+ }
31
+ }
32
+ }
33
+ catch { /* ignore */ }
34
+
35
+ const isPortBusy = () => new Promise((resolve) => {
36
+ const sock = net.createConnection({ host: '127.0.0.1', port });
37
+ sock.once('connect', () => { sock.destroy(); resolve(true); });
38
+ sock.once('error', () => { sock.destroy(); resolve(false); });
39
+ });
40
+
41
+ if (await isPortBusy() && process.platform === 'win32') {
42
+ try {
43
+ const out = childProcess.execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
44
+ for (const line of out.split(/\r?\n/)) {
45
+ const m = line.trim().match(/LISTENING\s+(\d+)$/i);
46
+ const pid = m ? Number(m[1]) : 0;
47
+ if (pid && pid !== process.pid) {
48
+ try { process.kill(pid); } catch { /* ignore */ }
49
+ }
50
+ }
51
+ for (let i = 0; i < 15; i++) {
52
+ await new Promise(r => setTimeout(r, 100));
53
+ if (!(await isPortBusy()))
54
+ break;
55
+ }
56
+ }
57
+ catch { /* ignore */ }
58
+ }
59
+
60
+ try {
61
+ fs.mkdirSync(dataDir, { recursive: true });
62
+ fs.writeFileSync(pidFile, String(process.pid), 'utf8');
63
+ }
64
+ catch { /* ignore */ }
65
+ }
66
+
67
+ await releasePreviousInstance();
68
+
69
+ const app = createLicenseServerApp({ dataDir, autoRecover: true });
13
70
  const server = await app.listen(port);
14
71
  const addr = server.address();
15
72
  const actualPort = typeof addr === 'object' && addr ? addr.port : port;
73
+ const counts = app.store.getStoreCounts();
16
74
  console.log(`[work.odant.org] Витрина ODANT-WORK: http://localhost:${actualPort}/`);
17
75
  console.log(`[work.odant.org] Панель администратора: http://localhost:${actualPort}/admin`);
76
+ console.log(`[work.odant.org] Постоянная база данных: ${app.store.stateFile} (пользователей: ${counts.users}, серверов/лицензий: ${counts.licenses}, заказов: ${counts.orders})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "work-license-server",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Лицензионный сервер, биллинг ЮKassa, боты поддержки (MAX, Telegram, VK) и лендинг ПАК WORK (work.odant.org)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/admin.js CHANGED
@@ -121,55 +121,61 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
121
121
 
122
122
  <section class="stage active" style="padding-top:24px">
123
123
  <div class="sec-layout">
124
- <!-- Левое вертикальное меню разделов со счётчиками в скобках (как на Скрине 1) -->
124
+ <!-- Левое вертикальное меню разделов со счётчиками в скобках -->
125
125
  <aside class="sec-nav" aria-label="Разделы администрирования">
126
- <button type="button" class="sec-nav-item active" data-adm-tab="servers" id="navAdmServers">Серверы и лицензии (0)</button>
127
- <button type="button" class="sec-nav-item" data-adm-tab="franchises" id="navAdmFranchises">Франшизы (0)</button>
128
- <div class="sec-nav-sep"></div>
126
+ <button type="button" class="sec-nav-item active" data-adm-tab="clients" id="navAdmClients">Клиенты (0)</button>
127
+ <button type="button" class="sec-nav-item" data-adm-tab="franchisers" id="navAdmFranchisers">Франчайзеры (0)</button>
129
128
  <button type="button" class="sec-nav-item" data-adm-tab="orders" id="navAdmOrders">Заказы и счета (0)</button>
130
- <button type="button" class="sec-nav-item" data-adm-tab="users" id="navAdmUsers">Клиенты и администраторы (0)</button>
131
129
  <button type="button" class="sec-nav-item" data-adm-tab="tickets" id="navAdmTickets">Тикеты поддержки (0)</button>
130
+ <div class="sec-nav-sep"></div>
131
+ <button type="button" class="sec-nav-item" data-adm-tab="smtp" id="navAdmSmtp">
132
+ <span>Почта SMTP</span>
133
+ <span class="tag ok" id="navBadgeSmtp" style="padding:2px 8px;font-size:11px">активна</span>
134
+ </button>
135
+ <button type="button" class="sec-nav-item" data-adm-tab="yookassa" id="navAdmYookassa">Платёжный шлюз ЮKassa</button>
136
+ <button type="button" class="sec-nav-item" data-adm-tab="bots" id="navAdmBots">Боты</button>
137
+ <button type="button" class="sec-nav-item" data-adm-tab="legal" id="navAdmLegal">Реквизиты оферты</button>
138
+ <div class="sec-nav-sep"></div>
139
+ <div style="padding:10px 12px;border-radius:10px;border:1px solid var(--line);background:var(--panel);display:flex;flex-direction:column;gap:6px">
140
+ <div class="small muted" style="font-weight:700;font-size:11.5px;text-transform:uppercase;letter-spacing:.04em">Резервная копия базы</div>
141
+ <button type="button" class="btn btn-ghost btn-sm" style="width:100%;justify-content:flex-start;font-size:12px;padding:6px 10px" data-action="admin-export-backup">📥 Скачать бэкап (.json)</button>
142
+ <button type="button" class="btn btn-ghost btn-sm" style="width:100%;justify-content:flex-start;font-size:12px;padding:6px 10px" data-action="admin-import-backup">📤 Восстановить из файла</button>
143
+ <button type="button" class="btn btn-ghost btn-sm" style="width:100%;justify-content:flex-start;font-size:12px;padding:6px 10px" data-action="admin-recover-npx" title="Найти и объединить данные из предыдущих запусков npx">🔄 Найти данные из npx</button>
144
+ <input type="file" id="inpAdmBackupFile" accept=".json,application/json" style="display:none">
145
+ <div class="small muted" id="admDataDirLabel" style="font-size:11px;word-break:break-all;line-height:1.3;margin-top:2px"></div>
146
+ </div>
132
147
  </aside>
133
148
 
134
149
  <!-- Правая область выбранного раздела -->
135
150
  <div class="sec-main">
136
- <!-- 1. СЕРВЕРЫ И ЛИЦЕНЗИИ -->
137
- <div data-adm-sec="servers">
151
+ <!-- 1. КЛИЕНТЫ (РАСКРЫВАЮЩИЕСЯ ПАНЕЛИ С СЕРВЕРАМИ И ЛИЦЕНЗИЯМИ) -->
152
+ <div data-adm-sec="clients">
138
153
  <div class="sec-bar" style="margin-bottom:12px">
139
- <h2 class="sec-title">Серверы (устройства) WORK и лицензии клиентов</h2>
154
+ <h2 class="sec-title">Клиенты (серверы WORK и лицензии)</h2>
140
155
  <div class="sec-controls">
141
- <input type="search" id="inpFilterServers" placeholder="Поиск по клиенту, Email, HWID…">
142
- <select id="selFilterServers">
143
- <option value="all">Все серверы</option>
144
- <option value="base_active">Базовая активна</option>
156
+ <input type="search" id="inpFilterClients" placeholder="Поиск по Email, HWID, ИНН…">
157
+ <select id="selFilterClients">
158
+ <option value="all">Все клиенты</option>
159
+ <option value="base_active">Базовая лицензия активна</option>
145
160
  <option value="base_inactive">Без базовой лицензии</option>
146
161
  <option value="hwid_bound">HWID привязан</option>
162
+ <option value="admins">Администраторы</option>
147
163
  </select>
148
164
  <button type="button" class="btn btn-primary btn-sm" data-action="open-create-server-modal">+ Добавить сервер</button>
149
165
  </div>
150
166
  </div>
151
- <div class="sec-box">
152
- <table class="sec-table">
153
- <thead><tr>
154
- <th>Сервер / ID</th>
155
- <th>Владелец / Организация</th>
156
- <th>Железо (HWID)</th>
157
- <th>Базовая лицензия</th>
158
- <th>Доп. места и SLA</th>
159
- <th style="text-align:right">Файлы</th>
160
- </tr></thead>
161
- <tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
162
- </table>
167
+ <div class="adm-acc-list" id="admClientsList">
168
+ <div class="sec-box muted">Загрузка…</div>
163
169
  </div>
164
170
  </div>
165
171
 
166
- <!-- 2. ФРАНШИЗЫ И ГРАФИКИ ПОГАШЕНИЯ (12 МЕС.) -->
167
- <div data-adm-sec="franchises" style="display:none">
172
+ <!-- 2. ФРАНЧАЙЗЕРЫ (РАСКРЫВАЮЩИЕСЯ ПАНЕЛИ С РЕГИОНАМИ И СТАТУСАМИ) -->
173
+ <div data-adm-sec="franchisers" style="display:none">
168
174
  <div class="sec-bar" style="margin-bottom:12px">
169
- <h2 class="sec-title">Региональные франшизы и графики погашения (12 мес.)</h2>
175
+ <h2 class="sec-title">Франчайзеры (регионы РФ и текущие статусы)</h2>
170
176
  <div class="sec-controls">
171
- <input type="search" id="inpFilterFranchises" placeholder="Поиск по региону, компании, ИНН…">
172
- <select id="selFilterFranchises">
177
+ <input type="search" id="inpFilterFranchisers" placeholder="Поиск по Email, региону, ИНН…">
178
+ <select id="selFilterFranchisers">
173
179
  <option value="all">Все активные</option>
174
180
  <option value="pending_deposit">Ожидают залога (15 мин)</option>
175
181
  <option value="deposit_paid">Погашение по графику</option>
@@ -179,18 +185,8 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
179
185
  </select>
180
186
  </div>
181
187
  </div>
182
- <div class="sec-box">
183
- <table class="sec-table">
184
- <thead><tr>
185
- <th>Субъект РФ / ID</th>
186
- <th>Франчайзи</th>
187
- <th>Залог (100 тыс. ₽)</th>
188
- <th>График (12 мес.)</th>
189
- <th>Оплачено / Остаток</th>
190
- <th style="text-align:right">Управление</th>
191
- </tr></thead>
192
- <tbody id="admFranchisesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
193
- </table>
188
+ <div class="adm-acc-list" id="admFranchisersList">
189
+ <div class="sec-box muted">Загрузка…</div>
194
190
  </div>
195
191
  </div>
196
192
 
@@ -223,30 +219,7 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
223
219
  </div>
224
220
  </div>
225
221
 
226
- <!-- 4. КЛИЕНТЫ И АДМИНИСТРАТОРЫ -->
227
- <div data-adm-sec="users" style="display:none">
228
- <div class="sec-bar" style="margin-bottom:12px">
229
- <h2 class="sec-title">Зарегистрированные клиенты и администраторы</h2>
230
- <div class="sec-controls">
231
- <input type="search" id="inpFilterUsers" placeholder="Поиск по Email, организации, ИНН…">
232
- </div>
233
- </div>
234
- <div class="sec-box">
235
- <table class="sec-table">
236
- <thead><tr>
237
- <th>Email / ID</th>
238
- <th>Роль</th>
239
- <th>Организация / ИНН</th>
240
- <th>Реестры клиента</th>
241
- <th>Оферта (ПЭП)</th>
242
- <th style="text-align:right">Вход</th>
243
- </tr></thead>
244
- <tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
245
- </table>
246
- </div>
247
- </div>
248
-
249
- <!-- 5. ТИКЕТЫ ПОДДЕРЖКИ SLA -->
222
+ <!-- 4. ТИКЕТЫ ПОДДЕРЖКИ SLA -->
250
223
  <div data-adm-sec="tickets" style="display:none">
251
224
  <div class="sec-bar" style="margin-bottom:12px">
252
225
  <h2 class="sec-title">Тикеты техподдержки SLA (MAX · Telegram · VK)</h2>
@@ -272,6 +245,183 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
272
245
  </table>
273
246
  </div>
274
247
  </div>
248
+
249
+ <!-- 5. ПОЧТА SMTP -->
250
+ <div data-adm-sec="smtp" style="display:none">
251
+ <div class="sec-bar" style="margin-bottom:12px">
252
+ <h2 class="sec-title">Почтовый сервер SMTP (отправка кодов входа)</h2>
253
+ <span class="tag ok" id="admSmtpStatusTag">✓ Подключена</span>
254
+ </div>
255
+ <div class="sec-box">
256
+ <div class="adm-preset-bar" style="margin-bottom:14px">
257
+ <span class="adm-form-label" style="margin-right:4px">Шаблон сервера:</span>
258
+ <button type="button" class="adm-preset-btn" data-smtp-preset="yandex">Яндекс (465)</button>
259
+ <button type="button" class="adm-preset-btn" data-smtp-preset="gmail">Gmail (465)</button>
260
+ <button type="button" class="adm-preset-btn" data-smtp-preset="mailru">Mail.ru (465)</button>
261
+ </div>
262
+ <div class="adm-form-grid">
263
+ <div class="adm-form-row">
264
+ <label class="adm-form-label" for="admSmtpHost">SMTP сервер (Host)</label>
265
+ <input type="text" id="admSmtpHost" placeholder="smtp.yandex.ru">
266
+ </div>
267
+ <div class="adm-form-row">
268
+ <label class="adm-form-label" for="admSmtpPort">Порт</label>
269
+ <input type="number" id="admSmtpPort" placeholder="465">
270
+ </div>
271
+ <div class="adm-form-row">
272
+ <span class="adm-form-label">Прямой TLS (465)</span>
273
+ <label style="display:flex;align-items:center;gap:10px;font-size:13.5px;cursor:pointer">
274
+ <input type="checkbox" id="admSmtpSecure" checked style="width:17px;height:17px">
275
+ <span>Включён (выключите для порта 587 STARTTLS)</span>
276
+ </label>
277
+ </div>
278
+ <div class="adm-form-row">
279
+ <label class="adm-form-label" for="admSmtpUser">Логин SMTP</label>
280
+ <input type="text" id="admSmtpUser" placeholder="user@yandex.ru">
281
+ </div>
282
+ <div class="adm-form-row">
283
+ <label class="adm-form-label" for="admSmtpPass">Пароль приложения</label>
284
+ <input type="text" id="admSmtpPass" placeholder="Пароль приложения SMTP">
285
+ </div>
286
+ <div class="adm-form-row">
287
+ <label class="adm-form-label" for="admSmtpFrom">Отправитель (From)</label>
288
+ <input type="text" id="admSmtpFrom" placeholder="user@yandex.ru">
289
+ </div>
290
+ <div class="adm-form-row">
291
+ <label class="adm-form-label" for="admSmtpSenderName">Имя отправителя</label>
292
+ <input type="text" id="admSmtpSenderName" placeholder="ODANT-WORK">
293
+ </div>
294
+ </div>
295
+ <div style="display:flex;align-items:center;gap:10px;margin-top:18px;flex-wrap:wrap">
296
+ <button type="button" class="btn btn-primary" data-action="save-smtp-settings">Сохранить настройки SMTP</button>
297
+ <button type="button" class="btn btn-ghost" data-action="test-smtp-settings">Отправить тестовое письмо</button>
298
+ <span class="small" id="admSmtpFeedback"></span>
299
+ </div>
300
+ </div>
301
+ </div>
302
+
303
+ <!-- 6. ПЛАТЁЖНЫЙ ШЛЮЗ ЮKASSA -->
304
+ <div data-adm-sec="yookassa" style="display:none">
305
+ <div class="sec-bar" style="margin-bottom:12px">
306
+ <h2 class="sec-title">Платёжный шлюз ЮKassa (API v3)</h2>
307
+ <span class="tag" id="admYookassaStatusTag">Режим песочницы</span>
308
+ </div>
309
+ <div class="sec-box">
310
+ <div style="margin-bottom:16px;font-size:13.5px;color:var(--text-2)">
311
+ Webhook URL для личного кабинета ЮKassa:
312
+ <code id="admYookassaWebhookUrl" style="padding:3px 8px;border-radius:6px;background:color-mix(in srgb,var(--brand) 8%,var(--panel))">https://${esc(P.domain)}/api/payments/yookassa/webhook</code>
313
+ </div>
314
+ <div class="adm-form-grid">
315
+ <div class="adm-form-row">
316
+ <label class="adm-form-label" for="admYkShopId">Shop ID магазина</label>
317
+ <input type="text" id="admYkShopId" placeholder="123456">
318
+ </div>
319
+ <div class="adm-form-row">
320
+ <label class="adm-form-label" for="admYkSecretKey">Секретный ключ</label>
321
+ <input type="text" id="admYkSecretKey" placeholder="live_... или test_...">
322
+ </div>
323
+ <div class="adm-form-row">
324
+ <span class="adm-form-label">Онлайн-чеки 54-ФЗ</span>
325
+ <label style="display:flex;align-items:center;gap:10px;font-size:13.5px;cursor:pointer">
326
+ <input type="checkbox" id="admYkSendReceipt" style="width:17px;height:17px">
327
+ <span>Передавать структуру чека (receipt)</span>
328
+ </label>
329
+ </div>
330
+ </div>
331
+ <div style="display:flex;align-items:center;gap:10px;margin-top:18px;flex-wrap:wrap">
332
+ <button type="button" class="btn btn-primary" data-action="save-yookassa-settings">Сохранить ЮKassa</button>
333
+ <span class="small" id="admYkFeedback"></span>
334
+ </div>
335
+ </div>
336
+ </div>
337
+
338
+ <!-- 7. БОТЫ TELEGRAM И MAX -->
339
+ <div data-adm-sec="bots" style="display:none">
340
+ <div class="sec-bar" style="margin-bottom:12px">
341
+ <h2 class="sec-title">Боты Telegram / MAX</h2>
342
+ </div>
343
+ <div class="sec-box">
344
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:8px">
345
+ <h3 class="h3" style="margin:0;font-size:15.5px">Единые боты платформы Telegram и MAX</h3>
346
+ <div style="display:flex;gap:8px;flex-wrap:wrap" id="admBotsBadges">
347
+ <span class="tag" id="admBadgeTgBot">TG: не задан</span>
348
+ <span class="tag" id="admBadgeMaxBot">MAX: не задан</span>
349
+ </div>
350
+ </div>
351
+ <p class="small muted" style="margin:0 0 16px;line-height:1.5">
352
+ Вставьте токен бота — имя бота (<code>@username</code>) определится по API автоматически. Каждый клиент привязывает свой личный чат в 1 клик и получает строго только свои собственные уведомления.
353
+ </p>
354
+ <div class="adm-form-grid">
355
+ <div class="adm-form-row">
356
+ <label class="adm-form-label" for="admBotTgToken">Токен Telegram Bot API</label>
357
+ <input type="text" id="admBotTgToken" placeholder="123456789:AA...">
358
+ </div>
359
+ <div class="adm-form-row">
360
+ <label class="adm-form-label" for="admBotTgUsername">Имя Telegram-бота (авто)</label>
361
+ <input type="text" id="admBotTgUsername" placeholder="odant_work_bot">
362
+ </div>
363
+ <div class="adm-form-row">
364
+ <label class="adm-form-label" for="admBotMaxToken">Токен MAX Bot API</label>
365
+ <input type="text" id="admBotMaxToken" placeholder="f9LHod...">
366
+ </div>
367
+ <div class="adm-form-row">
368
+ <label class="adm-form-label" for="admBotMaxUsername">Имя бота MAX (авто)</label>
369
+ <input type="text" id="admBotMaxUsername" placeholder="id6234049193_bot">
370
+ </div>
371
+ </div>
372
+ <div style="display:flex;align-items:center;gap:10px;margin-top:18px;flex-wrap:wrap">
373
+ <button type="button" class="btn btn-primary" data-action="save-bots-settings">Сохранить ботов платформы</button>
374
+ <span class="small" id="admBotsFeedback"></span>
375
+ </div>
376
+ </div>
377
+ </div>
378
+
379
+ <!-- 8. РЕКВИЗИТЫ ОФЕРТЫ -->
380
+ <div data-adm-sec="legal" style="display:none">
381
+ <div class="sec-bar" style="margin-bottom:12px">
382
+ <h2 class="sec-title">Юридические реквизиты оферты (<code>/api/oferta</code>)</h2>
383
+ </div>
384
+ <div class="sec-box">
385
+ <div class="adm-form-grid">
386
+ <div class="adm-form-row">
387
+ <label class="adm-form-label" for="admLegalCompany">Оператор / Наименование</label>
388
+ <input type="text" id="admLegalCompany" placeholder="ООО «БИС» («Бизнес Интер Софт»)">
389
+ </div>
390
+ <div class="adm-form-row">
391
+ <label class="adm-form-label" for="admLegalAuthor">Автор ПО</label>
392
+ <input type="text" id="admLegalAuthor" placeholder="Перепелкин Роман Александрович">
393
+ </div>
394
+ <div class="adm-form-row">
395
+ <label class="adm-form-label" for="admLegalStatus">Правовой статус</label>
396
+ <input type="text" id="admLegalStatus" placeholder="Правообладатель и Оператор платформы · Проприетарное ПО (Все права защищены)">
397
+ </div>
398
+ <div class="adm-form-row">
399
+ <label class="adm-form-label" for="admLegalInn">ИНН</label>
400
+ <input type="text" id="admLegalInn" placeholder="6234049193">
401
+ </div>
402
+ <div class="adm-form-row">
403
+ <label class="adm-form-label" for="admLegalOgrn">ОГРН / ОГРНИП</label>
404
+ <input type="text" id="admLegalOgrn" placeholder="">
405
+ </div>
406
+ <div class="adm-form-row">
407
+ <label class="adm-form-label" for="admLegalEmail">Email поддержки</label>
408
+ <input type="email" id="admLegalEmail" placeholder="support@odant.org">
409
+ </div>
410
+ <div class="adm-form-row">
411
+ <label class="adm-form-label" for="admLegalPhone">Телефон</label>
412
+ <input type="text" id="admLegalPhone" placeholder="+7 (4912) 77-70-21">
413
+ </div>
414
+ <div class="adm-form-row">
415
+ <label class="adm-form-label" for="admLegalAddress">Адрес</label>
416
+ <input type="text" id="admLegalAddress" placeholder="390000, г. Рязань, ул. Ленина, 47">
417
+ </div>
418
+ </div>
419
+ <div style="display:flex;align-items:center;gap:10px;margin-top:18px;flex-wrap:wrap">
420
+ <button type="button" class="btn btn-primary" data-action="save-legal-settings">Сохранить реквизиты</button>
421
+ <span class="small" id="admLegalFeedback"></span>
422
+ </div>
423
+ </div>
424
+ </div>
275
425
  </div>
276
426
  </div>
277
427
  </section>
@@ -286,23 +436,28 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
286
436
 
287
437
  <!-- Модальное окно: Создание сервера / Выпуск лицензии клиенту -->
288
438
  <div class="modal" id="admCreateServerModal" role="dialog" aria-modal="true">
289
- <div class="card" style="max-width:540px">
439
+ <div class="card" style="max-width:500px">
290
440
  <div class="modal-head">
291
441
  <img src="/assets/odant-logo.png" alt="">
292
442
  <div>
293
- <h3 class="h3" style="margin:0">Добавить сервер / выпустить лицензию</h3>
443
+ <h3 class="h3" style="margin:0">Добавить сервер клиенту</h3>
294
444
  <div class="small muted">Создание записи сервера в реестре с подписью Ed25519</div>
295
445
  </div>
296
446
  <button class="icon-btn" data-action="close-create-server-modal" aria-label="Закрыть">${icon('close')}</button>
297
447
  </div>
298
- <div class="field"><label for="admNewSrvName">Название сервера (устройства)</label><input type="text" id="admNewSrvName" placeholder="Основной сервер WORK"></div>
299
- <div class="row2">
300
- <div class="field"><label for="admNewSrvOrg">Организация клиента</label><input type="text" id="admNewSrvOrg" placeholder="ООО «Компания»"></div>
301
- <div class="field"><label for="admNewSrvInn">ИНН</label><input type="text" id="admNewSrvInn" placeholder="7701234567"></div>
448
+ <div class="field">
449
+ <label for="admNewSrvEmail">Email клиента *</label>
450
+ <input type="email" id="admNewSrvEmail" placeholder="client@company.ru">
302
451
  </div>
303
452
  <div class="row2">
304
- <div class="field"><label for="admNewSrvEmail">Email владельца *</label><input type="email" id="admNewSrvEmail" placeholder="client@company.ru"></div>
305
- <div class="field"><label for="admNewSrvHwid">HWID (необязательно)</label><input type="text" id="admNewSrvHwid" placeholder="PAK-..."></div>
453
+ <div class="field">
454
+ <label for="admNewSrvName">Название сервера (устройства)</label>
455
+ <input type="text" id="admNewSrvName" placeholder="Сервер WORK #1">
456
+ </div>
457
+ <div class="field">
458
+ <label for="admNewSrvHwid">HWID (необязательно)</label>
459
+ <input type="text" id="admNewSrvHwid" placeholder="PAK-...">
460
+ </div>
306
461
  </div>
307
462
  <label style="display:flex;align-items:center;gap:10px;font-size:13.5px;margin:8px 0 14px;cursor:pointer">
308
463
  <input type="checkbox" id="admNewSrvActivateBase" checked style="width:17px;height:17px">
package/src/index.js CHANGED
@@ -37,7 +37,13 @@ export {
37
37
  parseOwnerAccessToken,
38
38
  verifyOwnerAccessToken,
39
39
  } from './crypto.js';
40
- export { LicenseStore } from './store.js';
40
+ export {
41
+ LicenseStore,
42
+ isEphemeralPackagePath,
43
+ resolvePersistentDataDir,
44
+ findCandidateRecoveryDirs,
45
+ mergeStoreStates,
46
+ } from './store.js';
41
47
  export { YooKassaService } from './yookassa.js';
42
48
  export { SupportBotsHub } from './bots.js';
43
49
  export { LicenseServerAiAgent } from './agent.js';