Если аккаунт с этим email уже есть — введите его пароль. Если аккаунта ещё нет — придумайте новый пароль и повторите его.
`;
- bindPasswordEyes(gate);const btn=gate.querySelector('#sunInviteJoinV27');const run=async()=>{const c=client(),email=String(preview.email||'').trim(),name=String(gate.querySelector('#sunInviteNameV27')?.value||'').trim(),password=String(gate.querySelector('#sunInvitePasswordV27')?.value||''),password2=String(gate.querySelector('#sunInvitePassword2V27')?.value||''),err=gate.querySelector('#sunGateErrorV3');if(password.length<6){err.textContent='Пароль должен содержать минимум 6 символов.';return;}if(password!==password2){err.textContent='Пароли не совпадают.';return;}btn.disabled=true;err.textContent='Проверяю аккаунт…';try{let r=await c.auth.signInWithPassword({email,password});if(r.error){r=await c.auth.signUp({email,password,options:{data:{name,registration_source:'caterium_invite_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error)throw new Error('Этот email уже зарегистрирован. Введите пароль от существующего аккаунта.');r=retry;}}const userId=r.data.user?.id||r.data.session?.user?.id||'';if(userId)sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1');err.textContent='Аккаунт готов. Подключаю компанию…';setTimeout(()=>finishInvite(token,gate),180);}catch(e){err.textContent=String(e?.message||e||'Не удалось войти или создать аккаунт.');btn.disabled=false;}};btn.onclick=run;gate.querySelector('#sunInvitePassword2V27').addEventListener('keydown',e=>{if(e.key==='Enter')run()});
+ bindPasswordEyes(gate);const btn=gate.querySelector('#sunInviteJoinV27');const run=async()=>{const c=client(),email=String(preview.email||'').trim(),name=String(gate.querySelector('#sunInviteNameV27')?.value||'').trim(),password=String(gate.querySelector('#sunInvitePasswordV27')?.value||''),password2=String(gate.querySelector('#sunInvitePassword2V27')?.value||''),err=gate.querySelector('#sunGateErrorV3');if(password.length<6){err.textContent='Пароль должен содержать минимум 6 символов.';return;}if(password!==password2){err.textContent='Пароли не совпадают.';return;}btn.disabled=true;err.textContent='Проверяю аккаунт…';try{let r=await c.auth.signInWithPassword({email,password});if(r.error){r=await c.auth.signUp({email,password,options:{data:{name,registration_source:'caterium_invite_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error)throw new Error('Этот email уже зарегистрирован. Введите пароль от существующего аккаунта.');r=retry;}}err.textContent='Аккаунт готов. Подключаю компанию…';setTimeout(()=>finishInvite(token,gate),180);}catch(e){err.textContent=String(e?.message||e||'Не удалось войти или создать аккаунт.');btn.disabled=false;}};btn.onclick=run;gate.querySelector('#sunInvitePassword2V27').addEventListener('keydown',e=>{if(e.key==='Enter')run()});
}
// Password changes now happen from account settings (see cloud-sync-v2.js's
@@ -3920,7 +3890,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
else localOnlyButton.closest('p').remove();
const emailInput=gate.querySelector('#sunGateEmailV3'),passwordInput=gate.querySelector('#sunGatePasswordV3'),password2Input=gate.querySelector('#sunGatePassword2V27'),companyInput=gate.querySelector('#sunGateCompanyV3'),submitBtn=gate.querySelector('#sunGateSubmitV3'),gateError=gate.querySelector('#sunGateErrorV3');let registerMode=false;
const setMode=(registration)=>{registerMode=Boolean(registration);gate.querySelector('#sunGateRegisterFieldsV27').hidden=!registerMode;gate.querySelector('#sunGateConfirmWrapV27').hidden=!registerMode;gate.querySelector('#sunGateTitleV3').textContent=registerMode?'Создать аккаунт Caterium':'Войти в рабочее пространство';gate.querySelector('#sunGateSubtitleV3').textContent=registerMode?'Заполните данные — после регистрации Caterium откроется автоматически':'Ваши заказы. Ваша команда. Ваш результат.';submitBtn.textContent=registerMode?'Создать аккаунт':'Войти';gate.querySelector('#sunGateSwitchTextV27').textContent=registerMode?'Уже есть аккаунт?':'Нет аккаунта?';gate.querySelector('#sunGateSwitchV27').textContent=registerMode?'Войти':'Создать аккаунт';passwordInput.autocomplete=registerMode?'new-password':'current-password';gateError.textContent='';if(registerMode)setTimeout(()=>companyInput.focus(),30);else setTimeout(()=>emailInput.focus(),30)};
- async function auth(){const c=client(),email=emailInput.value.trim(),password=passwordInput.value,password2=password2Input.value,companyName=String(companyInput.value||'').trim();if(!c){gateError.textContent='Облачный сервис не подключён.';return;}if(registerMode&&!companyName){gateError.textContent='Введите название компании.';return;}if(!email||password.length<6){gateError.textContent='Введите email и пароль минимум из 6 символов.';return;}if(registerMode&&password!==password2){gateError.textContent='Пароли не совпадают.';return;}submitBtn.disabled=true;gateError.textContent=registerMode?'Создаю аккаунт…':'Выполняю вход…';try{let r;if(registerMode){savePendingRegistration(email,companyName);const existing=await c.auth.signInWithPassword({email,password});if(!existing.error&&existing.data.session){r=existing;}else{r=await c.auth.signUp({email,password,options:{data:{company_name:companyName,registration_source:'caterium_public_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error){clearPendingRegistration();throw new Error('Этот email уже зарегистрирован. Войдите в аккаунт или восстановите пароль.');}r=retry;}}}else r=await c.auth.signInWithPassword({email,password});if(r.error)throw r.error;const authUser=r.data.user||r.data.session?.user||null;const userId=authUser?.id||'';if(userId)sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1');gateError.textContent=registerMode?'Аккаунт готов. Создаю компанию…':'Вход выполнен. Загружаю рабочую базу…';setTimeout(async()=>{await cloud()?.reloadMemberships?.();if(registerMode&&workspace()){clearPendingRegistration();gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();return;}if(getPendingRegistration(email)&&!workspace()){await finishPendingRegistration(gate,email);return;}if(workspace()){gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();}else{gate.remove();ensureAuthGate();}},180);}catch(err){gateError.textContent=errText(err,'Не удалось выполнить операцию.')}finally{submitBtn.disabled=false}}
+ async function auth(){const c=client(),email=emailInput.value.trim(),password=passwordInput.value,password2=password2Input.value,companyName=String(companyInput.value||'').trim();if(!c){gateError.textContent='Облачный сервис не подключён.';return;}if(registerMode&&!companyName){gateError.textContent='Введите название компании.';return;}if(!email||password.length<6){gateError.textContent='Введите email и пароль минимум из 6 символов.';return;}if(registerMode&&password!==password2){gateError.textContent='Пароли не совпадают.';return;}submitBtn.disabled=true;gateError.textContent=registerMode?'Создаю аккаунт…':'Выполняю вход…';try{let r;if(registerMode){savePendingRegistration(email,companyName);const existing=await c.auth.signInWithPassword({email,password});if(!existing.error&&existing.data.session){r=existing;}else{r=await c.auth.signUp({email,password,options:{data:{company_name:companyName,registration_source:'caterium_public_signup'}}});if(r.error)throw r.error;if(!r.data.session){const retry=await c.auth.signInWithPassword({email,password});if(retry.error){clearPendingRegistration();throw new Error('Этот email уже зарегистрирован. Войдите в аккаунт или восстановите пароль.');}r=retry;}}}else r=await c.auth.signInWithPassword({email,password});if(r.error)throw r.error;gateError.textContent=registerMode?'Аккаунт готов. Создаю компанию…':'Вход выполнен. Загружаю рабочую базу…';setTimeout(async()=>{await cloud()?.reloadMemberships?.();if(registerMode&&workspace()){clearPendingRegistration();gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();return;}if(getPendingRegistration(email)&&!workspace()){await finishPendingRegistration(gate,email);return;}if(workspace()){gate.remove();document.body.classList.remove('sun-cloud-auth-required');await cloud()?.pull?.();applyNavPermissions();}else{gate.remove();ensureAuthGate();}},180);}catch(err){gateError.textContent=errText(err,'Не удалось выполнить операцию.')}finally{submitBtn.disabled=false}}
gate.querySelector('#sunGateSwitchV27').onclick=()=>setMode(!registerMode);submitBtn.onclick=auth;passwordInput.addEventListener('keydown',e=>{if(e.key==='Enter'&&!registerMode)auth()});password2Input.addEventListener('keydown',e=>{if(e.key==='Enter'&®isterMode)auth()});setMode(false);
}
@@ -3928,19 +3898,19 @@ window.SUN_LEGACY_CATALOG_V175=[];
if(applying)return;applying=true;
try{
const ws=workspace();if(ws?.id!==lastWorkspaceId){lastWorkspaceId=ws?.id||'';}
- ensureAuthGate();normalizeLegacyLocalAuth();installGuards();applyNavPermissions();hideLegacyRoleCard();injectQuickPinCard();showPinLockIfNeeded();
+ ensureAuthGate();normalizeLegacyLocalAuth();installGuards();applyNavPermissions();hideLegacyRoleCard();removeLegacyQuickPin();
}finally{applying=false;}
}
window.addEventListener('sun:open-cloud-users',openModal);
window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(onCloudChanged,0));
- document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки')setTimeout(()=>{hideLegacyRoleCard();injectQuickPinCard();},120);},true);
+ document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки')setTimeout(()=>{hideLegacyRoleCard();removeLegacyQuickPin();},120);},true);
(()=>{let queued=false;const run=()=>{if(queued||applying)return;queued=true;setTimeout(()=>{queued=false;if(!applying){applyNavPermissions();hideLegacyRoleCard();}},60);};[document.querySelector('header'),document.getElementById('enterprise-settings')].filter(Boolean).forEach(root=>new MutationObserver(run).observe(root,{childList:true,subtree:true}));})();
installStyle();normalizeLegacyLocalAuth();ensureModal();
let tries=0;const boot=setInterval(()=>{tries++;if(window.SunCloudV2){clearInterval(boot);setTimeout(onCloudChanged,80);}else if(tries>80)clearInterval(boot);},100);
- setInterval(()=>{if(document.hidden)return;try{ensureAuthGate();if(workspace()&&session()?.user){applyNavPermissions();injectQuickPinCard();showPinLockIfNeeded();}}catch(_){}},5000);
+ setInterval(()=>{if(document.hidden)return;try{ensureAuthGate();if(workspace()&&session()?.user){applyNavPermissions();removeLegacyQuickPin();}}catch(_){}},5000);
window.SunAdminRBACV3={VERSION,openUsers:openModal,apply:applyNavPermissions,hasPermission:has};
})();
diff --git a/public/core/auth-security-v1774.js b/public/core/auth-security-v1774.js
index f771c39..7322dc6 100644
--- a/public/core/auth-security-v1774.js
+++ b/public/core/auth-security-v1774.js
@@ -42,7 +42,7 @@
busy=true;const button=$('sunGateSubmitV3');if(button)button.disabled=true;setError(gate,'Выполняю вход…');
try{
const result=await c.auth.signInWithPassword({email,password});if(result.error)throw result.error;
- const authUser=result.data?.user||result.data?.session?.user||null;const userId=authUser?.id||'';if(userId)try{sessionStorage.setItem(`sunCloudQuickPinUnlockedV3:${userId}`,'1')}catch(_){}
+
const persisted=await c.auth.getSession();if(persisted.error)throw persisted.error;if(!persisted.data?.session?.user)throw new Error('Сессия входа не сохранилась. Повторите вход.');
setError(gate,'Вход выполнен. Открываю Caterium…');
setTimeout(()=>location.reload(),120);
diff --git a/public/core/performance.js b/public/core/performance.js
index 1085be9..49904cb 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-sidebar-cleanup';
+ const RELEASE='20260918-mobile-calendar';
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
index d0dfb0c..9daaaa6 100644
--- a/public/help/knowledge-v1.json
+++ b/public/help/knowledge-v1.json
@@ -22,7 +22,7 @@
{"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":"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"]},
diff --git a/public/index.html b/public/index.html
index b6e1b69..fe8b257 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 98984d9..9a71fbd 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,5 +1,5 @@
-const CACHE='sun-catering-pwa-v108-20260918-sidebar-cleanup';
-const VERSION='20260918-sidebar-cleanup';
+const CACHE='sun-catering-pwa-v109-20260918-mobile-calendar';
+const VERSION='20260918-mobile-calendar';
const CORE=[
'./vendor/supabase-2.112.4.min.js',
`./core/help-center.js?v=${VERSION}`,`./core/help-center.css?v=${VERSION}`,`./help/knowledge-v1.json?v=${VERSION}`,
diff --git a/tests/account-access.spec.mjs b/tests/account-access.spec.mjs
index ebe8f0a..c0c1b71 100644
--- a/tests/account-access.spec.mjs
+++ b/tests/account-access.spec.mjs
@@ -1,6 +1,22 @@
import fs from 'node:fs';
import {test,expect} from '@playwright/test';
+test('retired device PIN cannot lock an authenticated account or replace email login',async({page})=>{
+ await page.addInitScript(()=>{localStorage.setItem('sunCloudQuickPinV3:pin-test','legacy-device-pin');localStorage.setItem('unrelated-preference','keep')});
+ await page.route('https://**',r=>r.abort());
+ await page.goto('/index.html',{waitUntil:'domcontentloaded'});
+ await expect(page.locator('#sunGateEmailV3')).toBeVisible();
+ await expect(page.locator('#sunGatePasswordV3')).toBeVisible();
+ await page.evaluate(()=>{
+ window.SunCloudV2={getSession:()=>({user:{id:'pin-test',email:'pin@example.invalid'}}),getWorkspace:()=>({id:'pin-company',role:'admin'}),status:()=>({connected:true,signedIn:true,membershipsLoaded:true}),hasPermission:()=>true,getClient:()=>null};
+ window.dispatchEvent(new Event('sun:cloud-permissions-changed'));
+ });
+ await expect(page.locator('body > header')).toBeVisible();
+ await page.getByRole('button',{name:'Настройки',exact:true}).click();
+ await expect(page.locator('#sunCloudPinLockV3,#sunCloudQuickPinCardV3,#sunSetQuickPinV3')).toHaveCount(0);
+ expect(await page.evaluate(()=>({pin:localStorage.getItem('sunCloudQuickPinV3:pin-test'),preference:localStorage.getItem('unrelated-preference')}))).toEqual({pin:null,preference:'keep'});
+});
+
test('anonymous login stays closed with an old local-mode preference and an unavailable backend',async({page})=>{
await page.addInitScript(()=>localStorage.setItem('sunLocalOnlyModeV1','1'));
await page.route('**://api.caterium.ru/**',r=>r.abort());
diff --git a/tests/calendar-print.spec.mjs b/tests/calendar-print.spec.mjs
index 3d056e4..da5b4a6 100644
--- a/tests/calendar-print.spec.mjs
+++ b/tests/calendar-print.spec.mjs
@@ -1,6 +1,34 @@
import {test,expect} from '@playwright/test';
import {bootCalendar,printCalendar} from './calendar-fixture.mjs';
+test('phone and tablet calendar modes fit without sideways scrolling and still open orders',async({page,context})=>{
+ await bootCalendar(page,context,[9]);
+ await page.evaluate(()=>{
+ const now=new Date(),today=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
+ orders.forEach(o=>{o.date=today;o.event='Оченьдлинноеназваниемероприятиябезпробелов'.repeat(3);o.address='Длинныйадресдоставки'.repeat(5);o.total=123456789});
+ });
+ for(const width of [320,390,768]){
+ await page.setViewportSize({width,height:844});
+ for(const mode of ['month','week','day']){
+ await page.locator(`[data-cal-mode="${mode}"]`).click();
+ const fit=await page.locator('#calendar').evaluate(el=>{
+ const viewport=document.documentElement.clientWidth;
+ const panels=[el,...el.querySelectorAll('.cal-scroll,.cal-month,.cal-controls,.cal-metrics,.cal-agenda')];
+ return panels.every(p=>p.getBoundingClientRect().left>=-1&&p.getBoundingClientRect().right<=viewport+1&&p.scrollWidth<=p.clientWidth+1);
+ });
+ expect(fit,`${width}px ${mode}`).toBe(true);
+ if(mode==='month'){
+ await expect(page.locator('#calendar .cal-event:visible')).toHaveCount(9);
+ const timeFits=await page.locator('.cal-event-time').evaluateAll(nodes=>nodes.every(n=>n.getBoundingClientRect().right<=n.closest('button').getBoundingClientRect().right));
+ expect(timeFits,`${width}px times`).toBe(true);
+ }
+ }
+ }
+ await page.setViewportSize({width:390,height:844});
+ await page.locator('.cal-agenda-order').first().getByRole('button',{name:'Открыть'}).click();
+ expect(await page.evaluate(()=>draft.id)).toBe(1000);
+});
+
test('busy calendar days show nine single-line orders and retain access to the rest',async({page,context})=>{
const dates=await bootCalendar(page,context);
const day=i=>page.locator(`.cal-day[data-date="${dates[i]}"]`);
diff --git a/tests/playwright.config.mjs b/tests/playwright.config.mjs
index bccac5f..b8d1760 100644
--- a/tests/playwright.config.mjs
+++ b/tests/playwright.config.mjs
@@ -8,7 +8,7 @@ export default defineConfig({
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
projects:[
{name:'iphone-pdf',testMatch:['proposal-quality.spec.mjs'],grep:/all six selections|transparent wide|an actual offer downloads/,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:'iphone-webkit',testMatch:['help-center.spec.mjs','login-recovery.spec.mjs','account-access.spec.mjs','calendar-print.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 072ae9a..34bbc31 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('v108-20260918-sidebar-cleanup')&&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-sidebar-cleanup')&&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('v109-20260918-mobile-calendar')&&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-mobile-calendar')&&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('v108-20260918-sidebar-cleanup'),'release manifest points to current PWA cache');
+check(String(releaseManifest.pwaCache||'').includes('v109-20260918-mobile-calendar'),'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-sidebar-cleanup'),'index cache bust is v17.7.3');
-check(sw.includes('v108-20260918-sidebar-cleanup')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
+check(index.includes('20260918-mobile-calendar'),'index cache bust is v17.7.3');
+check(sw.includes('v109-20260918-mobile-calendar')&&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 544ea08..3258545 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-sidebar-cleanup')||!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-sidebar-cleanup')||!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-mobile-calendar')||!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-mobile-calendar')||!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');