Add searchable user guide and support area with future assistant design
This commit is contained in:
parent
09bfb0fed8
commit
ff7fe90ccf
@ -11,7 +11,7 @@
|
|||||||
"serverReady": true,
|
"serverReady": true,
|
||||||
"workspaceAutoDiscovery": true,
|
"workspaceAutoDiscovery": true,
|
||||||
"invitesTemporarilyDisabled": false,
|
"invitesTemporarilyDisabled": false,
|
||||||
"pwaCache": "v102-20260918-safari-login",
|
"pwaCache": "v103-20260918-help-center",
|
||||||
"fullOfferDescriptions": true,
|
"fullOfferDescriptions": true,
|
||||||
"dynamicOfferRows": true,
|
"dynamicOfferRows": true,
|
||||||
"pdfOfferDescriptionFix": true,
|
"pdfOfferDescriptionFix": true,
|
||||||
|
|||||||
55
docs/support-assistant.md
Normal file
55
docs/support-assistant.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Руководство и будущий помощник Caterium
|
||||||
|
|
||||||
|
## Что работает сейчас
|
||||||
|
|
||||||
|
Встроенная «Помощь»: руководство, поиск по текстам/ключевым словам, категории, связанные инструкции и вкладка поддержки с частыми вопросами. На форме авторизации есть «Помощь со входом». Статьи находятся в `public/help/knowledge-v1.json`; содержимое не содержит реальных клиентов, цен компаний, телефонов, адресов, паролей или записей базы. Бот и отправка обращений не подключены. Вопросы поиска не сохраняются и не отправляются наружу. Руководство включено в кэш установленного приложения, но первое получение требует соединения.
|
||||||
|
|
||||||
|
## Как вести базу знаний
|
||||||
|
|
||||||
|
Стабильный id статьи, категория, заголовок, ключевые слова, последовательность шагов, ограничение/примечание и related. Обновлять version при правке текста. Проверять инструкцию в текущем интерфейсе и ролях, не описывать запланированную функцию как работающую. Эти же статьи использовать для поиска ботом с возвратом ссылок на конкретные article_id. Отдельно хранить закрытые инженерные инструкции: не индексировать дампы базы, исходные Telegram-архивы, логи с токенами и весь репозиторий в пользовательский поиск.
|
||||||
|
|
||||||
|
## Два режима будущего бота
|
||||||
|
|
||||||
|
1. Объяснение функций по опубликованной базе знаний: «Как сделать предложение?». Ответ с шагами и ссылками на статьи; если материала недостаточно — уточнить вопрос, не придумывать кнопки.
|
||||||
|
2. Объяснение данных своей компании: «Почему в этом заказе остаток?». Только после серверной авторизации и проверки прав. Подключать отдельными разрешёнными инструментами чтения. Не давать модели прямое SQL-соединение, service_role или доступ к произвольным таблицам.
|
||||||
|
|
||||||
|
Максимально полезный контекст означает полный и актуальный справочник плюс минимальные данные конкретного вопроса, а не загрузку всей клиентской базы в модель.
|
||||||
|
|
||||||
|
## Предлагаемый серверный контракт (ещё не реализован)
|
||||||
|
|
||||||
|
`POST /support/chat` с пользовательской сессией. Тело: conversation_id, message, locale, текущий раздел из фиксированного списка, необязательный выбранный order_id, версия клиента. Не доверять workspace_id, роли, суммам и правам из браузера: определить их по сессии, членству и серверным разрешениям. Идентификатор заказа проверять в пределах разрешённой компании. Отсутствие компании допускает только публичные инструкции.
|
||||||
|
|
||||||
|
Ответ: answer, citations[{article_id,knowledge_version}], data_as_of, request_id, needs_human. При выключенном помощнике вернуть явный статус unavailable, UI сохраняет доступ к руководству. Ключ провайдера только на сервере. Перед отправкой в модель отделить системные правила, найденные инструкции и недоверенные данные пользователя.
|
||||||
|
|
||||||
|
Разрешённые инструменты первой версии:
|
||||||
|
|
||||||
|
- search_help(query) — только опубликованные статьи.
|
||||||
|
- get_order_summary(order_id) — стоимость позиций, доставка, скидка, оплачено, остаток и статус; без телефона и адреса по умолчанию; orders.view плюс проверка строки/компании.
|
||||||
|
- get_order_recipe_requirements(order_id) — состав и рассчитанная потребность; права на заказ и соответствующий каталог/закупки.
|
||||||
|
- get_stock_shortages(order_id) — потребность и остатки; дополнительно stock.view/shopping, с учётом фактических имён прав в проекте при реализации.
|
||||||
|
- get_catalog_item(item_id) — доступные пользователю сведения о позиции; не раскрывать себестоимость роли без такого права.
|
||||||
|
- get_subscription_help() — разрешённые сведения о тарифе, без платёжных реквизитов.
|
||||||
|
|
||||||
|
Начальная версия только читает. Оплата заказа, списание склада, удаление, рассылки и изменения прав не выполняются ботом. Если позже появятся действия, нужен отдельный проект: предпросмотр последствий, конкретное подтверждение пользователя и обычная серверная проверка прав. Фраза внутри клиентского примечания не даёт разрешения выполнить команду.
|
||||||
|
|
||||||
|
## Защита и изоляция
|
||||||
|
|
||||||
|
- RLS/проверка прав на каждом инструменте, не только в общем chat endpoint. Пользовательский контекст вместо обхода RLS привилегированным ключом. Проверять актуальные права при каждом запросе.
|
||||||
|
- Ключ истории и кэша включает компанию и пользователя; авторизация на каждом чтении. При смене компании/выходе очищать UI и отменять запросы. Не показывать запоздалый ответ другой компании.
|
||||||
|
- Текст заказа, вложения, статьи от пользователя и ответы инструментов — данные, не системные инструкции. Не исполнять указания о раскрытии секретов и внешней отправке из этих полей. В первой версии не читать произвольные URL.
|
||||||
|
- Минимизировать данные до передачи провайдеру. Сначала выбрать провайдера, регион, договор обработки, режим хранения/обучения и правовое основание. Согласовать с политикой данных и фактической инфраструктурой; сейчас база Supabase во Франкфурте, вопрос локализации отмечен в юридическом пакете.
|
||||||
|
- Ограничить длину сообщения, число обращений, бюджет инструментов, таймаут и стоимость. Не писать пароли, access/refresh tokens и дампы в журналы.
|
||||||
|
- Аудит: request_id, пользователь/компания, версия базы знаний, названия вызванных инструментов, результат проверки прав; содержимое переписки и сроки хранения определить отдельно. Пользователь должен знать, что подключён AI и кому передаются сообщения.
|
||||||
|
- Ответы рендерить как безопасный текст/очищенный Markdown, не исполнять HTML и произвольные ссылки. При недоступности данных обозначать это, а не подставлять пример как факт.
|
||||||
|
|
||||||
|
## Передача человеку
|
||||||
|
|
||||||
|
Когда появится канал поддержки, дать пользователю проверить текст обращения и выбранные вложения до отправки. Историю переписки или данные заказа не пересылать автоматически. Подтверждать отправку только после ответа сервера. Пока контакты/канал не определены — никаких фиктивных «обращение отправлено».
|
||||||
|
|
||||||
|
## Проверки перед подключением
|
||||||
|
|
||||||
|
Набор типовых вопросов по каждой статье; правильные ссылки; отсутствие вымышленных функций; запрос чужого заказа; смена компании во время ответа; отзыв прав; prompt injection в примечании; просроченная сессия; пустые данные; известная/неизвестная цена; повторы и лимиты; падение провайдера; мобильные Safari/Chrome; удобное закрытие и клавиатура. Не подключать бота к production до прохождения проверок изоляции и согласования передачи данных.
|
||||||
|
|
||||||
|
## Решения, оставшиеся владельцу
|
||||||
|
|
||||||
|
Провайдер и бюджет, собственный бот или API модели, канал передачи человеку, контакт поддержки, допустимые категории данных, срок хранения диалогов и необходимость режима ответов по данным компании. Текущая реализация не требует этих решений и уже полезна как руководство.
|
||||||
@ -4,7 +4,7 @@
|
|||||||
"version": "17.7.3",
|
"version": "17.7.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js && node --check public/core/trial-demo.js && node --check public/core/proposal-layout.js && node --check public/core/mobile-order.js",
|
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js && node --check public/core/trial-demo.js && node --check public/core/proposal-layout.js && node --check public/core/mobile-order.js && node --check public/core/help-center.js",
|
||||||
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs",
|
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs",
|
||||||
"check:release": "node tests/release-check.mjs",
|
"check:release": "node tests/release-check.mjs",
|
||||||
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db",
|
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db",
|
||||||
|
|||||||
35
public/core/help-center.css
Normal file
35
public/core/help-center.css
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#ctHelpDialog{box-sizing:border-box;width:min(980px,calc(100vw - 24px));max-width:calc(100vw - 24px);height:min(820px,calc(100dvh - 24px));max-height:calc(100dvh - 24px);padding:0;border:1px solid #d9ddda;border-radius:20px;background:#fafaf7;color:#263d31;font:15px/1.55 Arial,sans-serif;overflow:hidden;margin:auto}
|
||||||
|
#ctHelpDialog::backdrop{background:#152c27a6}
|
||||||
|
#ctHelpDialog[open]{display:flex;flex-direction:column}
|
||||||
|
#ctHelpDialog *{box-sizing:border-box}
|
||||||
|
#ctHelpDialog [hidden]{display:none!important}
|
||||||
|
#ctHelpDialog .ct-help-header{position:static!important;display:flex!important;align-items:center;justify-content:space-between;gap:12px;padding:20px 24px;background:#f1f4ed;border-bottom:1px solid #d9ddda;flex:none;min-height:0;width:auto;box-shadow:none}
|
||||||
|
#ctHelpDialog h2{margin:0;font-size:24px;color:#263d31}
|
||||||
|
#ctHelpDialog h3{font-size:19px;color:#263d31;margin:18px 0 10px}
|
||||||
|
#ctHelpDialog p{margin:8px 0 16px}
|
||||||
|
#ctHelpDialog #ctHelpEdition{margin:2px 0 0;font-size:12px;color:#617166}
|
||||||
|
#ctHelpDialog button{font:inherit;white-space:normal;border:1px solid #cdd5cb;border-radius:10px;background:white;color:#263d31;padding:10px 14px;min-height:44px;cursor:pointer;box-shadow:none;letter-spacing:normal;text-transform:none}
|
||||||
|
#ctHelpDialog button:focus-visible,#ctHelpDialog input:focus-visible,#ctHelpDialog select:focus-visible,#ctHelpDialog summary:focus-visible{outline:3px solid #b39b47;outline-offset:2px}
|
||||||
|
#ctHelpDialog #ctHelpClose{width:44px;flex:none;padding:0;font-size:28px}
|
||||||
|
#ctHelpDialog .ct-help-modes{display:flex;gap:8px;padding:12px 24px;border-bottom:1px solid #d9ddda;flex:none}
|
||||||
|
#ctHelpDialog .ct-help-modes button[aria-pressed=true]{background:#304f3d;color:#fff;border-color:#304f3d}
|
||||||
|
#ctHelpDialog .ct-help-content{padding:20px 24px;overflow-y:auto;overscroll-behavior:contain;min-height:0;flex:1}
|
||||||
|
#ctHelpDialog section{display:block;padding:0;margin:0;max-width:none;background:none}
|
||||||
|
#ctHelpDialog .ct-help-filters{display:grid;grid-template-columns:2fr 1fr;gap:12px}
|
||||||
|
#ctHelpDialog label{display:flex;flex-direction:column;gap:6px;font-size:13px;color:#425447}
|
||||||
|
#ctHelpDialog input,#ctHelpDialog select{width:100%;min-width:0;height:46px;padding:10px;border:1px solid #cdd5cb;border-radius:10px;background:white;color:#263d31;font:16px Arial,sans-serif}
|
||||||
|
#ctHelpDialog #ctHelpReset{justify-self:start}
|
||||||
|
#ctHelpDialog #ctHelpStatus{color:#617166;font-size:13px}
|
||||||
|
#ctHelpDialog details{border:1px solid #dce1d9;border-radius:12px;margin:10px 0;background:white;overflow-wrap:anywhere}
|
||||||
|
#ctHelpDialog summary{padding:15px;cursor:pointer;font-weight:700;min-height:48px}
|
||||||
|
#ctHelpDialog summary small{display:block;font-weight:400;font-size:12px;color:#6e7a6f;margin:3px 0 0 17px}
|
||||||
|
#ctHelpDialog ol{padding:0 22px 0 40px;margin:0 0 16px}
|
||||||
|
#ctHelpDialog li{margin:10px 0}
|
||||||
|
#ctHelpDialog .ct-help-note{background:#f7f4e8;border-left:3px solid #c5b366;padding:12px;margin:14px 16px;font-size:14px}
|
||||||
|
#ctHelpDialog .ct-help-related,#ctHelpDialog .ct-help-prompts{display:flex;flex-wrap:wrap;gap:8px;margin:16px}
|
||||||
|
#ctHelpDialog .ct-help-prompts{margin:18px 0}
|
||||||
|
#ctHelpDialog .ct-help-related button{font-size:13px}
|
||||||
|
#ctHelpDialog .ct-help-badge{display:inline-block;padding:5px 10px;border-radius:20px;background:#ede9d5;color:#635629;font-size:12px}
|
||||||
|
.ct-help-login{display:block!important;margin:16px auto 0!important;background:transparent!important;border:0!important;color:#5b655b!important;text-decoration:underline;min-height:44px;font-size:14px!important;cursor:pointer}
|
||||||
|
@media(max-width:600px){#ctHelpDialog{border-radius:14px}#ctHelpDialog h2{font-size:20px}#ctHelpDialog .ct-help-header,#ctHelpDialog .ct-help-content{padding:16px}#ctHelpDialog .ct-help-modes{padding:10px 16px}#ctHelpDialog .ct-help-filters{grid-template-columns:1fr}#ctHelpDialog .ct-help-prompts{flex-direction:column}}
|
||||||
|
@media print{#ctHelpDialog{display:none!important}}
|
||||||
62
public/core/help-center.js
Normal file
62
public/core/help-center.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
(()=>{
|
||||||
|
'use strict';
|
||||||
|
const version=new URL(document.currentScript.src).searchParams.get('v')||'1';
|
||||||
|
const url=new URL('../help/knowledge-v1.json',document.currentScript.src);url.searchParams.set('v',version);
|
||||||
|
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
const norm=s=>String(s).toLowerCase().replace(/ё/g,'е');
|
||||||
|
let dialog,data,loading=false,returnFocus;
|
||||||
|
function render(){
|
||||||
|
if(!data||!dialog)return;
|
||||||
|
const query=norm(dialog.querySelector('#ctHelpSearch').value).trim(),category=dialog.querySelector('#ctHelpCategory').value;
|
||||||
|
const terms=query.split(/\s+/).filter(Boolean);
|
||||||
|
const rows=data.articles.filter(a=>!category||a.category===category).map(a=>({a,score:terms.reduce((n,t)=>n+(norm(a.title+' '+a.keywords).includes(t)?3:0)+(norm(a.steps.join(' ')+' '+a.note).includes(t)?1:0),0)})).filter(r=>!terms.length||r.score>0).sort((a,b)=>b.score-a.score);
|
||||||
|
dialog.querySelector('#ctHelpStatus').textContent=rows.length?`Инструкций: ${rows.length}`:'Ничего не найдено. Попробуйте «оплата», «PDF», «склад» или откройте все темы.';
|
||||||
|
dialog.querySelector('#ctHelpResults').innerHTML=rows.map(({a})=>`<details data-article="${esc(a.id)}"><summary>${esc(a.title)}<small>${esc(a.category)}</small></summary><ol>${a.steps.map(s=>`<li>${esc(s)}</li>`).join('')}</ol><p class="ct-help-note">${esc(a.note)}</p><div class="ct-help-related">${(a.related||[]).map(id=>{const r=data.articles.find(x=>x.id===id);return r?`<button type="button" data-help-article="${esc(id)}">${esc(r.title)}</button>`:''}).join('')}</div></details>`).join('');
|
||||||
|
}
|
||||||
|
function showGuide(query=''){
|
||||||
|
switchMode(false);dialog.querySelector('#ctHelpSearch').value=query;dialog.querySelector('#ctHelpCategory').value='';render();dialog.querySelector('#ctHelpSearch').focus();
|
||||||
|
}
|
||||||
|
function switchMode(support){
|
||||||
|
dialog.querySelector('#ctHelpGuide').hidden=support;dialog.querySelector('#ctHelpSupport').hidden=!support;
|
||||||
|
dialog.querySelector('#ctHelpGuideTab').setAttribute('aria-pressed',String(!support));dialog.querySelector('#ctHelpSupportTab').setAttribute('aria-pressed',String(support));
|
||||||
|
}
|
||||||
|
async function load(){
|
||||||
|
if(data){render();return;}if(loading)return;loading=true;
|
||||||
|
const status=dialog.querySelector('#ctHelpStatus'),retry=dialog.querySelector('#ctHelpRetry');status.textContent='Загружаю руководство…';retry.hidden=true;
|
||||||
|
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),8000);
|
||||||
|
try{
|
||||||
|
const r=await fetch(url,{signal:controller.signal,credentials:'omit'});if(!r.ok)throw new Error('http');const value=await r.json();
|
||||||
|
if(!Array.isArray(value.articles)||!value.articles.every(a=>typeof a.id==='string'&&typeof a.title==='string'&&typeof a.category==='string'&&Array.isArray(a.steps)&&a.steps.every(s=>typeof s==='string')))throw new Error('format');
|
||||||
|
data=value;dialog.querySelector('#ctHelpCategory').innerHTML='<option value="">Все темы</option>'+[...new Set(data.articles.map(a=>a.category))].map(c=>`<option>${esc(c)}</option>`).join('');
|
||||||
|
dialog.querySelector('#ctHelpEdition').textContent=`Руководство · редакция ${data.version}`;render();
|
||||||
|
}catch(_){status.textContent='Не удалось загрузить руководство. Проверьте соединение и повторите.';retry.hidden=false;}finally{loading=false;clearTimeout(timer);}
|
||||||
|
}
|
||||||
|
function ensureDialog(){
|
||||||
|
if(dialog)return;
|
||||||
|
dialog=document.createElement('dialog');dialog.id='ctHelpDialog';dialog.setAttribute('aria-labelledby','ctHelpTitle');
|
||||||
|
dialog.innerHTML=`<div class="ct-help-header"><div><h2 id="ctHelpTitle">Помощь в Caterium</h2><p id="ctHelpEdition">Руководство пользователя и поддержка</p></div><button type="button" id="ctHelpClose" aria-label="Закрыть помощь">×</button></div><div class="ct-help-modes" aria-label="Раздел помощи"><button type="button" id="ctHelpGuideTab" aria-pressed="true">Руководство</button><button type="button" id="ctHelpSupportTab" aria-pressed="false">Поддержка</button></div><div class="ct-help-content"><section id="ctHelpGuide"><div class="ct-help-filters"><label>Что хотите сделать?<input id="ctHelpSearch" type="search" placeholder="Например: оплата, PDF, склад" maxlength="240" autocomplete="off"></label><label>Категория<select id="ctHelpCategory"><option value="">Все темы</option></select></label><button type="button" id="ctHelpReset">Все инструкции</button></div><p id="ctHelpStatus" role="status"></p><button type="button" id="ctHelpRetry" hidden>Повторить загрузку</button><div id="ctHelpResults"></div></section><section id="ctHelpSupport" hidden><span class="ct-help-badge">Помощник готовится</span><h3>Ответы по работе с приложением</h3><p>Бот и канал обращений пока не подключены. Сейчас можно найти готовую инструкцию. Поиск работает внутри руководства и не отправляет вопросы или данные вашей компании внешнему сервису.</p><div class="ct-help-prompts"><button type="button" data-help-query="создать заказ">Как создать заказ?</button><button type="button" data-help-query="оплата">Как отметить оплату?</button><button type="button" data-help-query="закупки">Как рассчитать закупки?</button><button type="button" data-help-query="синхронизация">Не загружается база</button></div><h3>Если инструкция не помогла</h3><p>Подготовьте название раздела, последовательность действий, точный текст ошибки, устройство и браузер. На снимке экрана закройте лишние телефоны и адреса. Не передавайте пароли и коды подтверждения.</p><button type="button" data-help-query="обращение">Памятка для обращения</button></section></div>`;
|
||||||
|
document.body.appendChild(dialog);
|
||||||
|
dialog.querySelector('#ctHelpClose').onclick=()=>dialog.close();
|
||||||
|
dialog.addEventListener('keydown',e=>{if(e.key==='Escape'){e.preventDefault();e.stopPropagation();dialog.close();}});
|
||||||
|
dialog.addEventListener('close',()=>{dialog.querySelector('#ctHelpSearch').value='';if(returnFocus?.isConnected)returnFocus.focus({preventScroll:true});});
|
||||||
|
dialog.querySelector('#ctHelpGuideTab').onclick=()=>switchMode(false);dialog.querySelector('#ctHelpSupportTab').onclick=()=>switchMode(true);
|
||||||
|
dialog.querySelector('#ctHelpSearch').oninput=render;dialog.querySelector('#ctHelpCategory').onchange=render;
|
||||||
|
dialog.querySelector('#ctHelpReset').onclick=()=>showGuide();dialog.querySelector('#ctHelpRetry').onclick=load;
|
||||||
|
dialog.addEventListener('click',e=>{
|
||||||
|
const q=e.target.closest('[data-help-query]');if(q){showGuide(q.dataset.helpQuery);return;}
|
||||||
|
const a=e.target.closest('[data-help-article]');if(a&&data){showGuide();const target=[...dialog.querySelectorAll('details')].find(x=>x.dataset.article===a.dataset.helpArticle);if(target){target.open=true;target.scrollIntoView({block:'nearest'});target.querySelector('summary').focus();}}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function open(trigger){ensureDialog();returnFocus=trigger?.currentTarget||trigger||document.activeElement;switchMode(false);if(!dialog.open)dialog.showModal();load();dialog.querySelector('#ctHelpSearch').focus();}
|
||||||
|
function mount(){
|
||||||
|
const nav=document.querySelector('header nav');
|
||||||
|
if(nav&&!document.getElementById('ctHelpNav')){const b=document.createElement('button');b.id='ctHelpNav';b.type='button';b.dataset.navLabel='Помощь';b.textContent='Помощь';b.onclick=open;nav.appendChild(b);window.sunRegisterNavButton?.({button:b,group:'management',navLabel:'Помощь'});}
|
||||||
|
const gate=document.getElementById('sunCloudAuthGateV3');
|
||||||
|
if(gate&&!gate.querySelector('.ct-help-login')){const b=document.createElement('button');b.type='button';b.className='ct-help-login';b.textContent='Помощь со входом';b.onclick=()=>{open(b);showGuide('вход');};(gate.querySelector('.sun-cloud-auth-card')||gate).appendChild(b);}
|
||||||
|
}
|
||||||
|
function boot(){mount();let frame=0;new MutationObserver(()=>{if(frame)return;frame=requestAnimationFrame(()=>{frame=0;mount();});}).observe(document.body,{childList:true,subtree:true});
|
||||||
|
const context=()=>`${window.SunCloudV2?.getSession?.()?.user?.id||''}:${window.SunCloudV2?.getWorkspace?.()?.id||''}`;
|
||||||
|
let current=context();window.addEventListener('sun:cloud-permissions-changed',()=>{const next=context();if(next!==current&&dialog?.open)dialog.close();current=next;});
|
||||||
|
}
|
||||||
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||||||
|
})();
|
||||||
@ -1,7 +1,7 @@
|
|||||||
(()=>{
|
(()=>{
|
||||||
'use strict';
|
'use strict';
|
||||||
const VERSION='17.7.3';
|
const VERSION='17.7.3';
|
||||||
const RELEASE='20260918-safari-login';
|
const RELEASE='20260918-help-center';
|
||||||
|
|
||||||
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
||||||
function installAuthBoot(){
|
function installAuthBoot(){
|
||||||
|
|||||||
36
public/help/knowledge-v1.json
Normal file
36
public/help/knowledge-v1.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"version":"2026-09-18.1",
|
||||||
|
"product":"Caterium",
|
||||||
|
"language":"ru",
|
||||||
|
"articles":[
|
||||||
|
{"id":"start","category":"Начало работы","title":"С чего начать работу в Caterium","keywords":"первый запуск обучение инструкция настройка","steps":["Войдите под своим email и паролем. Сотрудники используют личные учётные записи с доступом к компании.","В настройках заполните реквизиты и логотип своей компании для документов.","Добавьте позиции в меню, укажите цены и составы. Для расчёта закупок заполните ТТК.","Проверьте продукты, единицы измерения, поставщиков и остатки на складе.","Создайте заказ, добавьте позиции и данные клиента, сохраните его. Затем подготовьте предложение или откройте закупки."],"note":"Доступные разделы зависят от тарифа и роли сотрудника.","related":["trial","menu","order"]},
|
||||||
|
{"id":"login","category":"Начало работы","title":"Вход и подтверждение почты","keywords":"авторизация регистрация email not confirmed письмо пароль","steps":["Введите email и пароль, использованные при регистрации.","Если требуется подтверждение email, откройте самое новое письмо и перейдите по ссылке подтверждения.","После подтверждения вернитесь в приложение и войдите. При задержке письма проверьте папку «Спам».","При ошибке запишите её точный текст. Не отправляйте никому пароль или ссылку из письма подтверждения."],"note":"Подтверждение email и доступ к рабочей компании — разные проверки. Если компании нет, владельцу нужно проверить приглашение или условия регистрации по промокоду.","related":["iphone","permissions","sync"]},
|
||||||
|
{"id":"iphone","category":"Решение проблем","title":"Не получается войти на iPhone","keywords":"safari readable stream uploading is not supported айфон телефон вход","steps":["Проверьте соединение и откройте app.caterium.ru в Safari.","При сообщении «ReadableStream uploading is not supported» обновите страницу: эта ошибка исправлена в новой версии Caterium.","Если приложение открывается значком с главного экрана, полностью закройте его и запустите снова.","Если ошибка остаётся, запишите версию iOS, способ запуска и точный текст сообщения для поддержки."],"note":"Не удаляйте данные сайта, если на устройстве есть несинхронизированные изменения.","related":["login","sync","support"]},
|
||||||
|
{"id":"trial","category":"Начало работы","title":"Как проверить приложение на демонстрационных данных","keywords":"демо тест пробная версия промокод боксы тестовый","steps":["В пробной компании доступны демонстрационные позиции, если демобаза была установлена.","На экране нового заказа откройте «Технологические карты боксов и блюд» и посмотрите составы.","Кнопка «Создать пробный заказ» создаёт учебный заказ в вашей рабочей компании. Используйте её осознанно.","По пробному заказу проверьте потребность в закупках, затем поступление и списание продуктов на складе."],"note":"Демоданные — учебные. Перед реальной работой проверьте рецептуры, цены, выход и поставщиков. Существующие позиции компании не должны заменяться демобазой.","related":["ttk","purchases","stock"]},
|
||||||
|
{"id":"order","category":"Заказы и клиенты","title":"Как создать и сохранить заказ","keywords":"новый заказ добавить бокс количество мероприятие","steps":["Откройте «Новый заказ» и выберите мероприятие, если оно нужно.","Нажимайте на боксы или блюда в каталоге, затем проверьте количество и состав заказа.","На вкладке «Детали» заполните имя, телефон, дату, время и адрес доставки.","Укажите доставку, предоплату и скидку при необходимости. Проверьте итог и остаток.","Нажмите «Сохранить». Сохранённый заказ доступен в разделе «Заказы»."],"note":"На телефоне поля заказа расположены над каталогом. При прокрутке кнопка «↑ Наверх» возвращает к ним.","related":["payment","delivery","proposal"]},
|
||||||
|
{"id":"edit-order","category":"Заказы и клиенты","title":"Как изменить существующий заказ","keywords":"редактировать исправить изменить количество стоимость","steps":["Найдите заказ в разделе «Заказы» и откройте изменение.","Исправьте нужные поля, позиции или количество. Проверьте сумму, доставку и предоплату.","Сохраните заказ. Если предложение клиенту было сформировано раньше, обновите его и проверьте итоговый документ."],"note":"Не создавайте новый заказ только для исправления существующего: это может привести к дублированию.","related":["order","proposal","sync"]},
|
||||||
|
{"id":"payment","category":"Заказы и клиенты","title":"Что означают цвета оплаты заказов","keywords":"синий зеленый красный оплачено долг погасить остаток предоплата","steps":["В стандартной цветовой схеме красный означает отсутствие предоплаты, зелёный — частичную оплату, синий — полную оплату.","Для фиксации поступившей оплаты откройте заказ и проверьте значение «Предоплата»: оно учитывается как оплаченная сумма.","Если оплачена вся сумма заказа с доставкой, укажите полную полученную сумму и сохраните. Остаток должен стать нулевым.","При наличии специальной отметки об отмене заказ отображается отдельным цветом. Цвета оплаты можно изменить в настройках."],"note":"Отметка оплаты в Caterium не проводит банковский платёж и не подтверждает его сама по себе. Указывайте только фактически полученную сумму. Статус выполнения и оплата — разные сведения.","related":["delivery","import","money"]},
|
||||||
|
{"id":"delivery","category":"Заказы и клиенты","title":"Доставка, скидка и остаток к оплате","keywords":"итого сумма промокод расчет адрес доставка","steps":["Внесите стоимость доставки в отдельное поле заказа.","При скидке проверьте её тип: проценты или рубли. Промокод выбирается отдельно, если доступен.","Сверьте стоимость позиций, скидки, доставку и итог.","Внесите полученную оплату. Остаток рассчитывается из итоговой стоимости и оплаченной суммы."],"note":"Не дублируйте одну и ту же доставку одновременно отдельной строкой каталога и полем доставки, если это не два разных начисления.","related":["payment","order"]},
|
||||||
|
{"id":"clients","category":"Заказы и клиенты","title":"Клиенты, телефоны и адреса","keywords":"клиент база контакт карточка телефон адрес история","steps":["Откройте раздел «Клиенты» и найдите нужную карточку.","Перед добавлением нового клиента проверьте телефон: одинаковые имена могут относиться к разным людям.","Проверьте адрес непосредственно в заказе: у одного клиента может быть несколько адресов доставки.","Вносите только сведения, необходимые для работы, и предоставляйте доступ к ним только уполномоченным сотрудникам."],"note":"Телефон и адрес из заказа не означают согласия человека на рекламную рассылку.","related":["order","mailings","permissions"]},
|
||||||
|
{"id":"import","category":"Заказы и клиенты","title":"Заказы, перенесённые из Telegram","keywords":"импорт архив старые заказы цены совпадение telegram","steps":["В разделе «Заказы» откройте «Исходный архив Telegram», если для вашей компании был выполнен импорт.","Сверяйте спорные сведения с исходными сообщениями и фотографиями.","Если позиция не совпала с каталогом, она может быть сохранена под исходным названием и с исторической ценой.","Отсутствующие в источнике цена, телефон или адрес требуют уточнения. Нулевую неизвестную цену нельзя автоматически считать бесплатным заказом."],"note":"Архив и импортированные заказы доступны только соответствующей компании и пользователям с необходимыми правами.","related":["payment","clients"]},
|
||||||
|
{"id":"menu","category":"Меню и ТТК","title":"Боксы, премиум, напитки и посуда","keywords":"каталог позиции название фото цена меню премиум посуда напитки","steps":["Откройте «Меню» и выберите нужную категорию.","Добавьте или откройте позицию, заполните название, цену, фотографию и описание.","Для блюд проверьте вес, количество штук и состав. Для напитков и посуды используйте подходящие единицы.","Сохраните позицию и проверьте её отображение в каталоге нового заказа."],"note":"Каталог принадлежит рабочей компании. Отсутствие возможности редактировать может быть связано с ролью сотрудника.","related":["ttk","banquet","permissions"]},
|
||||||
|
{"id":"ttk","category":"Меню и ТТК","title":"Зачем нужна технологическая карта","keywords":"ттк рецепт ингредиент состав брутто нетто вес себестоимость","steps":["В редакторе позиции заполните «Состав / ТТК»: продукты, количества и единицы измерения.","Убедитесь, что количества относятся к одной продаваемой единице — одному боксу или одной порции.","Проверьте соответствие названий продуктов складу и единицы: граммы, килограммы, литры и штуки нельзя смешивать без пересчёта.","Создайте небольшой пробный заказ и сверьте потребность в продуктах в закупках."],"note":"Описание блюда для клиента и складской состав решают разные задачи. Одна фотография или текстовое описание не создаёт корректного расчёта продуктов.","related":["stock","purchases","banquet"]},
|
||||||
|
{"id":"banquet","category":"Меню и ТТК","title":"Как работать с банкетным меню","keywords":"банкет салаты холодные горячие закуски гарниры десерты порции гости","steps":["В каталоге заказа выберите «Банкетное меню».","Используйте категории блюд, чтобы найти салаты, закуски, горячие блюда и другие позиции.","Перед добавлением проверьте цену и вес одной порции. Укажите требуемое количество порций.","Проверьте итог по заказу: количество гостей и количество порций не всегда совпадают."],"note":"Демонстрационные веса и цены являются учебными и требуют проверки перед коммерческим предложением.","related":["ttk","proposal","order"]},
|
||||||
|
{"id":"stock","category":"Склад и закупки","title":"Остатки и движения склада","keywords":"склад приход поступление списание продукты остатки единицы","steps":["Откройте «Склад» и проверьте список продуктов и единицы измерения.","Оформляйте поступление при фактическом получении продуктов, проверяя количество и цену.","Списание отражает расход продуктов. Перед его выполнением проверьте заказ и рассчитанный состав.","Если остаток выглядит неверно, сначала проверьте историю движений и единицы, а не добавляйте компенсирующий приход наугад."],"note":"Один и тот же расход не следует списывать повторно. Доступ к складским операциям зависит от роли.","related":["purchases","ttk","suppliers"]},
|
||||||
|
{"id":"purchases","category":"Склад и закупки","title":"Как проверить, что нужно закупить","keywords":"закупка нехватка потребность список поставщик ингредиенты","steps":["Убедитесь, что заказы сохранены, а их позиции содержат корректные ТТК.","Откройте «Закупки» и выберите нужные заказы или период доступными фильтрами.","Сверьте потребность с остатками склада и проверьте единицы измерения.","После покупки отдельно оформите фактическое поступление на склад."],"note":"Список закупок сам по себе не означает, что товар уже получен. Если продукт отсутствует в расчёте, проверьте его наличие в ТТК.","related":["ttk","stock","suppliers"]},
|
||||||
|
{"id":"suppliers","category":"Склад и закупки","title":"Поставщики и продукты","keywords":"поставщик закупочная цена контакт продукт","steps":["Откройте «Поставщики» и проверьте сведения о контрагенте.","Используйте актуальные наименования и контакты при подготовке закупки.","Учебные поставщики из демобазы замените реальными перед работой."],"note":"Изменение справочника не подтверждает факт покупки или оплаты поставщику.","related":["purchases","stock"]},
|
||||||
|
{"id":"proposal","category":"Документы","title":"Предложение клиенту и PDF","keywords":"кп коммерческое предложение pdf скачать печать шаблон оформление документ","steps":["Сохраните заказ с правильным составом, количеством, ценами и доставкой.","Нажмите «Предложение клиенту». Дождитесь подготовки просмотра.","Проверьте шаблон, данные компании, фотографии и итоговые суммы. Используйте редактирование и обновление при необходимости.","Перед отправкой откройте итоговый PDF и проверьте все страницы."],"note":"При медленной загрузке фотографий предложение может открыться без части изображений. После восстановления связи обновите просмотр. Отправка документа клиенту выполняется вами.","related":["branding","pdf-problem","edit-order"]},
|
||||||
|
{"id":"branding","category":"Документы","title":"Свой логотип и реквизиты в документах","keywords":"логотип компания бренд реквизиты солнце caterium название","steps":["В настройках найдите данные компании и оформление документов.","Укажите название и реквизиты своей компании, загрузите собственный логотип.","Сформируйте предложение или бланк и проверьте результат."],"note":"Название продукта в панели навигации и бренд компании в документах — разные настройки. Специальное оформление панели назначается отдельно.","related":["proposal","permissions"]},
|
||||||
|
{"id":"pdf-problem","category":"Решение проблем","title":"Предложение долго открывается или не хватает фотографий","keywords":"pdf висит готовлю просмотр обновляю предложение фото изображения загрузка","steps":["Проверьте соединение и дождитесь завершения подготовки документа.","Если просмотр не появился, закройте его и откройте снова из сохранённого заказа.","Если не загрузились фотографии, используйте «Обновить» в предложении после восстановления связи.","При повторении запишите шаблон, номер заказа и точный текст ошибки. Не отправляйте клиентскую базу целиком."],"note":"Перед отправкой клиенту всегда проверяйте итоговый PDF, а не только экран редактирования.","related":["proposal","sync","support"]},
|
||||||
|
{"id":"calendar","category":"Планирование","title":"Календарь и печать событий","keywords":"календарь дата день девять 9 заказов печать","steps":["Проверьте дату и время в сохранённых заказах.","Откройте «Календарь» и выберите нужный месяц.","В насыщенные дни заказы показываются компактными строками; перед печатью проверьте, что все нужные события видны.","Просмотрите страницы в окне печати и настройте размер бумаги и ориентацию."],"note":"Печатный календарь отражает состояние заказов на момент формирования. После изменений сформируйте его заново.","related":["order","routes"]},
|
||||||
|
{"id":"routes","category":"Планирование","title":"Маршруты доставки","keywords":"маршрут карта курьер адрес доставка поездка","steps":["Заполните полный адрес и время в заказах.","Откройте «Маршруты» или «Карта» и проверьте выбранные доставки.","Перед выездом сверяйте адрес, контакты, время и комментарий курьеру с заказом."],"note":"Карта не заменяет проверку подъезда, пропуска и условий разгрузки. Уточняйте их у клиента.","related":["delivery","clients","calendar"]},
|
||||||
|
{"id":"permissions","category":"Компания и доступ","title":"Сотрудники, роли и недоступные разделы","keywords":"команда права сотрудник приглашение доступ нет кнопки владелец администратор","steps":["Владелец управляет сотрудниками через «Настройки» → «Пользователи и права».","Назначайте каждому сотруднику только необходимые права.","Если раздел или действие недоступны, проверьте роль и тариф компании.","Не передавайте пароль владельца сотрудникам. Для каждого нужна отдельная учётная запись."],"note":"Поддержка и будущий помощник не должны предоставлять данные в обход прав пользователя.","related":["login","subscription","chat"]},
|
||||||
|
{"id":"subscription","category":"Компания и доступ","title":"Где посмотреть тариф и пробный период","keywords":"подписка тариф оплата продление пробный срок лимиты","steps":["Откройте «Настройки» → «Тариф и подписка».","Посмотрите активный тариф, статус, срок доступа и лимиты сотрудников.","При недоступной функции проверьте условия тарифа и роль пользователя."],"note":"Условия будущих автосписаний и лицензионное соглашение находятся в разработке. Этот справочник не подтверждает подключение банковской оплаты или автоматического продления.","related":["trial","permissions"]},
|
||||||
|
{"id":"sync","category":"Решение проблем","title":"Ошибка синхронизации или долгая загрузка базы","keywords":"aborterror no content provided сеть сервер облако синхронизация база потеря","steps":["Проверьте интернет и точный текст ошибки. Обновите приложение до последней версии.","Если есть несинхронизированные изменения, не очищайте данные сайта и не переустанавливайте приложение.","В настройках доступна выгрузка резервной копии базы в файл — сохраните её перед действиями, которые могут заменить локальные данные.","«Загрузить из облака» заменяет локальную копию серверной. Используйте это действие только после понимания, где находятся последние изменения."],"note":"Не повторяйте создание одного заказа много раз из-за задержки ответа. Сначала проверьте, появился ли он в списке.","related":["backup","iphone","support"]},
|
||||||
|
{"id":"backup","category":"Компания и доступ","title":"Резервная копия и восстановление","keywords":"скачать базу файл резервная копия восстановить импорт экспорт","steps":["В настройках найдите «Резервная копия базы (файл)».","Нажмите «Скачать базу в файл» и сохраните файл в защищённом месте.","Перед загрузкой базы из файла проверьте компанию, содержимое и актуальность копии. Сначала сохраните текущую базу.","Не отправляйте резервную копию в открытые чаты: она может содержать данные клиентов."],"note":"Резервная копия другой компании не предназначена для загрузки в текущую рабочую базу.","related":["sync","permissions"]},
|
||||||
|
{"id":"money","category":"Заказы и клиенты","title":"Деньги и статистика","keywords":"деньги статистика выручка отчет суммы доход","steps":["Выберите нужный период и проверьте, какие заказы участвуют в отчёте.","Сверяйте стоимость заказов с фактически полученной оплатой: это разные показатели.","При расхождении проверьте даты, отменённые заказы, доставку, скидки и предоплаты."],"note":"Учебные заказы могут влиять на отчёты пробной компании. Отчёты приложения не заменяют обязательный бухгалтерский учёт.","related":["payment","trial"]},
|
||||||
|
{"id":"chat","category":"Компания и доступ","title":"Чат команды и будущая поддержка","keywords":"чат сообщение команда бот помощник поддержка","steps":["Раздел «Чат» предназначен для общения внутри рабочей компании в пределах доступных функций.","Для вопросов о работе приложения используйте руководство в «Помощь».","AI-помощник поддержки пока не подключён. Поисковая строка руководства ищет готовые инструкции и не отправляет вопрос боту."],"note":"Не передавайте пароли, коды подтверждения и банковские данные в чатах.","related":["support","permissions"]},
|
||||||
|
{"id":"mailings","category":"Заказы и клиенты","title":"Рассылки и согласие клиентов","keywords":"рассылки реклама промокод маркетинг согласие","steps":["Перед рекламным сообщением проверьте законное основание и необходимое согласие получателя.","Наличие телефона в заказе или импорт из Telegram сами по себе не означают подписку на рекламу.","Проверьте получателей и содержание перед отправкой доступными средствами."],"note":"Импорт клиентской базы не должен автоматически проставлять маркетинговое согласие.","related":["clients","permissions"]},
|
||||||
|
{"id":"support","category":"Решение проблем","title":"Что подготовить для обращения в поддержку","keywords":"помощь проблема ошибка не работает обращение оператор","steps":["Укажите раздел и действие, после которого возникла проблема.","Запишите точный текст ошибки, время, устройство и браузер.","Опишите ожидаемый результат и что получилось вместо него.","При необходимости приложите снимок экрана, предварительно закрыв телефоны, адреса и другие лишние данные."],"note":"Канал обращений и бот ещё не подключены. Вкладка «Поддержка» пока предлагает поиск по инструкциям и памятку; сообщение никуда не отправляется.","related":["login","sync","pdf-problem"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,7 @@
|
|||||||
const CACHE='sun-catering-pwa-v102-20260918-safari-login';
|
const CACHE='sun-catering-pwa-v103-20260918-help-center';
|
||||||
const VERSION='20260918-safari-login';
|
const VERSION='20260918-help-center';
|
||||||
const CORE=[
|
const CORE=[
|
||||||
|
`./core/help-center.js?v=${VERSION}`,`./core/help-center.css?v=${VERSION}`,`./help/knowledge-v1.json?v=${VERSION}`,
|
||||||
'./','./index.html',`./core/mobile-order.js?v=${VERSION}`,`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
'./','./index.html',`./core/mobile-order.js?v=${VERSION}`,`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
||||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/login-signature-v1776.css?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/login-signature-v1776.css?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||||
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
|
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
|
||||||
@ -9,6 +10,7 @@ const CORE=[
|
|||||||
'./offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg'
|
'./offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg'
|
||||||
];
|
];
|
||||||
const CRITICAL_FRESH=new Set([
|
const CRITICAL_FRESH=new Set([
|
||||||
|
'/core/help-center.js','/core/help-center.css','/help/knowledge-v1.json',
|
||||||
'/core/mobile-order.js','/core/cloud-transport.js','/core/trial-demo.js','/core/proposal-layout.js',
|
'/core/mobile-order.js','/core/cloud-transport.js','/core/trial-demo.js','/core/proposal-layout.js',
|
||||||
'/core/banquet-menu.js','/core/access-policy.js','/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/login-signature-v1776.css','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js'
|
'/core/banquet-menu.js','/core/access-policy.js','/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/login-signature-v1776.css','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js'
|
||||||
]);
|
]);
|
||||||
|
|||||||
48
tests/help-center.spec.mjs
Normal file
48
tests/help-center.spec.mjs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {test,expect} from '@playwright/test';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
const kb=JSON.parse(fs.readFileSync('public/help/knowledge-v1.json','utf8'));
|
||||||
|
|
||||||
|
async function fixture(page){
|
||||||
|
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html; charset=utf-8',body:'<!doctype html><html><body><header><nav></nav></header><main>Рабочая база</main></body></html>'}));
|
||||||
|
await page.goto('/index.html');await page.addStyleTag({url:'/core/help-center.css'});await page.addScriptTag({url:'/core/help-center.js'});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('manual search, categories and related instructions work without sending company data',async({page})=>{
|
||||||
|
expect(new Set(kb.articles.map(a=>a.id)).size).toBe(kb.articles.length);
|
||||||
|
expect(kb.articles.every(a=>a.related.every(id=>kb.articles.some(b=>b.id===id)))).toBe(true);
|
||||||
|
await fixture(page);await page.evaluate(()=>localStorage.setItem('sunOrders','private-order-canary'));
|
||||||
|
const requests=[];page.on('request',r=>requests.push(r.url()));
|
||||||
|
await page.getByRole('button',{name:'Помощь',exact:true}).click();
|
||||||
|
await expect(page.locator('#ctHelpResults details')).toHaveCount(kb.articles.length);
|
||||||
|
await page.getByRole('searchbox').fill('предоплата');
|
||||||
|
await expect(page.locator('[data-article="payment"]')).toBeVisible();
|
||||||
|
await page.locator('[data-article="payment"] summary').click();await expect(page.locator('[data-article="payment"]')).toContainText('синий');
|
||||||
|
await page.locator('[data-article="payment"] [data-help-article="delivery"]').click();await expect(page.locator('[data-article="delivery"]')).toHaveAttribute('open','');
|
||||||
|
await page.getByRole('combobox',{name:'Категория',exact:true}).selectOption('Меню и ТТК');await expect(page.locator('#ctHelpResults details')).toHaveCount(3);
|
||||||
|
await page.getByRole('searchbox').fill('<img src=x onerror=alert(1)>');await expect(page.locator('#ctHelpStatus')).toContainText('Ничего не найдено');
|
||||||
|
expect(requests.every(u=>u.includes('/help/knowledge-v1.json'))).toBe(true);
|
||||||
|
expect(await page.evaluate(()=>localStorage.sunOrders)).toBe('private-order-canary');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('support clearly stays offline, question opens a guide and dialog fits mobile',async({page})=>{
|
||||||
|
await fixture(page);await page.getByRole('button',{name:'Помощь',exact:true}).click();
|
||||||
|
await expect(page.locator('#ctHelpResults details')).toHaveCount(kb.articles.length);
|
||||||
|
await page.getByRole('button',{name:'Поддержка',exact:true}).click();await expect(page.locator('#ctHelpSupport')).toContainText('пока не подключены');
|
||||||
|
await page.getByRole('button',{name:'Как отметить оплату?',exact:true}).click();await expect(page.getByRole('searchbox')).toHaveValue('оплата');
|
||||||
|
expect(await page.locator('#ctHelpDialog').evaluate(el=>el.getBoundingClientRect().width<=innerWidth&&el.scrollWidth<=el.clientWidth)).toBe(true);
|
||||||
|
await page.keyboard.press('Escape');await expect(page.locator('#ctHelpDialog')).not.toBeVisible();await expect(page.getByRole('button',{name:'Помощь',exact:true})).toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed handbook load provides a working retry',async({page})=>{
|
||||||
|
let count=0;await page.route('**/help/knowledge-v1.json*',r=>++count===1?r.fulfill({status:503,body:''}):r.continue());
|
||||||
|
await fixture(page);await page.getByRole('button',{name:'Помощь',exact:true}).click();await expect(page.locator('#ctHelpStatus')).toContainText('Не удалось');
|
||||||
|
await page.getByRole('button',{name:'Повторить загрузку',exact:true}).click();await expect(page.locator('#ctHelpResults details')).toHaveCount(kb.articles.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('full app exposes help on login without exposing the workspace',async({page})=>{
|
||||||
|
await page.route('**://api.caterium.ru/**',r=>r.abort());await page.route('**://*.supabase.co/**',r=>r.abort());
|
||||||
|
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||||
|
await page.getByRole('button',{name:'Помощь со входом',exact:true}).click();
|
||||||
|
await expect(page.locator('[data-article="iphone"]')).toBeVisible();await expect(page.locator('body > header')).toBeHidden();
|
||||||
|
await page.getByRole('button',{name:'Закрыть помощь',exact:true}).click();await expect(page.locator('#sunGateEmailV3')).toBeVisible();
|
||||||
|
});
|
||||||
@ -2,12 +2,12 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
import {fileURLToPath} from 'node:url';
|
import {fileURLToPath} from 'node:url';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir:'.',
|
testDir:'.',
|
||||||
testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'],
|
testMatch:['help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'],
|
||||||
timeout:30000,
|
timeout:30000,
|
||||||
use:{baseURL:'http://127.0.0.1:4173'},
|
use:{baseURL:'http://127.0.0.1:4173'},
|
||||||
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
|
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
|
||||||
projects:[
|
projects:[
|
||||||
{name:'iphone-webkit',testMatch:['login-recovery.spec.mjs','account-access.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
{name:'iphone-webkit',testMatch:['help-center.spec.mjs','login-recovery.spec.mjs','account-access.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
||||||
{name:'desktop',use:{...devices['Desktop Chrome']}},
|
{name:'desktop',use:{...devices['Desktop Chrome']}},
|
||||||
{name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}}
|
{name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent')
|
|||||||
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
|
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
|
||||||
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
||||||
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
||||||
check(sw.includes('v102-20260918-safari-login')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
check(sw.includes('v103-20260918-help-center')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
||||||
check(index.includes('20260918-safari-login')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
check(index.includes('20260918-help-center')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
||||||
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
|
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
|
||||||
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
|
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
|
||||||
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
||||||
@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30
|
|||||||
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
|
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
|
||||||
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
|
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
|
||||||
check(releaseManifest.channel==='production','release manifest channel is production');
|
check(releaseManifest.channel==='production','release manifest channel is production');
|
||||||
check(String(releaseManifest.pwaCache||'').includes('v102-20260918-safari-login'),'release manifest points to current PWA cache');
|
check(String(releaseManifest.pwaCache||'').includes('v103-20260918-help-center'),'release manifest points to current PWA cache');
|
||||||
check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
|
check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
|
||||||
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
|
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
|
||||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
|
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
|
||||||
@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace
|
|||||||
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
|
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
|
||||||
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
|
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
|
||||||
check(pkg.version==='17.7.3','package version is v17.7.3');
|
check(pkg.version==='17.7.3','package version is v17.7.3');
|
||||||
check(index.includes('20260918-safari-login'),'index cache bust is v17.7.3');
|
check(index.includes('20260918-help-center'),'index cache bust is v17.7.3');
|
||||||
check(sw.includes('v102-20260918-safari-login')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
check(sw.includes('v103-20260918-help-center')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
||||||
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
||||||
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
||||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
||||||
|
|||||||
@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
|
|||||||
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
||||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||||
if(!sw.includes('20260918-safari-login')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
if(!sw.includes('20260918-help-center')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
||||||
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-safari-login')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-help-center')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||||
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
||||||
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
||||||
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user