work-license-server 1.0.6 → 1.0.8

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.6",
3
+ "version": "1.0.8",
4
4
  "description": "Лицензионный сервер, биллинг ЮKassa, боты поддержки (MAX, Telegram, VK) и лендинг ПАК WORK (work.odant.org)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/landing.js CHANGED
@@ -180,24 +180,19 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
180
180
 
181
181
  <nav class="tabs" id="tabs" aria-label="Разделы">
182
182
  <span class="tab-ind" id="tabInd"></span>
183
- <button class="tab active" data-stage="product">Платформа</button>
184
- <button class="tab" data-stage="agents">ИИ-агенты</button>
185
- <button class="tab" data-stage="pricing">Тарифы</button>
186
- <button class="tab" data-stage="order">Комплекс</button>
187
- <button class="tab" id="tileAddons" data-stage="addons" style="display:none;">Докупка лицензий</button>
183
+ <button class="tab active" data-stage="product">О платформе</button>
184
+ <button class="tab" data-stage="clients">Клиентам</button>
188
185
  <button class="tab" data-stage="franchise">Франшиза</button>
189
- <button class="tab" data-stage="support">Поддержка</button>
186
+ <button class="tab" id="tileAddons" data-stage="addons" style="display:none;">Личный кабинет</button>
190
187
  </nav>
191
188
 
192
189
  <div class="top-actions">
193
190
  <button class="icon-btn" id="btnTheme" onclick="toggleTheme()" title="Светлая / тёмная тема" aria-label="Переключить тему">${icon('sun')}</button>
194
- <button class="btn btn-ghost btn-sm" data-action="open-oferta" title="Публичный договор-оферта ODANT-WORK">${icon('docs')}<span>Оферта</span></button>
195
- <button class="btn btn-ghost btn-sm" id="btnAuth" data-action="auth">${icon('user')}<span id="btnAuthText">Войти / Регистрация</span></button>
196
- <button class="btn btn-primary btn-sm" data-stage="order">Заказать</button>
191
+ <button class="btn btn-primary btn-sm" id="btnAuth" data-action="auth">${icon('user')}<span id="btnAuthText">Войти / Регистрация</span></button>
197
192
  </div>
198
193
  </header>
199
194
 
200
- <!-- ============ ПЛАТФОРМА ============ -->
195
+ <!-- ============ ЛЕНДИНГ: ПЛАТФОРМА И ИИ-АГЕНТЫ (при клике на лого) ============ -->
201
196
  <section class="stage active" id="stage-product">
202
197
  <div class="hero">
203
198
  <div>
@@ -205,8 +200,8 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
205
200
  <h1 class="h1">Учёт, документооборот, процессы и&nbsp;<span class="grad">приватный ИИ</span> — в&nbsp;одном комплексе</h1>
206
201
  <p class="lead">ODANT-WORK — вычислительный центр предприятия, который ставится в серверную и работает автономно. Встроенная система учёта, единое пространство документов и задач, ИИ-консультант в каждой форме и команда ИИ-агентов — без облаков и передачи данных наружу.</p>
207
202
  <div class="hero-cta">
208
- <button class="btn btn-primary btn-lg" data-stage="order">Заказать комплекс · ${rub(P.basePriceRub)}</button>
209
- <button class="btn btn-ghost btn-lg" data-stage="agents">${icon('agents')} Как работают ИИ-агенты</button>
203
+ <button class="btn btn-primary btn-lg" data-stage="clients">Клиентам · Условия и покупка</button>
204
+ <button class="btn btn-ghost btn-lg" data-stage="franchise">Партнёрская франшиза</button>
210
205
  </div>
211
206
  <div class="trust">
212
207
  <span>${icon('shield')} Данные не покидают контур</span>
@@ -253,10 +248,9 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
253
248
  <span class="kicker">Паспорт комплекса</span>
254
249
  <h2 class="h2">ODANT-WORK · ${esc(P.sku)}</h2>
255
250
  <div class="price-big">${rub(P.basePriceRub)}</div>
256
- <p class="note">Вечная лицензия на ${P.baseSeats} одновременных рабочих сессий, привязанная к оборудованию, и ${P.includedSupportYears} год максимальной поддержки уровня Enterprise (24/7, реакция ≤ 15 мин) — в комплекте. Дальнейшее продление поддержки и докупка мест — по желанию.</p>
251
+ <p class="note">Вечная лицензия на ${P.baseSeats} одновременных рабочих сессий и ${P.includedSupportYears} год максимальной поддержки уровня Enterprise — в комплекте с машиной (или ${rub(P.baseLicensePriceRub || 4_000_000)} за базовую лицензию без оборудования).</p>
257
252
  <div class="hero-cta" style="margin-top:20px">
258
- <button class="btn btn-primary" data-stage="order">Оформить заказ</button>
259
- <button class="btn btn-ghost" data-stage="pricing">Тарифы поддержки</button>
253
+ <button class="btn btn-primary" data-stage="clients">Условия и покупка</button>
260
254
  </div>
261
255
  </div>
