work-license-server 1.0.13 → 1.0.15

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.13",
3
+ "version": "1.0.15",
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,15 @@
1
1
  /**
2
- * Отдельная защищённая страница панели администратора центра лицензирования и биллинга ODANT-WORK (/admin).
2
+ * Рабочая консоль администратора центра лицензирования и биллинга ODANT-WORK (/admin).
3
3
  *
4
- * Не связана ссылками с публичной витриной (/).
5
- * Требует обязательной авторизации администратора (POST /api/admin/login -> Bearer adm_...).
4
+ * Доступна пользователям с ролью BOSS (первый зарегистрированный по Email пользователь
5
+ * или назначенный администратор) по их штатной сессии без отдельных паролей.
6
+ *
7
+ * Разделы:
8
+ * 1. Реестр серверов (устройств) и лицензий клиентов
9
+ * 2. Реестр региональных франшиз и 12-месячных графиков погашения
10
+ * 3. Заказы ЮKassa и безналичные счета-договоры
11
+ * 4. Зарегистрированные клиенты, оферты (ПЭП) и роли BOSS
12
+ * 5. Тикеты техподдержки SLA (MAX · Telegram · VK)
6
13
  */
7
14
  import {
8
15
  PAK_PRODUCT,
@@ -15,7 +22,6 @@ import {
15
22
  import { icon } from './landing.js';
16
23
 
17
24
  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
25
  const jsonForScript = (v) => JSON.stringify(v).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
20
26
 
21
27
  export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
@@ -38,7 +44,7 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
38
44
  <head>
39
45
  <meta charset="utf-8">
40
46
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
41
- <title>ODANT-WORK · Консоль администратора центра лицензирования</title>
47
+ <title>ODANT-WORK · Админ-панель центра лицензирования и франшиз</title>
42
48
  <meta name="robots" content="noindex, nofollow">
43
49
  <meta name="theme-color" content="#04060b">
44
50
  <link rel="icon" type="image/png" href="/favicon.ico">
@@ -97,208 +103,197 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
97
103
  </div>
98
104
  </div>
99
105
 
100
- <!-- Основная консоль администратора (скрыта до успешной авторизации) -->
106
+ <!-- Основная операционная консоль администратора -->
101
107
  <div class="shell" id="adminAppShell" style="display:none">
102
- <header class="topbar" style="grid-template-columns: auto 1fr auto">
108
+ <header class="topbar">
103
109
  <a href="/" class="brand" style="text-decoration:none">
104
110
  <img src="/assets/odant-logo.png" alt="ODANT">
105
111
  <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>
112
+ <span class="brand-name">ODANT<b>·</b>WORK <span class="tag ok" style="margin-left:6px;vertical-align:middle">BOSS · АДМИН</span></span><br>
113
+ <span class="brand-sub">реестр серверов · реестр франшиз · платежи и счета</span>
108
114
  </span>
109
115
  </a>
110
- <div></div>
116
+
117
+ <nav class="tabs" id="admTopTabs" aria-label="Реестры администратора">
118
+ <button class="tab active" data-adm-tab="servers">Серверы и лицензии <span class="tag" id="tabCntServers" style="margin-left:4px">0</span></button>
119
+ <button class="tab" data-adm-tab="franchises">Франшизы и графики <span class="tag" id="tabCntFranchises" style="margin-left:4px">0</span></button>
120
+ <button class="tab" data-adm-tab="orders">Заказы и счета <span class="tag" id="tabCntOrders" style="margin-left:4px">0</span></button>
121
+ <button class="tab" data-adm-tab="users">Клиенты и роли BOSS <span class="tag" id="tabCntUsers" style="margin-left:4px">0</span></button>
122
+ <button class="tab" data-adm-tab="tickets">Тикеты SLA <span class="tag" id="tabCntTickets" style="margin-left:4px">0</span></button>
123
+ </nav>
124
+
111
125
  <div class="top-actions">
112
126
  <a href="/#addons" class="btn btn-ghost btn-sm" style="text-decoration:none">← Личный кабинет</a>
113
127
  <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>
128
+ <button class="btn btn-ghost btn-sm" data-action="registry">${icon('server')}<span>Обновить</span></button>
129
+ <button class="btn btn-ghost btn-sm" data-action="admin-logout" title="Завершить сессию">${icon('lock')}<span id="admUserLabel">Выйти</span></button>
117
130
  </div>
118
131
  </header>
119
132
 
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>
133
+ <section class="stage active" style="padding-top:20px">
134
+ <!-- Сводные показатели -->
135
+ <div class="stats" style="grid-template-columns:repeat(6,1fr);margin-bottom:20px">
136
+ <div class="stat"><b id="admStatLic">0</b><span id="admStatLicSub">серверов · 0 мест</span></div>
137
+ <div class="stat"><b id="admStatFranchises">0</b><span id="admStatFranchisesSub">активных франшиз</span></div>
123
138
  <div class="stat"><b id="admStatPaid">0 ₽</b><span id="admStatPaidSub">оплачено заказов: 0</span></div>
124
139
  <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>
140
+ <div class="stat"><b id="admStatUsers">0</b><span id="admStatUsersSub">клиентов в базе</span></div>
127
141
  <div class="stat"><b id="admStatTickets">0</b><span id="admStatTicketsSub">открытых тикетов SLA</span></div>
128
142
  </div>
129
143
 
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>
142
-
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>
149
- </div>
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>
164
- </div>
144
+ <!-- 1. РЕЕСТР СЕРВЕРОВ (УСТРОЙСТВ) И ЛИЦЕНЗИЙ -->
145
+ <div class="card adm-section" data-adm-sec="servers">
146
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:14px">
147
+ <div>
148
+ <span class="kicker">Реестр 1 · Устройства и лицензии</span>
149
+ <h3 class="h3" style="margin:2px 0 0">Серверы (устройства) WORK и лицензии клиентов</h3>
165
150
  </div>
166
- </aside>
167
-
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>
176
- </div>
177
- </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>
151
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
152
+ <input type="search" id="inpFilterServers" placeholder="Поиск по клиенту, Email, ИНН, серверу, HWID…" style="width:280px;padding:8px 12px;font-size:13px">
153
+ <select id="selFilterServers" style="padding:8px 12px;font-size:13px">
154
+ <option value="all">Все серверы</option>
155
+ <option value="base_active">Базовая активирована</option>
156
+ <option value="base_inactive">Без базовой лицензии</option>
157
+ <option value="hwid_bound">Железо (HWID) привязано</option>
158
+ </select>
159
+ <button class="btn btn-primary btn-sm" data-action="open-create-server-modal">${icon('plus')} Добавить сервер / лицензию</button>
188
160
  </div>
161
+ </div>
162
+ <div class="table-wrap">
163
+ <table class="t">
164
+ <thead><tr>
165
+ <th>Сервер / ID</th>
166
+ <th>Владелец / Организация</th>
167
+ <th>Привязка железа (HWID)</th>
168
+ <th>Базовая лицензия (10 вечн.)</th>
169
+ <th>Доп. места и SLA</th>
170
+ <th>Управление</th>
171
+ </tr></thead>
172
+ <tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
173
+ </table>
174
+ </div>
175
+ </div>
189
176
 
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">
194
- <thead><tr>
195
- <th>Лицензия / Код бота</th>
196
- <th>Организация / ИНН</th>
197
- <th>Привязка оборудования / Партнёр</th>
198
- <th class="num">Сессии (вечные + доп.)</th>
199
- <th>SLA и срок</th>
200
- <th>Сертификат</th>
201
- </tr></thead>
202
- <tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
203
- </table>
204
- </div>
177
+ <!-- 2. РЕЕСТР ФРАНШИЗ И ГРАФИКОВ ПОГАШЕНИЯ (12 МЕС.) -->
178
+ <div class="card adm-section" data-adm-sec="franchises" style="display:none">
179
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:14px">
180
+ <div>
181
+ <span class="kicker">Реестр 2 · Региональная партнёрская сеть</span>
182
+ <h3 class="h3" style="margin:2px 0 0">Региональные франшизы, резервы (15 мин) и годовые графики погашения</h3>
205
183
  </div>
206
-
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">
212
- <thead><tr>
213
- <th>Заказ / Платёж</th>
214
- <th>Назначение</th>
215
- <th>Плательщик</th>
216
- <th class="num">Сумма</th>
217
- <th>Статус</th>
218
- <th>Действия</th>
219
- </tr></thead>
220
- <tbody id="admOrdersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
221
- </table>
222
- </div>
184
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
185
+ <input type="search" id="inpFilterFranchises" placeholder="Поиск по региону, компании, ИНН, Email…" style="width:280px;padding:8px 12px;font-size:13px">
186
+ <select id="selFilterFranchises" style="padding:8px 12px;font-size:13px">
187
+ <option value="all">Все активные франшизы и резервы</option>
188
+ <option value="pending_deposit">Ожидают залога (таймер 15 мин)</option>
189
+ <option value="deposit_paid">Погашение по графику (12 мес.)</option>
190
+ <option value="overdue">С просрочкой ежемесячного платежа</option>
191
+ <option value="paid_in_full">Полностью погашены</option>
192
+ <option value="closed">Отменённые / Истёкшие</option>
193
+ </select>
223
194
  </div>
195
+ </div>
196
+ <div class="table-wrap">
197
+ <table class="t">
198
+ <thead><tr>
199
+ <th>Субъект РФ / ID</th>
200
+ <th>Франчайзи / Контакты</th>
201
+ <th>Начальный взнос (залог 100 тыс. ₽)</th>
202
+ <th>График погашения (12 мес.)</th>
203
+ <th>Финансовая сводка</th>
204
+ <th>Управление</th>
205
+ </tr></thead>
206
+ <tbody id="admFranchisesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
207
+ </table>
208
+ </div>
209
+ </div>
224
210
 
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>
239
- </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
- </div>
211
+ <!-- 3. ЗАКАЗЫ ЮKASSA И БЕЗНАЛИЧНЫЕ СЧЕТА-ДОГОВОРЫ -->
212
+ <div class="card adm-section" data-adm-sec="orders" style="display:none">
213
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:14px">
214
+ <div>
215
+ <span class="kicker">Реестр 3 · Биллинг и расчёты</span>
216
+ <h3 class="h3" style="margin:2px 0 0">Заказы ЮKassa, счета-договоры юрлиц и взносы по франшизам</h3>
248
217
  </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">
254
- <thead><tr>
255
- <th>ID / Тип</th>
256
- <th>Субъект РФ</th>
257
- <th>Компания / ИНН</th>
258
- <th>Условия / Скидка</th>
259
- <th>Статус и аттестация</th>
260
- <th>Действия</th>
261
- </tr></thead>
262
- <tbody id="admPartnersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
263
- </table>
264
- </div>
218
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
219
+ <input type="search" id="inpFilterOrders" placeholder="Поиск по номеру заказа, счёту, плательщику, ИНН…" style="width:280px;padding:8px 12px;font-size:13px">
220
+ <select id="selFilterOrders" style="padding:8px 12px;font-size:13px">
221
+ <option value="all">Все заказы</option>
222
+ <option value="pending">Ожидают оплаты</option>
223
+ <option value="paid">Оплачены</option>
224
+ <option value="canceled">Отменены</option>
225
+ </select>
265
226
  </div>
227
+ </div>
228
+ <div class="table-wrap">
229
+ <table class="t">
230
+ <thead><tr>
231
+ <th>Заказ / Счёт / Дата</th>
232
+ <th>Назначение и объект</th>
233
+ <th>Плательщик / ИНН</th>
234
+ <th class="num">Сумма</th>
235
+ <th>Статус</th>
236
+ <th>Действия</th>
237
+ </tr></thead>
238
+ <tbody id="admOrdersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
239
+ </table>
240
+ </div>
241
+ </div>
266
242
 
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">
272
- <thead><tr>
273
- <th>Тикет / Канал</th>
274
- <th>Клиент / Лицензия</th>
275
- <th>SLA</th>
276
- <th>Переписка</th>
277
- <th>Ответ инженера</th>
278
- </tr></thead>
279
- <tbody id="admTicketsBody"><tr><td colspan="5" class="muted">Загрузка…</td></tr></tbody>
280
- </table>
281
- </div>
243
+ <!-- 4. КЛИЕНТЫ, ОФЕРТЫ ПЭП И РОЛИ BOSS -->
244
+ <div class="card adm-section" data-adm-sec="users" style="display:none">
245
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:14px">
246
+ <div>
247
+ <span class="kicker">Реестр 4 · Аккаунты владельцев и права доступа</span>
248
+ <h3 class="h3" style="margin:2px 0 0">Зарегистрированные клиенты, подписанные оферты (ПЭП) и администраторы (BOSS)</h3>
249
+ </div>
250
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
251
+ <input type="search" id="inpFilterUsers" placeholder="Поиск по Email, организации, ИНН…" style="width:280px;padding:8px 12px;font-size:13px">
282
252
  </div>
253
+ </div>
254
+ <div class="table-wrap">
255
+ <table class="t">
256
+ <thead><tr>
257
+ <th>Email / ID</th>
258
+ <th>Роль на сервере</th>
259
+ <th>Организация / ИНН</th>
260
+ <th>Серверов / Франшиз</th>
261
+ <th>Договор-оферта (ПЭП)</th>
262
+ <th>Регистрация / Вход</th>
263
+ </tr></thead>
264
+ <tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
265
+ </table>
266
+ </div>
267
+ </div>
283
268
 
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>
269
+ <!-- 5. ТИКЕТЫ SLA -->
270
+ <div class="card adm-section" data-adm-sec="tickets" style="display:none">
271
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:14px">
272
+ <div>
273
+ <span class="kicker">Реестр 5 · Служба поддержки</span>
274
+ <h3 class="h3" style="margin:2px 0 0">Обращения техподдержки SLA (MAX · Telegram · VK)</h3>
275
+ </div>
276
+ <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
277
+ <select id="selFilterTickets" style="padding:8px 12px;font-size:13px">
278
+ <option value="all">Все обращения</option>
279
+ <option value="open">Открытые (ожидают ответа)</option>
280
+ <option value="answered">Отвеченные</option>
281
+ <option value="closed">Закрытые</option>
282
+ </select>
300
283
  </div>
301
284
  </div>
285
+ <div class="table-wrap">
286
+ <table class="t">
287
+ <thead><tr>
288
+ <th>Тикет / Канал</th>
289
+ <th>Клиент / Сервер</th>
290
+ <th>Тариф SLA</th>
291
+ <th>Переписка</th>
292
+ <th>Ответ инженера</th>
293
+ </tr></thead>
294
+ <tbody id="admTicketsBody"><tr><td colspan="5" class="muted">Загрузка…</td></tr></tbody>
295
+ </table>
296
+ </div>
302
297
  </div>
303
298
  </section>
304
299
 
@@ -310,15 +305,117 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
310
305
  </footer>
311
306
  </div>
312
307
 
313
- <!-- Просмотр подписанного сертификата Ed25519 -->
308
+ <!-- Модальное окно: Создание сервера / Выпуск лицензии клиенту -->
309
+ <div class="modal" id="admCreateServerModal" role="dialog" aria-modal="true">
310
+ <div class="card" style="max-width:540px">
311
+ <div class="modal-head">
312
+ <img src="/assets/odant-logo.png" alt="">
313
+ <div>
314
+ <h3 class="h3" style="margin:0">Добавить сервер / выпустить лицензию</h3>
315
+ <div class="small muted">Создание записи сервера в реестре с подписью Ed25519</div>
316
+ </div>
317
+ <button class="icon-btn" data-action="close-create-server-modal" aria-label="Закрыть">${icon('close')}</button>
318
+ </div>
319
+ <div class="field"><label for="admNewSrvName">Название сервера (устройства)</label><input type="text" id="admNewSrvName" placeholder="Основной сервер WORK"></div>
320
+ <div class="row2">
321
+ <div class="field"><label for="admNewSrvOrg">Организация клиента</label><input type="text" id="admNewSrvOrg" placeholder="ООО «Компания»"></div>
322
+ <div class="field"><label for="admNewSrvInn">ИНН</label><input type="text" id="admNewSrvInn" placeholder="7701234567"></div>
323
+ </div>
324
+ <div class="row2">
325
+ <div class="field"><label for="admNewSrvEmail">Email владельца *</label><input type="email" id="admNewSrvEmail" placeholder="client@company.ru"></div>
326
+ <div class="field"><label for="admNewSrvHwid">HWID (необязательно)</label><input type="text" id="admNewSrvHwid" placeholder="PAK-..."></div>
327
+ </div>
328
+ <label style="display:flex;align-items:center;gap:10px;font-size:13.5px;margin:8px 0 14px;cursor:pointer">
329
+ <input type="checkbox" id="admNewSrvActivateBase" checked style="width:17px;height:17px">
330
+ <span>Сразу активировать базовую постоянную лицензию (10 вечных мест + ИИ-агенты)</span>
331
+ </label>
332
+ <div class="status" id="admCreateServerStatus" style="color:var(--danger);min-height:18px;margin-bottom:8px"></div>
333
+ <div style="display:flex;gap:10px;justify-content:flex-end">
334
+ <button class="btn btn-ghost" data-action="close-create-server-modal">Отмена</button>
335
+ <button class="btn btn-primary" data-action="submit-create-server">${icon('key')} Создать и подписать</button>
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ <!-- Модальное окно: Начисление дополнительных мест серверу -->
341
+ <div class="modal" id="admGrantSeatsModal" role="dialog" aria-modal="true">
342
+ <div class="card" style="max-width:480px">
343
+ <div class="modal-head">
344
+ <img src="/assets/odant-logo.png" alt="">
345
+ <div>
346
+ <h3 class="h3" id="admGrantTitle" style="margin:0">Начислить доп. рабочие места</h3>
347
+ <div class="small muted" id="admGrantSub">Сервер WORK</div>
348
+ </div>
349
+ <button class="icon-btn" data-action="close-grant-modal" aria-label="Закрыть">${icon('close')}</button>
350
+ </div>
351
+ <input type="hidden" id="admGrantLicId" value="">
352
+ <div class="row2">
353
+ <div class="field">
354
+ <label for="admGrantSeatsCount">Количество доп. мест</label>
355
+ <input type="number" id="admGrantSeatsCount" min="1" max="2000" value="5">
356
+ </div>
357
+ <div class="field">
358
+ <label for="admGrantYears">Срок действия (лет)</label>
359
+ <input type="number" id="admGrantYears" min="1" max="5" value="1">
360
+ </div>
361
+ </div>
362
+ <div class="field">
363
+ <label for="admGrantTier">Уровень поддержки SLA</label>
364
+ <select id="admGrantTier">
365
+ <option value="start">Start (10 000 ₽/место в год)</option>
366
+ <option value="business" selected>Business (15 000 ₽/место в год)</option>
367
+ <option value="enterprise">Enterprise (20 000 ₽/место в год)</option>
368
+ </select>
369
+ </div>
370
+ <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:14px">
371
+ <button class="btn btn-ghost" data-action="close-grant-modal">Отмена</button>
372
+ <button class="btn btn-primary" data-action="submit-grant-seats">Начислить и выпустить .lic.json</button>
373
+ </div>
374
+ </div>
375
+ </div>
376
+
377
+ <!-- Модальное окно: 12-месячный график платежей по франшизе -->
378
+ <div class="modal" id="admFranchiseScheduleModal" role="dialog" aria-modal="true">
379
+ <div class="card" style="max-width:780px">
380
+ <div class="modal-head">
381
+ <img src="/assets/odant-logo.png" alt="">
382
+ <div>
383
+ <h3 class="h3" id="admSchedTitle" style="margin:0">График погашения франшизы (12 месяцев)</h3>
384
+ <div class="small muted" id="admSchedSub"></div>
385
+ </div>
386
+ <button class="icon-btn" data-action="close-sched-modal" aria-label="Закрыть">${icon('close')}</button>
387
+ </div>
388
+ <div id="admSchedSummary" style="margin-bottom:12px"></div>
389
+ <div class="table-wrap" style="max-height:420px">
390
+ <table class="t">
391
+ <thead><tr>
392
+ <th>Месяц</th>
393
+ <th>Дата платежа</th>
394
+ <th class="num">Сумма</th>
395
+ <th>Статус</th>
396
+ <th style="text-align:right">Действие администратора</th>
397
+ </tr></thead>
398
+ <tbody id="admSchedBody"></tbody>
399
+ </table>
400
+ </div>
401
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:14px;flex-wrap:wrap">
402
+ <div id="admSchedEarlyBox"></div>
403
+ <button class="btn btn-ghost" data-action="close-sched-modal">Закрыть</button>
404
+ </div>
405
+ </div>
406
+ </div>
407
+
408
+ <!-- Модальное окно: Просмотр сертификата и файлов лицензий (.lic.json) -->
314
409
  <div class="modal" id="certModal" role="dialog" aria-modal="true" aria-labelledby="certTitle">
315
- <div class="card" style="max-width:640px">
410
+ <div class="card" style="max-width:680px">
316
411
  <div class="modal-head">
317
412
  <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>
413
+ <div><h3 class="h3" id="certTitle" style="margin:0">Сертификат и файлы лицензий (.lic.json)</h3><div class="small muted" id="certSub">WORK-LIC-v1</div></div>
414
+ <button class="icon-btn" data-action="cert-close" aria-label="Закрыть">${icon('close')}</button>
319
415
  </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">
416
+ <div id="certFilesList" style="margin-bottom:12px"></div>
417
+ <pre class="console" id="certJsonBox" style="max-height:320px;margin:0"></pre>
418
+ <div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;justify-content:flex-end">
322
419
  <button class="btn btn-primary" data-action="cert-copy">Скопировать JSON</button>
323
420
  <button class="btn btn-ok" data-action="cert-download">Скачать .json</button>
324
421
  <button class="btn btn-ghost" data-action="cert-close">Закрыть</button>