work-license-server 1.0.14 → 1.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "work-license-server",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Отдельная защищённая страница панели администратора центра лицензирования и биллинга ODANT-WORK (/admin).
2
+ * Админ-панель центра лицензирования и франшиз ODANT-WORK (/admin).
3
3
  *
4
- * Не связана ссылками с публичной витриной (/).
5
- * Требует обязательной авторизации администратора (POST /api/admin/login -> Bearer adm_...).
4
+ * Единая шапка с основным сайтом (О платформе · Клиентам · Франшиза · Личный кабинет · Админ-панель)
5
+ * и двухколоночная навигация со счётчиками в скобках (по эталону Скрина 1).
6
6
  */
7
7
  import {
8
8
  PAK_PRODUCT,
@@ -15,7 +15,6 @@ import {
15
15
  import { icon } from './landing.js';
16
16
 
17
17
  const esc = (s) => String(s ?? '').replace(/[&<>"']/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' }[ch]));
18
- const rub = (n) => Number(n || 0).toLocaleString('ru-RU').replace(/\u00a0/g, ' ') + ' ₽';
19
18
  const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
20
19
 
21
20
  export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
@@ -38,7 +37,7 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
38
37
  <head>
39
38
  <meta charset="utf-8">
40
39
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
41
- <title>ODANT-WORK · Консоль администратора центра лицензирования</title>
40
+ <title>ODANT-WORK · Админ-панель</title>
42
41
  <meta name="robots" content="noindex, nofollow">
43
42
  <meta name="theme-color" content="#04060b">
44
43
  <link rel="icon" type="image/png" href="/favicon.ico">
@@ -91,234 +90,311 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
91
90
  <div style="margin-top:8px;text-align:center">
92
91
  <a href="/" class="small muted">← Вернуться на главную страницу work.odant.org</a>
93
92
  </div>
94
- <div class="small muted" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--line)">
95
- Ключ ЦС Ed25519: <code>${esc(publicKey.slice(0, 26))}…</code>
96
- </div>
97
93
  </div>
98
94
  </div>
99
95
 
100
- <!-- Основная консоль администратора (скрыта до успешной авторизации) -->
96
+ <!-- Основная оболочка с единой шапкой сайта и двухколоночным меню разделов -->
101
97
  <div class="shell" id="adminAppShell" style="display:none">
102
- <header class="topbar" style="grid-template-columns: auto 1fr auto">
103
- <a href="/" class="brand" style="text-decoration:none">
98
+ <header class="topbar">
99
+ <a href="/" class="brand" style="text-decoration:none" aria-label="ODANT-WORK — на главную">
104
100
  <img src="/assets/odant-logo.png" alt="ODANT">
105
101
  <span>
106
- <span class="brand-name">ODANT<b>·</b>WORK <span class="tag ok" style="margin-left:6px;vertical-align:middle">BOSS · ADMIN</span></span><br>
107
- <span class="brand-sub">центр сертификации Ed25519 · биллинг ЮKassa · франшизы · SLA</span>
102
+ <span class="brand-name">ODANT<b>·</b>WORK</span><br>
103
+ <span class="brand-sub">учёт · документооборот · процессы · приватный ИИ</span>
108
104
  </span>
109
105
  </a>
110
- <div></div>
106
+
107
+ <nav class="tabs" id="tabs" aria-label="Разделы">
108
+ <span class="tab-ind" id="tabInd"></span>
109
+ <a class="tab" href="/#product" style="text-decoration:none">О платформе</a>
110
+ <a class="tab" href="/#clients" style="text-decoration:none">Клиентам</a>
111
+ <a class="tab" href="/#franchise" style="text-decoration:none">Франшиза</a>
112
+ <a class="tab" href="/#addons" style="text-decoration:none">Личный кабинет</a>
113
+ <a class="tab active" href="/admin" id="tileAdmin" style="text-decoration:none">Админ-панель</a>
114
+ </nav>
115
+
111
116
  <div class="top-actions">
112
- <a href="/#addons" class="btn btn-ghost btn-sm" style="text-decoration:none">← Личный кабинет</a>
113
117
  <button class="icon-btn" id="btnTheme" title="Светлая / тёмная тема" aria-label="Переключить тему">${icon('sun')}</button>
114
- <button class="btn btn-ghost btn-sm" data-action="registry">${icon('server')}<span>Обновить реестр</span></button>
115
- <button class="btn btn-ok btn-sm" data-action="demo-pak">+ Демо-лицензия ПАК</button>
116
- <button class="btn btn-ghost btn-sm" data-action="admin-logout" title="Завершить сессию администратора">${icon('lock')}<span id="admUserLabel">Выйти</span></button>
118
+ <button class="btn btn-primary btn-sm" data-action="admin-logout" title="Нажмите, чтобы выйти из сессии">${icon('user')}<span id="admUserLabel">BOSS</span></button>
117
119
  </div>
118
120
  </header>
119
121
 
120
- <section class="stage active" style="padding-top:26px">
121
- <div class="stats" style="grid-template-columns:repeat(6,1fr)">
122
- <div class="stat"><b id="admStatLic">0</b><span id="admStatLicSub">лицензий ПАК · 0 мест</span></div>
123
- <div class="stat"><b id="admStatPaid">0 ₽</b><span id="admStatPaidSub">оплачено заказов: 0</span></div>
124
- <div class="stat"><b id="admStatPending">0 ₽</b><span id="admStatPendingSub">ожидают оплаты: 0</span></div>
125
- <div class="stat"><b id="admStatPartners">0 / 0</b><span>партнёров / резервов франшиз</span></div>
126
- <div class="stat"><b id="admStatUsers">0</b><span id="admStatUsersSub">клиентов (оферт ПЭП: 0)</span></div>
127
- <div class="stat"><b id="admStatTickets">0</b><span id="admStatTicketsSub">открытых тикетов SLA</span></div>
128
- </div>
129
-
130
- <div class="admin-workspace" id="adminWorkspace">
131
- <!-- Левая панель навигации и ИИ-агента администратора -->
132
- <aside class="admin-left">
133
- <nav class="admin-nav" aria-label="Вкладки админ-панели">
134
- <button class="adm-tab on" data-adm-tab="all">${icon('ai')}<span>ИИ-агент + Все разделы</span><i class="live-dot"></i></button>
135
- <button class="adm-tab" data-adm-tab="agent">${icon('agents')}<span>Только ИИ-агент</span></button>
136
- <button class="adm-tab" data-adm-tab="licenses">${icon('key')}<span>Лицензии (${P.baseSeats} вечн. + доп.)</span></button>
137
- <button class="adm-tab" data-adm-tab="orders">${icon('cart')}<span>Заказы ЮKassa</span></button>
138
- <button class="adm-tab" data-adm-tab="partners">${icon('map')}<span>Франшизы и партнёры</span></button>
139
- <button class="adm-tab" data-adm-tab="tickets">${icon('chat')}<span>Тикеты SLA (MAX·TG·VK)</span></button>
140
- <button class="adm-tab" data-adm-tab="users" style="grid-column:1 / -1">${icon('user')}<span>Зарегистрированные клиенты и подписанные оферты (ПЭП)</span></button>
141
- </nav>
122
+ <section class="stage active" style="padding-top:24px">
123
+ <div class="sec-layout">
124
+ <!-- Левое вертикальное меню разделов со счётчиками в скобках (как на Скрине 1) -->
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>
129
+ <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
+ <button type="button" class="sec-nav-item" data-adm-tab="tickets" id="navAdmTickets">Тикеты поддержки (0)</button>
132
+ </aside>
142
133
 
143
- <div class="card adm-agent-card" id="admAgentCard">
144
- <div class="ai-head" style="padding:0 0 12px;margin-bottom:10px">
145
- <img src="/assets/odant-logo.png" alt="">
146
- <div>
147
- <b>ИИ-агент администратора</b>
148
- <span>● управление реестром · защищённый контур</span>
134
+ <!-- Правая область выбранного раздела -->
135
+ <div class="sec-main">
136
+ <!-- 1. СЕРВЕРЫ И ЛИЦЕНЗИИ -->
137
+ <div data-adm-sec="servers">
138
+ <div class="sec-bar" style="margin-bottom:12px">
139
+ <h2 class="sec-title">Серверы (устройства) WORK и лицензии клиентов</h2>
140
+ <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>
145
+ <option value="base_inactive">Без базовой лицензии</option>
146
+ <option value="hwid_bound">HWID привязан</option>
147
+ </select>
148
+ <button type="button" class="btn btn-primary btn-sm" data-action="open-create-server-modal">+ Добавить сервер</button>
149
149
  </div>
150
150
  </div>
151
- <div class="ai-msgs adm-ai-msgs" id="admAiMessages">
152
- <div class="msg bot">Здравствуйте! Я ИИ-агент консоли администратора ODANT-WORK. Помогу проанализировать реестр лицензий (${P.baseSeats} вечных мест + 1 год Enterprise), выпустить сертификат Ed25519, провести оплату заказов ЮKassa, активировать резервы франшиз или проверить квоты по регионам РФ.</div>
153
- </div>
154
- <div class="ai-sugg" style="padding:8px 0;border-top:1px solid var(--line)">
155
- <button data-adm-ask="Сводка по серверу лицензий">Сводка реестра</button>
156
- <button data-adm-ask="Выпусти базовую лицензию для ООО «АгроТех» ИНН 6234001122">+ Выпустить лицензию</button>
157
- <button data-adm-ask="Проведи оплату ожидающих заказов">Провести оплаты</button>
158
- <button data-adm-ask="Активируй резервы франшиз">Активировать резервы</button>
159
- <button data-adm-ask="Сколько вакантных франшиз в Татарстане?">Квота Татарстана</button>
160
- </div>
161
- <div class="ai-input" style="padding:10px 0 0">
162
- <input type="text" id="admAiInput" placeholder="Команда или вопрос ИИ-агенту админки…" autocomplete="off">
163
- <button class="btn btn-primary" data-action="adm-ai-send" aria-label="Отправить">${icon('send')}</button>
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>
164
163
  </div>
165
164
  </div>
166
- </aside>
167
165
 
168
- <!-- Правая область разделов админки -->
169
- <div class="admin-main" id="adminMain">
170
- <!-- 1. ЛИЦЕНЗИИ -->
171
- <div class="card" data-adm-sec="licenses">
172
- <div class="sec-head" style="margin-bottom:14px">
173
- <div>
174
- <span class="kicker">Выпуск сертификата Ed25519</span>
175
- <h3 class="h3" style="margin:4px 0 0">Выпустить базовую лицензию комплекса (${P.baseSeats} вечных лицензий + 1 год поддержки Enterprise)</h3>
166
+ <!-- 2. ФРАНШИЗЫ И ГРАФИКИ ПОГАШЕНИЯ (12 МЕС.) -->
167
+ <div data-adm-sec="franchises" style="display:none">
168
+ <div class="sec-bar" style="margin-bottom:12px">
169
+ <h2 class="sec-title">Региональные франшизы и графики погашения (12 мес.)</h2>
170
+ <div class="sec-controls">
171
+ <input type="search" id="inpFilterFranchises" placeholder="Поиск по региону, компании, ИНН…">
172
+ <select id="selFilterFranchises">
173
+ <option value="all">Все активные</option>
174
+ <option value="pending_deposit">Ожидают залога (15 мин)</option>
175
+ <option value="deposit_paid">Погашение по графику</option>
176
+ <option value="overdue">С просрочкой</option>
177
+ <option value="paid_in_full">Полностью погашены</option>
178
+ <option value="closed">Отменённые / Истёкшие</option>
179
+ </select>
176
180
  </div>
177
181
  </div>
178
- <div class="grid g4" style="align-items:end">
179
- <div class="field" style="margin:0"><label for="admLicOrg">Организация</label><input type="text" id="admLicOrg" value="АО «РязаньАгроПром»"></div>
180
- <div class="field" style="margin:0"><label for="admLicInn">ИНН</label><input type="text" id="admLicInn" value="6234109876"></div>
181
- <div class="field" style="margin:0"><label for="admLicEmail">Email клиента</label><input type="email" id="admLicEmail" value="it@ryazan-agro.ru"></div>
182
- <div class="field" style="margin:0"><label for="admLicHwid">HWID комплекса (необязательно)</label><input type="text" id="admLicHwid" placeholder="PAK-..."></div>
183
- </div>
184
- <div style="display:flex;align-items:center;gap:14px;margin-top:14px;flex-wrap:wrap">
185
- <button class="btn btn-primary btn-sm" data-action="adm-issue-lic">${icon('key')} Выпустить и подписать сертификат</button>
186
- <span class="small muted" id="admIssueStatus"></span>
187
- </div>
188
- </div>
189
-
190
- <div class="card" data-adm-sec="licenses">
191
- <h3 class="h3" style="margin:0 0 12px">Выпущенные лицензии комплексов ODANT-WORK</h3>
192
- <div class="table-wrap">
193
- <table class="t">
182
+ <div class="sec-box">
183
+ <table class="sec-table">
194
184
  <thead><tr>
195
- <th>Лицензия / Код бота</th>
196
- <th>Организация / ИНН</th>
197
- <th>Привязка оборудования / Партнёр</th>
198
- <th class="num">Сессии (вечные + доп.)</th>
199
- <th>SLA и срок</th>
200
- <th>Сертификат</th>
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>
201
191
  </tr></thead>
202
- <tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
192
+ <tbody id="admFranchisesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
203
193
  </table>
204
194
  </div>
205
195
  </div>
206
196
 
207
- <!-- 2. ЗАКАЗЫ ЮKASSA -->
208
- <div class="card" data-adm-sec="orders">
209
- <h3 class="h3" style="margin:0 0 12px">Заказы и платежи ЮKassa</h3>
210
- <div class="table-wrap">
211
- <table class="t">
197
+ <!-- 3. ЗАКАЗЫ И СЧЕТА -->
198
+ <div data-adm-sec="orders" style="display:none">
199
+ <div class="sec-bar" style="margin-bottom:12px">
200
+ <h2 class="sec-title">Заказы ЮKassa и безналичные счета-договоры</h2>
201
+ <div class="sec-controls">
202
+ <input type="search" id="inpFilterOrders" placeholder="Поиск по заказу, счёту, ИНН…">
203
+ <select id="selFilterOrders">
204
+ <option value="all">Все заказы</option>
205
+ <option value="pending">Ожидают оплаты</option>
206
+ <option value="paid">Оплачены</option>
207
+ <option value="canceled">Отменены</option>
208
+ </select>
209
+ </div>
210
+ </div>
211
+ <div class="sec-box">
212
+ <table class="sec-table">
212
213
  <thead><tr>
213
- <th>Заказ / Платёж</th>
214
+ <th>Заказ / Счёт</th>
214
215
  <th>Назначение</th>
215
216
  <th>Плательщик</th>
216
- <th class="num">Сумма</th>
217
+ <th>Сумма</th>
217
218
  <th>Статус</th>
218
- <th>Действия</th>
219
+ <th style="text-align:right">Действия</th>
219
220
  </tr></thead>
220
221
  <tbody id="admOrdersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
221
222
  </table>
222
223
  </div>
223
224
  </div>
224
225
 
225
- <!-- 3. ФРАНШИЗЫ И ПАРТНЁРЫ -->
226
- <div class="card" data-adm-sec="partners">
227
- <div class="sec-head" style="margin-bottom:14px">
228
- <div>
229
- <span class="kicker">Региональная партнёрская сеть</span>
230
- <h3 class="h3" style="margin:4px 0 0">Зарегистрировать партнёра-франчайзи вручную</h3>
231
- </div>
232
- </div>
233
- <div class="grid g4" style="align-items:end">
234
- <div class="field" style="margin:0">
235
- <label for="admPartRegion">Субъект РФ</label>
236
- <select id="admPartRegion">
237
- ${visibleRegions.map(r => `<option value="${esc(r.code)}" ${r.code === 'RU-RYA' ? 'selected' : ''}>${esc(r.name)} (${esc(r.district)})</option>`).join('')}
238
- </select>
226
+ <!-- 4. КЛИЕНТЫ И РОЛИ BOSS -->
227
+ <div data-adm-sec="users" style="display:none">
228
+ <div class="sec-bar" style="margin-bottom:12px">
229
+ <h2 class="sec-title">Зарегистрированные клиенты и права администраторов (BOSS)</h2>
230
+ <div class="sec-controls">
231
+ <input type="search" id="inpFilterUsers" placeholder="Поиск по Email, организации, ИНН…">
239
232
  </div>
240
- <div class="field" style="margin:0"><label for="admPartCompany">Компания-партнёр</label><input type="text" id="admPartCompany" value="ООО «ЦентрИнтеграция»"></div>
241
- <div class="field" style="margin:0"><label for="admPartInn">ИНН</label><input type="text" id="admPartInn" value="6234556677"></div>
242
- <div class="field" style="margin:0"><label for="admPartEmail">Email партнёра</label><input type="email" id="admPartEmail" value="partner@center-it.ru"></div>
243
- </div>
244
- <div style="display:flex;align-items:center;gap:14px;margin-top:14px;flex-wrap:wrap">
245
- <button class="btn btn-primary btn-sm" data-action="adm-register-partner">${icon('map')} Зарегистрировать партнёра</button>
246
- <span class="small muted" id="admPartnerStatus"></span>
247
233
  </div>
248
- </div>
249
-
250
- <div class="card" data-adm-sec="partners">
251
- <h3 class="h3" style="margin:0 0 12px">Резервы региональных франшиз и действующие партнёры</h3>
252
- <div class="table-wrap">
253
- <table class="t">
234
+ <div class="sec-box">
235
+ <table class="sec-table">
254
236
  <thead><tr>
255
- <th>ID / Тип</th>
256
- <th>Субъект РФ</th>
257
- <th>Компания / ИНН</th>
258
- <th>Условия / Скидка</th>
259
- <th>Статус и аттестация</th>
260
- <th>Действия</th>
237
+ <th>Email / ID</th>
238
+ <th>Роль</th>
239
+ <th>Организация / ИНН</th>
240
+ <th>Реестры клиента</th>
241
+ <th>Оферта (ПЭП)</th>
242
+ <th style="text-align:right">Вход</th>
261
243
  </tr></thead>
262
- <tbody id="admPartnersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
244
+ <tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
263
245
  </table>
264
246
  </div>
265
247
  </div>
266
248
 
267
- <!-- 4. ТИКЕТЫ SLA -->
268
- <div class="card" data-adm-sec="tickets">
269
- <h3 class="h3" style="margin:0 0 12px">Обращения техподдержки (MAX · Telegram · VK)</h3>
270
- <div class="table-wrap">
271
- <table class="t">
249
+ <!-- 5. ТИКЕТЫ ПОДДЕРЖКИ SLA -->
250
+ <div data-adm-sec="tickets" style="display:none">
251
+ <div class="sec-bar" style="margin-bottom:12px">
252
+ <h2 class="sec-title">Тикеты техподдержки SLA (MAX · Telegram · VK)</h2>
253
+ <div class="sec-controls">
254
+ <select id="selFilterTickets">
255
+ <option value="all">Все обращения</option>
256
+ <option value="open">Открытые</option>
257
+ <option value="answered">Отвеченные</option>
258
+ <option value="closed">Закрытые</option>
259
+ </select>
260
+ </div>
261
+ </div>
262
+ <div class="sec-box">
263
+ <table class="sec-table">
272
264
  <thead><tr>
273
265
  <th>Тикет / Канал</th>
274
- <th>Клиент / Лицензия</th>
266
+ <th>Клиент / Сервер</th>
275
267
  <th>SLA</th>
276
268
  <th>Переписка</th>
277
- <th>Ответ инженера</th>
269
+ <th style="text-align:right">Ответ инженера</th>
278
270
  </tr></thead>
279
271
  <tbody id="admTicketsBody"><tr><td colspan="5" class="muted">Загрузка…</td></tr></tbody>
280
272
  </table>
281
273
  </div>
282
274
  </div>
283
-
284
- <!-- 5. КЛИЕНТЫ И РОЛИ BOSS -->
285
- <div class="card" data-adm-sec="users">
286
- <h3 class="h3" style="margin:0 0 12px">Зарегистрированные клиенты, администраторы (BOSS) и договоры-оферты</h3>
287
- <div class="table-wrap">
288
- <table class="t">
289
- <thead><tr>
290
- <th>ID / Email</th>
291
- <th>Роль на сервере</th>
292
- <th>Организация / ИНН</th>
293
- <th>Лицензия комплекса</th>
294
- <th>Статус оферты (ПЭП)</th>
295
- <th>Дата регистрации</th>
296
- </tr></thead>
297
- <tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
298
- </table>
299
- </div>
300
- </div>
301
275
  </div>
302
276
  </div>
303
277
  </section>
304
278
 
305
279
  <footer>
306
280
  <div class="footer-bottom">
307
- <span>© ODANT · Консоль администратора центра лицензирования ${esc(P.domain)}</span>
308
- <span>Ключ центра сертификации Ed25519: <code>${esc(publicKey)}</code></span>
281
+ <span>© ODANT · ${esc(P.domain)}</span>
282
+ <span>Ключ центра сертификации Ed25519: <code>${esc(publicKey.slice(0, 28))}…</code></span>
309
283
  </div>
310
284
  </footer>
311
285
  </div>
312
286
 
313
- <!-- Просмотр подписанного сертификата Ed25519 -->
287
+ <!-- Модальное окно: Создание сервера / Выпуск лицензии клиенту -->
288
+ <div class="modal" id="admCreateServerModal" role="dialog" aria-modal="true">
289
+ <div class="card" style="max-width:540px">
290
+ <div class="modal-head">
291
+ <img src="/assets/odant-logo.png" alt="">
292
+ <div>
293
+ <h3 class="h3" style="margin:0">Добавить сервер / выпустить лицензию</h3>
294
+ <div class="small muted">Создание записи сервера в реестре с подписью Ed25519</div>
295
+ </div>
296
+ <button class="icon-btn" data-action="close-create-server-modal" aria-label="Закрыть">${icon('close')}</button>
297
+ </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>
302
+ </div>
303
+ <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>
306
+ </div>
307
+ <label style="display:flex;align-items:center;gap:10px;font-size:13.5px;margin:8px 0 14px;cursor:pointer">
308
+ <input type="checkbox" id="admNewSrvActivateBase" checked style="width:17px;height:17px">
309
+ <span>Сразу активировать базовую постоянную лицензию (10 вечных мест + ИИ-агенты)</span>
310
+ </label>
311
+ <div class="status" id="admCreateServerStatus" style="color:var(--danger);min-height:18px;margin-bottom:8px"></div>
312
+ <div style="display:flex;gap:10px;justify-content:flex-end">
313
+ <button class="btn btn-ghost" data-action="close-create-server-modal">Отмена</button>
314
+ <button class="btn btn-primary" data-action="submit-create-server">${icon('key')} Создать и подписать</button>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ <!-- Модальное окно: Начисление дополнительных мест серверу -->
320
+ <div class="modal" id="admGrantSeatsModal" role="dialog" aria-modal="true">
321
+ <div class="card" style="max-width:480px">
322
+ <div class="modal-head">
323
+ <img src="/assets/odant-logo.png" alt="">
324
+ <div>
325
+ <h3 class="h3" id="admGrantTitle" style="margin:0">Начислить доп. рабочие места</h3>
326
+ <div class="small muted" id="admGrantSub">Сервер WORK</div>
327
+ </div>
328
+ <button class="icon-btn" data-action="close-grant-modal" aria-label="Закрыть">${icon('close')}</button>
329
+ </div>
330
+ <input type="hidden" id="admGrantLicId" value="">
331
+ <div class="row2">
332
+ <div class="field">
333
+ <label for="admGrantSeatsCount">Количество доп. мест</label>
334
+ <input type="number" id="admGrantSeatsCount" min="1" max="2000" value="5">
335
+ </div>
336
+ <div class="field">
337
+ <label for="admGrantYears">Срок действия (лет)</label>
338
+ <input type="number" id="admGrantYears" min="1" max="5" value="1">
339
+ </div>
340
+ </div>
341
+ <div class="field">
342
+ <label for="admGrantTier">Уровень поддержки SLA</label>
343
+ <select id="admGrantTier">
344
+ <option value="start">Start (10 000 ₽/место в год)</option>
345
+ <option value="business" selected>Business (15 000 ₽/место в год)</option>
346
+ <option value="enterprise">Enterprise (20 000 ₽/место в год)</option>
347
+ </select>
348
+ </div>
349
+ <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:14px">
350
+ <button class="btn btn-ghost" data-action="close-grant-modal">Отмена</button>
351
+ <button class="btn btn-primary" data-action="submit-grant-seats">Начислить и выпустить .lic.json</button>
352
+ </div>
353
+ </div>
354
+ </div>
355
+
356
+ <!-- Модальное окно: 12-месячный график платежей по франшизе -->
357
+ <div class="modal" id="admFranchiseScheduleModal" role="dialog" aria-modal="true">
358
+ <div class="card" style="max-width:780px">
359
+ <div class="modal-head">
360
+ <img src="/assets/odant-logo.png" alt="">
361
+ <div>
362
+ <h3 class="h3" id="admSchedTitle" style="margin:0">График погашения франшизы (12 месяцев)</h3>
363
+ <div class="small muted" id="admSchedSub"></div>
364
+ </div>
365
+ <button class="icon-btn" data-action="close-sched-modal" aria-label="Закрыть">${icon('close')}</button>
366
+ </div>
367
+ <div id="admSchedSummary" style="margin-bottom:12px"></div>
368
+ <div style="max-height:420px;overflow-y:auto">
369
+ <table class="sec-table">
370
+ <thead><tr>
371
+ <th>Месяц</th>
372
+ <th>Дата платежа</th>
373
+ <th>Сумма</th>
374
+ <th>Статус</th>
375
+ <th style="text-align:right">Действие</th>
376
+ </tr></thead>
377
+ <tbody id="admSchedBody"></tbody>
378
+ </table>
379
+ </div>
380
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:14px;flex-wrap:wrap">
381
+ <div id="admSchedEarlyBox"></div>
382
+ <button class="btn btn-ghost" data-action="close-sched-modal">Закрыть</button>
383
+ </div>
384
+ </div>
385
+ </div>
386
+
387
+ <!-- Модальное окно: Просмотр сертификата и файлов лицензий (.lic.json) -->
314
388
  <div class="modal" id="certModal" role="dialog" aria-modal="true" aria-labelledby="certTitle">
315
- <div class="card" style="max-width:640px">
389
+ <div class="card" style="max-width:680px">
316
390
  <div class="modal-head">
317
391
  <img src="/assets/odant-logo.png" alt="">
318
- <div><h3 class="h3" id="certTitle" style="margin:0">Сертификат лицензии Ed25519</h3><div class="small muted" id="certSub">WORK-LIC-v1</div></div>
392
+ <div><h3 class="h3" id="certTitle" style="margin:0">Сертификат и файлы лицензий (.lic.json)</h3><div class="small muted" id="certSub">WORK-LIC-v1</div></div>
393
+ <button class="icon-btn" data-action="cert-close" aria-label="Закрыть">${icon('close')}</button>
319
394
  </div>
320
- <pre class="console" id="certJsonBox" style="max-height:360px;margin:0"></pre>
321
- <div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap">
395
+ <div id="certFilesList" style="margin-bottom:12px"></div>
396
+ <pre class="console" id="certJsonBox" style="max-height:320px;margin:0"></pre>
397
+ <div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;justify-content:flex-end">
322
398
  <button class="btn btn-primary" data-action="cert-copy">Скопировать JSON</button>
323
399
  <button class="btn btn-ok" data-action="cert-download">Скачать .json</button>
324
400
  <button class="btn btn-ghost" data-action="cert-close">Закрыть</button>
package/src/crypto.js CHANGED
@@ -289,6 +289,8 @@ export function createOwnerAccessToken({
289
289
  hubUrl = 'https://work.odant.org',
290
290
  accountId = '',
291
291
  email = '',
292
+ licenseId = '',
293
+ serverName = '',
292
294
  tokenId = '',
293
295
  secret = '',
294
296
  issuedAt = Date.now(),
@@ -298,6 +300,8 @@ export function createOwnerAccessToken({
298
300
  hubUrl: String(hubUrl || 'https://work.odant.org').replace(/\/+$/, ''),
299
301
  accountId: String(accountId || ''),
300
302
  email: String(email || '').toLowerCase(),
303
+ ...(licenseId ? { licenseId: String(licenseId) } : {}),
304
+ ...(serverName ? { serverName: String(serverName) } : {}),
301
305
  tokenId: String(tokenId || ('TOK-' + crypto.randomBytes(4).toString('hex').toUpperCase())),
302
306
  secret: String(secret || crypto.randomBytes(16).toString('hex')),
303
307
  issuedAt: Number(issuedAt || Date.now()),