262
256
  <dl class="spec">
@@ -266,7 +260,7 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
266
260
  <div><dt>Память</dt><dd>${esc(H.ram)}</dd></div>
267
261
  <div><dt>Накопитель</dt><dd>${esc(H.storage)}</dd></div>
268
262
  <div><dt>Защита</dt><dd>${esc(H.security)}</dd></div>
269
- <div><dt>Лицензия</dt><dd>По одновременным сессиям, привязка к HWID и ключу узла</dd></div>
263
+ <div><dt>Лицензия</dt><dd>По одновременным сессиям, файлы .lic.json в #system</dd></div>
270
264
  </dl>
271
265
  </div>
272
266
  </div>
@@ -279,21 +273,10 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
279
273
  <p>Интегрированные локальные модели предназначены для базовых ежедневных операций внутри контура. Если требуются более мощные модели ИИ, их можно подключить через API из любого источника.</p>
280
274
  </div>
281
275
  <div class="grid g4">${renderModels()}</div>
282
- <div class="card reveal" style="display:flex;align-items:center;justify-content:space-between;gap:20px;flex-wrap:wrap">
283
- <div>
284
- <span class="kicker">Подключение мощных моделей ИИ по API</span>
285
- <h3 class="h3" style="margin:4px 0 6px">Нужны более мощные модели? Подключайте из любого источника</h3>
286
- <p class="note" style="margin:0">Предустановленные на комплексе модели закрывают базовые операции учёта, голоса, иллюстраций и поиска без внешних каналов. Для сложной аналитики, больших контекстов и специализированных агентов вы можете в один клик подключить по стандартному API модели из любого источника: внешних облачных провайдеров, корпоративного GPU-кластера или коммерческого API регионального партнёра.</p>
287
- </div>
288
- <button class="btn btn-ghost btn-sm" data-stage="agents">Подробнее об ИИ-агентах →</button>
289
- </div>
290
- </section>
291
276
 
292
- <!-- ============ ИИ-АГЕНТЫ ============ -->
293
- <section class="stage" id="stage-agents">
294
- <div class="agents-hero">
277
+ <div class="agents-hero" style="margin-top:28px">
295
278
  <div>
296
- <span class="kicker">ИИ-консультант</span>
279
+ <span class="kicker">ИИ-консультант и мультиагентная система</span>
297
280
  <h2 class="h2" style="font-size:clamp(2rem,3.6vw,3.2rem)">Помощник, который <span class="grad">знает вашу организацию</span></h2>
298
281
  <p class="lead">Консультант встроен в каждую форму системы. Он видит открытый документ, понимает структуру учёта и права пользователя — и не просто отвечает, а выполняет работу.</p>
299
282
  <ul class="list">${AI_PLATFORM.consultant.features.map(f => `<li>${esc(f)}</li>`).join('')}</ul>
@@ -324,10 +307,6 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
324
307
  </div>
325
308
  </div>
326
309
 
327
- <div class="sec-head reveal">
328
- <div><span class="kicker">Оркестрация</span><h2 class="h2">Как агенты работают вместе</h2></div>
329
- <p>Параллельность и скорость — без потери контроля: права, подтверждения и журнал действий заложены в код платформы, а не в текст подсказки.</p>
330
- </div>
331
310
  <div class="grid g3">${renderOrchestration()}</div>
332
311
 
333
312
  <div class="card reveal">
@@ -339,110 +318,106 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
339
318
  </div>
340
319
  </section>
341
320
 
342
- <!-- ============ ТАРИФЫ ============ -->
343
- <section class="stage" id="stage-pricing">
321
+ <!-- ============ КЛИЕНТАМ: УСЛОВИЯ И ПОКУПКА ============ -->
322
+ <section class="stage" id="stage-clients">
344
323
  <div class="sec-head">
345
324
  <div>
346
- <span class="kicker">Лицензии и поддержка</span>
347
- <h2 class="h2">10 вечных лицензий + 1 год Enterprise в базе, дальше — по желанию</h2>
348
- </div>
349
- <p>${P.baseSeats} рабочих мест входят в комплекс навсегда вместе с 1-м годом поддержки уровня Enterprise. Дальнейшее продление поддержки и докупка годовых мест — по желанию. Считаются одновременные активные сессии, а не учётные записи.</p>
350
- </div>
351
- <div class="tiers">${renderTiers()}</div>
352
- <div class="card">
353
- <div class="sec-head" style="margin-bottom:16px">
354
- <div><span class="kicker">Скидка от количества</span><h3 class="h3" style="font-size:20px">Чем больше мест — тем ниже цена каждого</h3></div>
355
- <p class="small">Скидка применяется ко всему пакету докупаемых мест.</p>
325
+ <span class="kicker">Клиентам · Условия и покупка</span>
326
+ <h2 class="h2">Выберите вариант поставки и количество пользователей</h2>
356
327
  </div>
