diff --git a/docs/release-manifest.json b/docs/release-manifest.json index 67d92ce..2f838b5 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v102-20260918-safari-login", + "pwaCache": "v103-20260918-help-center", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, diff --git a/docs/support-assistant.md b/docs/support-assistant.md new file mode 100644 index 0000000..13ace9f --- /dev/null +++ b/docs/support-assistant.md @@ -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 модели, канал передачи человеку, контакт поддержки, допустимые категории данных, срок хранения диалогов и необходимость режима ответов по данным компании. Текущая реализация не требует этих решений и уже полезна как руководство. diff --git a/package.json b/package.json index 998d29b..98a821c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "17.7.3", "type": "module", "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", "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", diff --git a/public/core/help-center.css b/public/core/help-center.css new file mode 100644 index 0000000..8cb3ff0 --- /dev/null +++ b/public/core/help-center.css @@ -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}} diff --git a/public/core/help-center.js b/public/core/help-center.js new file mode 100644 index 0000000..8657f7a --- /dev/null +++ b/public/core/help-center.js @@ -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})=>`
${esc(a.title)}${esc(a.category)}
    ${a.steps.map(s=>`
  1. ${esc(s)}
  2. `).join('')}

${esc(a.note)}

`).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=''+[...new Set(data.articles.map(a=>a.category))].map(c=>``).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=`

Помощь в Caterium

Руководство пользователя и поддержка

`; + 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(); +})(); diff --git a/public/core/performance.js b/public/core/performance.js index 27f3028..e221d67 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; 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}}; function installAuthBoot(){ diff --git a/public/help/knowledge-v1.json b/public/help/knowledge-v1.json new file mode 100644 index 0000000..d0dfb0c --- /dev/null +++ b/public/help/knowledge-v1.json @@ -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"]} + ] +} diff --git a/public/index.html b/public/index.html index 3892a56..5852d56 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ - - + diff --git a/public/service-worker.js b/public/service-worker.js index ddc65ea..dbc92e6 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,6 +1,7 @@ -const CACHE='sun-catering-pwa-v102-20260918-safari-login'; -const VERSION='20260918-safari-login'; +const CACHE='sun-catering-pwa-v103-20260918-help-center'; +const VERSION='20260918-help-center'; 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}`, `./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', @@ -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' ]; 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/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' ]); diff --git a/tests/help-center.spec.mjs b/tests/help-center.spec.mjs new file mode 100644 index 0000000..c3bc557 --- /dev/null +++ b/tests/help-center.spec.mjs @@ -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:'
Рабочая база
'})); + 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('');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(); +}); diff --git a/tests/playwright.config.mjs b/tests/playwright.config.mjs index c566be7..9a827d2 100644 --- a/tests/playwright.config.mjs +++ b/tests/playwright.config.mjs @@ -2,12 +2,12 @@ import { defineConfig, devices } from '@playwright/test'; import {fileURLToPath} from 'node:url'; export default defineConfig({ 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, 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}, 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:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}} ] diff --git a/tests/release-check.mjs b/tests/release-check.mjs index acbb6ed..ffa8595 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -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(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(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(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(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-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("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'); @@ -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(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json'); 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(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'); @@ -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(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(index.includes('20260918-safari-login'),'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(index.includes('20260918-help-center'),'index cache bust is v17.7.3'); +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(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'); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index a90a59e..cbb436c 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -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'); 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(!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(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(!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-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("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');