357
- ${renderLadder('ladderPricing')}
328
+ <p>Сразу после установки из <code>npm</code> система работает бесплатно в режиме настройки (1 сессия BOSS, ИИ-агенты отключены). Первичная постоянная лицензия открывает <b>${P.baseSeats} вечных мест</b> и включает агентскую ИИ-систему.</p>
358
329
  </div>
359
- </section>
360
330
 
361
- <!-- ============ ЗАКАЗ КОМПЛЕКСА ============ -->
362
- <section class="stage" id="stage-order">
363
- <div class="sec-head">
364
- <div><span class="kicker">Заказ</span><h2 class="h2">Программно-аппаратный комплекс ODANT-WORK</h2></div>
365
- <p>Сервер AMD Ryzen™ AI Max+ PRO 495 · 192 ГБ · 4 ТБ с установленной платформой, вечной лицензией на ${P.baseSeats} мест и 1 годом поддержки Enterprise (дальше — по желанию). Оплата через ЮKassa с чеком по 54-ФЗ.</p>
366
- </div>
367
- <div class="card oferta-banner" data-oferta-banner></div>
368
331
  <div class="split">
369
332
  <div class="card">
370
333
  <div class="field">
371
- <label>Вариант базовой поставки</label>
334
+ <label>1. Базовая постоянная лицензия (10 вечных пользователей + ИИ-агенты)</label>
372
335
  <div class="seg" id="segPakVariant">
373
- <button data-variant="pak" class="on">Комплект с машиной<small>${rub(P.basePriceRub)} · железо + 10 мест</small></button>
374
- <button data-variant="license_only">Базовая лицензия без железа<small>${rub(P.baseLicensePriceRub || 4_000_000)} · 10 вечных мест</small></button>
336
+ <button data-variant="pak" class="on">С машинкой (ПАК)<small>${rub(P.basePriceRub)} · сервер + 10 вечных мест</small></button>
337
+ <button data-variant="license_only">Без машинки (лицензия)<small>${rub(P.baseLicensePriceRub || 4_000_000)} · 10 вечных мест</small></button>
338
+ <button data-variant="addons_only">Только доп. юзеры<small>0 ₽ · базовая уже есть</small></button>
375
339
  </div>
376
340
  </div>
377
- <div class="row2">
378
- <div class="field"><label for="inpPakOrg">Организация</label><input type="text" id="inpPakOrg" value="ООО «Маслоторг»"></div>
379
- <div class="field"><label for="inpPakInn">ИНН</label><input type="text" id="inpPakInn" value="7701234567" inputmode="numeric"></div>
380
- </div>
381
- <div class="field"><label for="inpPakEmail">Email для счёта и личного кабинета</label><input type="email" id="inpPakEmail" value="admin@maslotorg.ru"></div>
382
341
  <div class="field">
383
- <label>Дополнительные годовые места сразу в поставку: <b id="lblPakSeats" style="color:var(--text)">0</b></label>
384
- <input type="range" id="inpPakExtraSeats" min="0" max="150" value="0">
342
+ <label>2. Дополнительные временные пользователи (сверх 10 вечных)</label>
343
+ <div style="display:flex;align-items:center;gap:16px">
344
+ <input type="range" id="inpPakExtraSeats" min="0" max="150" value="0" style="flex:1">
345
+ <input type="number" id="inpPakExtraSeatsNum" min="0" max="2000" value="0" style="width:110px">
346
+ </div>
385
347
  </div>
386
- <div class="field">
387
- <label>Уровень поддержки дополнительных мест</label>
388
- <div class="seg" id="segPakTier">${Object.values(SLA_TIERS).map(t => `<button data-tier="${t.id}" class="${t.id === 'business' ? 'on' : ''}">${esc(t.name)}<small>${rub(t.pricePerUserYearRub)}/год</small></button>`).join('')}</div>
348
+ <div class="row2">
349
+ <div class="field">
350
+ <label>Уровень поддержки доп. мест</label>
351
+ <div class="seg" id="segPakTier">${Object.values(SLA_TIERS).map(t => `<button data-tier="${t.id}" class="${t.id === 'business' ? 'on' : ''}">${esc(t.name)}<small>${rub(t.pricePerUserYearRub)}/год</small></button>`).join('')}</div>
352
+ </div>
353
+ <div class="field">
354
+ <label for="inpPakYears">Срок временных мест, лет (1–5)</label>
355
+ <input type="number" id="inpPakYears" min="1" max="5" value="1">
356
+ </div>
389
357
  </div>
390
- <p class="note muted" style="margin:6px 0 0">Уже есть комплекс? <a href="#" data-action="addons">Войдите в кабинет и подпишите оферту</a>, чтобы докупить места со скидкой или получить токен привязки.</p>
358
+ <div class="field" style="margin-top:6px"><label>Скидка от количества дополнительных мест</label>${renderLadder('ladderPricing')}</div>
391
359
  </div>
360
+
392
361
  <div class="card summary card-glow">
393
- <span class="kicker">Итого</span>
362
+ <span class="kicker">Ваш выбор</span>
394
363
  <div class="sum-total" id="lblPakTotal">${rub(P.basePriceRub)}</div>
364
+ <div class="small" id="lblPakDiscount" style="color:var(--ok);font-weight:650">Включает 10 вечных мест и 1 год поддержки Enterprise</div>
395
365
  <div class="sum-lines" id="lblPakBreakdown"></div>
396
- <button class="btn btn-primary btn-lg" style="width:100%" data-action="order-pak">Оформить и оплатить через ЮKassa</button>
397
- <button class="btn btn-ghost btn-sm" style="width:100%;margin-top:8px" data-action="order-pak-invoice">Выставить счёт-договор (для юр. лиц)</button>
366
+ <button class="btn btn-primary btn-lg" style="width:100%" data-action="buy-client-config">Купить</button>
367
+ <p class="small muted" style="margin:8px 0 0;text-align:center">При нажатии «Купить» заказ добавится в ваш Личный кабинет, где вы сможете оплатить его через ЮKassa или выставить счёт-договор для юр. лица.</p>
398
368
  <div class="status" id="pakOrderStatus"></div>
399
369
  </div>
400
370
  </div>
371
+
372
+ <div class="sec-head" style="margin-top:12px">
373
+ <div>
374
+ <span class="kicker">Тарифы поддержки и конкурентных мест</span>
375
+ <h2 class="h2">Условия годовых лицензий и SLA</h2>
376
+ </div>
377
+ <p>Считаются одновременные активные сессии, а не число заведённых сотрудников в базе.</p>
378
+ </div>
379
+ <div class="tiers">${renderTiers()}</div>
401
380
  </section>
402
381
 
403
- <!-- ============ ДОКУПКА ЛИЦЕНЗИЙ И ЛИЧНЫЙ КАБИНЕТ (только после входа и подписания оферты) ============ -->
382
+ <!-- ============ ЛИЧНЫЙ КАБИНЕТ (открывается после входа / регистрации) ============ -->
404
383
  <section class="stage" id="stage-addons">
405
384
  <div class="sec-head">
406
- <div><span class="kicker">Личный кабинет владельца</span><h2 class="h2">Токен привязки локальной системы и покупка лицензий</h2></div>
385
+ <div><span class="kicker">Личный кабинет владельца</span><h2 class="h2">Заказы, токен привязки и лицензии</h2></div>
407
386
  <p id="userBadgeBox"></p>
408
387
  </div>
409
- <div class="card oferta-banner" data-oferta-banner></div>
388
+
410
389
  <div class="card" style="margin-bottom:20px">
411
- <span class="kicker">Ключ доступа для локального сервера WORK</span>
412
- <h3 class="h3" style="margin:4px 0 8px">Токен владельца (содержит URL сервера поддержки и ключ синхронизации лицензий)</h3>
413
- <p class="small" style="margin:0 0 10px">Скопируйте этот токен и вставьте его в раздел «Лицензирование» вашей локальной системы ODANT-WORK. Система автоматически привяжется к личному кабинету и загрузит файлы лицензий в папку <code>#system</code>.</p>
414
- <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
415
- <input type="text" id="inpCabinetHubToken" readonly placeholder="owk1..." style="flex:1;min-width:260px;font-family:monospace">
416
- <button class="btn btn-ghost btn-sm" data-action="gen-hub-token">Сгенерировать / Обновить токен</button>
390
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
391
+ <div>
392
+ <span class="kicker">Заказы и оплата</span>
393
+ <h3 class="h3" style="margin:4px 0 0">Ваши заказы в Личном кабинете</h3>
394
+ </div>
395
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
396
+ <button class="btn btn-ghost btn-sm" data-stage="clients">+ Добавить лицензию / ПАК</button>
397
+ <button class="btn btn-ghost btn-sm" data-stage="franchise">+ Зарезервировать франшизу</button>
398
+ <button class="btn btn-ghost btn-sm" data-action="refresh-cabinet">Обновить</button>
399
+ </div>
417
400
  </div>
401
+ <div id="cabinetOrdersBox" style="margin-top:14px"></div>
418
402
  </div>
403
+
419
404
  <div class="split">
420
405
  <div class="card">
421
- <div class="field">
422
- <label>Количество дополнительных мест</label>
423
- <div style="display:flex;align-items:center;gap:16px">
424
- <input type="range" id="rngAddonSeats" min="1" max="150" value="10" style="flex:1">
425
- <input type="number" id="inpSeats" min="1" max="2000" value="10" style="width:110px">
426
- </div>
427
- </div>
428
- <div class="field">
429
- <label>Уровень поддержки</label>
430
- <div class="seg" id="segTier">${Object.values(SLA_TIERS).map(t => `<button data-tier="${t.id}" class="${t.id === 'business' ? 'on' : ''}">${esc(t.name)}<small>${rub(t.pricePerUserYearRub)} · ${esc(t.reactionLabel)}</small></button>`).join('')}</div>
431
- </div>
432
- <div class="row2">
433
- <div class="field"><label for="inpYears">Срок, лет (кратно годам)</label><input type="number" id="inpYears" min="1" max="5" value="1"></div>
434
- <div class="field"><label for="inpLicId">Лицензия комплекса</label><input type="text" id="inpLicId" readonly></div>
406
+ <span class="kicker">Привязка локального сервера WORK</span>
407
+ <h3 class="h3" style="margin:4px 0 8px">Токен владельца (содержит URL сервера поддержки)</h3>
408
+ <p class="small" style="margin:0 0 12px">Скопируйте этот токен и вставьте его в разделе <b>WORK → Лицензия</b> вашей локальной системы. По этому токену система сама скачивает и обновляет оплаченные файлы лицензий в папку <code>#system</code>.</p>
409
+ <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
410
+ <input type="text" id="inpCabinetHubToken" readonly placeholder="owk1..." style="flex:1;min-width:220px;font-family:monospace">
411
+ <button class="btn btn-primary btn-sm" data-action="copy-hub-token">Скопировать токен</button>
412
+ <button class="btn btn-ghost btn-sm" data-action="gen-hub-token">Перевыпустить</button>
435
413
  </div>
436
- <div class="field" style="margin-top:6px"><label>Скидка от количества</label>${renderLadder('ladderAddons')}</div>
414
+ <div class="small" id="tokenCopyStatus" style="color:var(--ok);min-height:18px;margin-top:6px"></div>
437
415
  </div>
438
- <div class="card summary card-glow">
439
- <span class="kicker">К оплате</span>
440
- <div class="sum-total" id="lblTotal">0 ₽</div>
441
- <div class="small" id="lblDiscountBadge" style="color:var(--ok);font-weight:650"></div>
442
- <div class="sum-lines" id="lblBreakdown"></div>
443
- <button class="btn btn-primary btn-lg" style="width:100%" data-action="order-addons">Оплатить через ЮKassa</button>
444
- <button class="btn btn-ghost btn-sm" style="width:100%;margin-top:8px" data-action="order-addons-invoice">Выставить счёт-договор (для юр. лиц)</button>
445
- <div class="status" id="orderStatus"></div>
416
+
417
+ <div class="card">
418
+ <span class="kicker">Действующие лицензии (.lic.json) и франшизы</span>
419
+ <h3 class="h3" style="margin:4px 0 6px">Выпущенные файлы лицензий</h3>
420
+ <div id="cabinetLicensesBox" class="small" style="margin-top:8px;line-height:1.55"></div>
446
421
  </div>
447
422
  </div>
448
423
  </section>
@@ -508,12 +483,8 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
508
483
  <div><span>Индекс экономики</span><b id="mapRegIndex">—</b></div>
509
484
  <div><span>Остаток (в течение года)</span><b id="mapRegRest">—</b></div>
510
485
  </div>
511
- <div class="field"><label for="inpPartnerCompany">Компания</label><input type="text" id="inpPartnerCompany" value="ООО «УралТехИнтеграция»"></div>
512
- <div class="row2">
513
- <div class="field"><label for="inpPartnerInn">ИНН</label><input type="text" id="inpPartnerInn" value="6670123456" inputmode="numeric"></div>
514
- <div class="field"><label for="inpPartnerEmail">Email</label><input type="email" id="inpPartnerEmail" value="partner@uraltech.ru"></div>
515
- </div>
516
- <button class="btn btn-primary" style="width:100%" id="btnReserveSelected" data-action="reserve-selected" disabled>Зарезервировать · залог ${rub(FRANCHISE_PROGRAM.reservationDepositRub)}</button>
486
+ <button class="btn btn-primary" style="width:100%;margin-top:8px" id="btnReserveSelected" data-action="reserve-selected" disabled>Зарезервировать · залог ${rub(FRANCHISE_PROGRAM.reservationDepositRub)}</button>
487
+ <p class="small muted" style="margin:8px 0 0;text-align:center">При нажатии «Зарезервировать» заявка на регион добавится как заказ в ваш Личный кабинет.</p>
517
488
  <div class="status" id="partnerActionMsg"></div>
518
489
  </aside>
519
490
  </div>
@@ -566,56 +537,6 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
566
537
  <div class="grid g4">${renderTracks()}</div>
567
538
  </section>
568
539
 
569
- <!-- ============ ПОДДЕРЖКА ============ -->
570
- <section class="stage" id="stage-support">
571
- <div class="sec-head">
572
- <div><span class="kicker">Сервис</span><h2 class="h2">Поддержка в мессенджерах MAX, Telegram и VK</h2></div>
573
- <p>Боты принимают обращения, показывают статус лицензии, выставляют счета и передают вопросы инженеру по SLA вашего тарифа.</p>
574
- </div>
575
- <div class="grid g2">
576
- <div class="card">
577
- <h3 class="h3">Консоль бота</h3>
578
- <div class="row2" style="margin-top:12px">
579
- <div class="field"><label for="botChannel">Канал</label><select id="botChannel"><option value="max">MAX</option><option value="telegram">Telegram</option><option value="vk">VK</option></select></div>
580
- <div class="field"><label for="botChatId">Chat ID</label><input type="text" id="botChatId" value="client-101"></div>
581
- </div>
582
- <div class="field"><label for="botText">Команда или текст</label>
583
- <div style="display:flex;gap:8px"><input type="text" id="botText" value="/start"><button class="btn btn-primary" data-action="bot-send">${icon('send')}</button></div>
584
- </div>
585
- <div class="chips" style="margin-bottom:12px">
586
- <button class="chip" data-bot="/status">/status</button>
587
- <button class="chip" data-bot="/buy business 10">/buy business 10</button>
588
- <button class="chip" data-bot="/help">/help</button>
589
- </div>
590
- <div class="console" id="botReplyBox">Ответ бота появится здесь…</div>
591
- </div>
592
- <div class="card">
593
- <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">
594
- <h3 class="h3" style="margin:0">Ваша лицензия и привязка мессенджеров</h3>
595
- <div style="display:flex;gap:8px;flex-wrap:wrap">
596
- <button class="btn btn-ghost btn-sm" data-action="auth">Личный кабинет</button>
597
- <button class="btn btn-ghost btn-sm" data-action="open-oferta">Договор-оферта</button>
598
- </div>
599
- </div>
600
- <div class="console" id="clientLicenseBox" style="margin-top:14px">Войдите в личный кабинет и подпишите договор-оферту, чтобы получить код привязки вашего комплекса к ботам MAX, Telegram и VK.</div>
601
- </div>
602
- </div>
603
- <div class="card">
604
- <div class="spec-wrap">
605
- <div>
606
- <span class="kicker">Разработчик платформы</span>
607
- <h2 class="h2">${esc(VENDOR_CONTACTS.company)}</h2>
608
- <p class="note">${esc(VENDOR_CONTACTS.description)}. Головной офис ODANT — в Рязани.</p>
609
- </div>
610
- <div>
611
- <div class="contact-row">${icon('pin')}<div><b>${esc(VENDOR_CONTACTS.address)}</b><br><a href="https://yandex.ru/maps/?pt=${VENDOR_CONTACTS.geo.lon},${VENDOR_CONTACTS.geo.lat}&z=17&l=map" target="_blank" rel="noopener">Открыть на карте</a></div></div>
612
- <div class="contact-row">${icon('phone')}<div>${VENDOR_CONTACTS.phones.map(p => `<a href="tel:${p.replace(/[^\d+]/g, '')}">${esc(p)}</a>`).join('<br>')}</div></div>
613
- <div class="contact-row">${icon('globe')}<div><a href="${esc(VENDOR_CONTACTS.site)}" target="_blank" rel="noopener">${esc(VENDOR_CONTACTS.site.replace(/^https?:\/\//, ''))}</a> · <a href="https://${esc(P.domain)}">${esc(P.domain)}</a></div></div>
614
- </div>
615
- </div>
616
- </div>
617
- </section>
618
-
619
540
  <footer>
620
541
  <div class="footer">
621
542
  <div>
@@ -642,39 +563,57 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
642
563
  </footer>
643
564
  </div>
644
565
 
645
- <!-- Регистрация / Вход в кабинет и подписание публичной оферты ПЭП -->
566
+ <!-- Вход / Регистрация в Личный кабинет (без ИНН, ФИО и ПЭП — по Email, через мессенджеры или OAuth) -->
646
567
  <div class="modal" id="authModal" role="dialog" aria-modal="true" aria-labelledby="authTitle">
647
- <div class="card" style="max-width:540px">
568
+ <div class="card" style="max-width:480px">
648
569
  <div class="modal-head">
649
570
  <img src="/assets/odant-logo.png" alt="">
650
571
  <div>
651
- <h3 class="h3" id="authTitle" style="margin:0">Регистрация и подписание оферты</h3>
652
- <div class="small muted" id="authSub">Заказ комплекса, докупка лицензий и резервирование франшизы доступны после регистрации и акцепта оферты (ПЭП)</div>
572
+ <h3 class="h3" id="authTitle" style="margin:0">Вход / Регистрация</h3>
573
+ <div class="small muted" id="authSub">Войдите в Личный кабинет по Email, через мессенджер или OAuth</div>
653
574
  </div>
654
575
  </div>
655
- <div class="row2">
656
- <div class="field"><label for="authEmail">Корпоративный email *</label><input type="email" id="authEmail" value="admin@maslotorg.ru" required></div>
657
- <div class="field"><label for="authInn">ИНН организации *</label><input type="text" id="authInn" value="7701234567" inputmode="numeric" required></div>
576
+
577
+ <div id="authIntentNotice" class="small" style="display:none;padding:10px 12px;border-radius:10px;background:rgba(0,163,224,.12);border:1px solid rgba(0,163,224,.35);margin-bottom:4px"></div>
578
+
579
+ <div class="field">
580
+ <label for="authEmail">Ваш Email</label>
581
+ <div style="display:flex;gap:8px">
582
+ <input type="email" id="authEmail" value="admin@maslotorg.ru" placeholder="name@company.ru" style="flex:1" required>
583
+ <button class="btn btn-primary" data-action="login">${icon('check')} Войти по Email</button>
584
+ </div>
658
585
  </div>
659
- <div class="row2">
660
- <div class="field"><label for="authCompany">Наименование организации *</label><input type="text" id="authCompany" value="ООО «Маслоторг»" required></div>
661
- <div class="field"><label for="authSigner">ФИО и должность подписанта *</label><input type="text" id="authSigner" value="Перепелкин Р.А., Генеральный директор" required></div>
586
+
587
+ <div class="field" style="margin-top:6px">
588
+ <label>Быстрый вход через мессенджеры</label>
589
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px">
590
+ <button class="btn btn-ghost btn-sm" data-action="oauth-login" data-provider="telegram">Telegram</button>
591
+ <button class="btn btn-ghost btn-sm" data-action="oauth-login" data-provider="max">MAX</button>
592
+ <button class="btn btn-ghost btn-sm" data-action="oauth-login" data-provider="vk">VK Мессенджер</button>
593
+ </div>
594
+ </div>
595
+
596
+ <div class="field">
597
+ <label>Вход через OAuth-провайдеров</label>
598
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px">
599
+ <button class="btn btn-ghost btn-sm" data-action="oauth-login" data-provider="yandex">Яндекс ID</button>
600
+ <button class="btn btn-ghost btn-sm" data-action="oauth-login" data-provider="vk_id">VK ID</button>
601
+ <button class="btn btn-ghost btn-sm" data-action="oauth-login" data-provider="google">Google</button>
602
+ </div>
662
603
  </div>
663
- <div class="field"><label for="authLicId">Номер лицензии ПАК (если уже выдан)</label><input type="text" id="authLicId" placeholder="Будет привязан или выпущен автоматически"></div>
664
604
 
665
- <div class="oferta-check-box">
605
+ <div class="oferta-check-box" style="margin-top:6px">
666
606
  <label class="oferta-check-label">
667
607
  <input type="checkbox" id="authOfertaCheck" checked>
668
608
  <span>
669
- Я ознакомился с условиями <a href="#" data-action="open-oferta">Публичного договора-оферты ODANT-WORK (${esc(PUBLIC_OFERTA.version)})</a> и подписываю его простой электронной подписью (ПЭП) от имени указанной организации.
609
+ Регистрируясь и входя в Личный кабинет, я принимаю условия <a href="#" data-action="open-oferta">Публичного договора-оферты ODANT-WORK (${esc(PUBLIC_OFERTA.version)})</a>.
670
610
  </span>
671
611
  </label>
672
612
  </div>
673
- <div class="status" id="authErrorMsg" style="color:var(--danger);min-height:18px;margin-top:6px"></div>
674
- <div style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap">
675
- <button class="btn btn-primary" style="flex:1" data-action="login">${icon('check')} Зарегистрироваться и подписать оферту</button>
676
- <button class="btn btn-ghost" id="btnLogoutModal" data-action="logout" style="display:none">Выйти</button>
677
- <button class="btn btn-ghost" data-action="auth-close">Закрыть</button>
613
+ <div class="status" id="authErrorMsg" style="color:var(--danger);min-height:18px;margin-top:4px"></div>
614
+ <div style="display:flex;gap:10px;margin-top:6px;justify-content:flex-end;flex-wrap:wrap">
615
+ <button class="btn btn-ghost btn-sm" id="btnLogoutModal" data-action="logout" style="display:none">Выйти из аккаунта</button>
616
+ <button class="btn btn-ghost btn-sm" data-action="auth-close">Закрыть</button>
678
617
  </div>
679
618
  </div>
680
619
  </div>
@@ -702,7 +641,7 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
702
641
  </div>
703
642
  </div>
704
643
  <div style="display:flex;gap:10px;margin-top:14px;justify-content:flex-end;flex-wrap:wrap">
705
- <button class="btn btn-primary" data-action="accept-from-oferta">${icon('check')} Принять условия и перейти к подписанию ПЭП</button>
644
+ <button class="btn btn-primary" data-action="accept-from-oferta">${icon('check')} Принять условия оферты</button>
706
645
  <button class="btn btn-ghost" data-action="oferta-close">Закрыть</button>
707
646
  </div>
708
647
  </div>
@@ -809,7 +748,12 @@ export function renderSandboxPaymentPage(order) {
809
748
  });
810
749
  const data = await res.json();
811
750
  document.getElementById('st').textContent = data.status || event;
812
- document.getElementById('msg').textContent = data.status === 'paid' ? 'Оплата проведена. Лицензия обновлена.' : 'Платёж отменён.';
751
+ document.getElementById('msg').innerHTML = data.status === 'paid'
752
+ ? 'Оплата успешно проведена! Файлы лицензий выпущены в вашем Личном кабинете.<br><button class="btn cancel" style="margin-top:10px" onclick="if(window.opener){window.opener.postMessage({type:\\'odant:order-paid\\'},\\'*\\');window.close();}else{location.href=\\'/#addons\\';}">Вернуться в Личный кабинет →</button>'
753
+ : 'Платёж отменён.';
754
+ if (data.status === 'paid' && window.opener) {
755
+ try { window.opener.postMessage({ type: 'odant:order-paid', orderId: ORDER.orderId }, '*'); } catch {}
756
+ }
813
757
  }
814
758
  </script>
815
759
  </body>
package/src/server.js CHANGED
@@ -327,7 +327,8 @@ export function createLicenseServerApp(options = {}) {
327
327
  const user = ensureCustomerOferta(req, body);
328
328
  const reserved = store.reserveFranchiseSlot({
329
329
  ...body,
330
- companyName: body.companyName || user?.companyName || '',
330
+ ownerAccountId: user?.userId || body.ownerAccountId || '',
331
+ companyName: body.companyName || user?.companyName || user?.email || '',
331
332
  inn: body.inn || user?.inn || '',
332
333
  email: body.email || user?.email || '',
333
334
  });
@@ -340,6 +341,90 @@ export function createLicenseServerApp(options = {}) {
340
341
  });
341
342
  }
342
343
 
344
+ // 2.6.1 Добавление позиции («Купить» или «Зарезервировать») как неоплаченного заказа в Личный кабинет
345
+ if (req.method === 'POST' && pathname === '/api/cabinet/orders/draft') {
346
+ const body = await readJsonBody(req);
347
+ const user = ensureCustomerOferta(req, body);
348
+ let order;
349
+ let reservation = null;
350
+ if (body.kind === 'franchise_reservation' || body.regionCode) {
351
+ const reserved = store.reserveFranchiseSlot({
352
+ regionCode: body.regionCode,
353
+ ownerAccountId: user?.userId || '',
354
+ companyName: body.companyName || user?.companyName || user?.email || 'Партнёр ODANT-WORK',
355
+ inn: body.inn || user?.inn || '',
356
+ email: body.email || user?.email || '',
357
+ });
358
+ order = reserved.order;
359
+ reservation = reserved.reservation;
360
+ }
361
+ else {
362
+ order = store.createOrder({
363
+ ...body,
364
+ licenseId: body.licenseId || user?.licenseId || '',
365
+ ownerAccountId: user?.userId || '',
366
+ customer: {
367
+ name: body.customer?.name || user?.companyName || user?.email || '',
368
+ inn: body.customer?.inn || user?.inn || '',
369
+ email: body.customer?.email || user?.email || '',
370
+ phone: body.customer?.phone || user?.phone || '',
371
+ },
372
+ });
373
+ }
374
+ const updatedUser = store.findUserBySessionKey(user?.sessionKey);
375
+ return sendJson(res, 200, {
376
+ ok: true,
377
+ order,
378
+ reservation,
379
+ user: updatedUser,
380
+ });
381
+ }
382
+
383
+ // 2.6.2 Оплата существующего заказа из Личного кабинета (через ЮKassa или по счёту-договору для юрлиц)
384
+ if (req.method === 'POST' && pathname === '/api/cabinet/orders/pay') {
385
+ const body = await readJsonBody(req);
386
+ const user = ensureCustomerOferta(req, body);
387
+ const order = store.prepareOrderPayment(body.orderId, {
388
+ paymentMethod: body.paymentMethod || 'yookassa',
389
+ companyName: body.companyName || user?.companyName || '',
390
+ inn: body.inn || user?.inn || '',
391
+ });
392
+ const updatedUser = store.findUserBySessionKey(user?.sessionKey);
393
+ if (order.paymentMethod === 'invoice') {
394
+ return sendJson(res, 200, {
395
+ ok: true,
396
+ orderId: order.orderId,
397
+ paymentMethod: 'invoice',
398
+ invoiceNumber: order.invoiceNumber,
399
+ contractNumber: order.contractNumber,
400
+ invoiceUrl: `${reqOrigin}${order.invoiceUrl}`,
401
+ confirmationUrl: `${reqOrigin}${order.invoiceUrl}`,
402
+ order,
403
+ user: updatedUser,
404
+ });
405
+ }
406
+ const payment = await yookassa.createPaymentForOrder(order, { originOverride: reqOrigin });
407
+ return sendJson(res, 200, {
408
+ ok: true,
409
+ ...payment,
410
+ order,
411
+ user: updatedUser,
412
+ });
413
+ }
414
+
415
+ // 2.6.3 Отмена (удаление) неоплаченного заказа из Личного кабинета
416
+ if (req.method === 'POST' && pathname === '/api/cabinet/orders/cancel') {
417
+ const body = await readJsonBody(req);
418
+ const user = ensureCustomerOferta(req, body);
419
+ store.cancelOrder(body.orderId);
420
+ const updatedUser = store.findUserBySessionKey(user?.sessionKey);
421
+ return sendJson(res, 200, {
422
+ ok: true,
423
+ orderId: body.orderId,
424
+ user: updatedUser,
425
+ });
426
+ }
427
+
343
428
  // 2.7 Собственный ИИ-агент лицензионного сервера ODANT-WORK
344
429
  if (req.method === 'POST' && pathname === '/api/agent/chat') {
345
430
  const body = await readJsonBody(req);