diff --git a/docs/release-manifest.json b/docs/release-manifest.json index 9e8c57d..d5112ad 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v108-20260918-sidebar-cleanup", + "pwaCache": "v109-20260918-mobile-calendar", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, diff --git a/public/app-runtime.js b/public/app-runtime.js index 07b286d..16e1b27 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -3793,43 +3793,13 @@ window.SUN_LEGACY_CATALOG_V175=[]; catch(e){toast(e.message||String(e),'error',7000);} } - const hex=bytes=>[...bytes].map(x=>x.toString(16).padStart(2,'0')).join(''); - const unhex=value=>new Uint8Array(String(value||'').match(/.{1,2}/g)?.map(x=>parseInt(x,16))||[]); - async function pinDigest(pin,salt){ - const raw=new TextEncoder().encode(String(pin||'')); - if(crypto?.subtle){ - const key=await crypto.subtle.importKey('raw',raw,'PBKDF2',false,['deriveBits']); - const bits=await crypto.subtle.deriveBits({name:'PBKDF2',hash:'SHA-256',salt,iterations:120000},key,256); - return hex(new Uint8Array(bits)); - } - let v=2166136261;for(let n=0;n<5000;n++)for(const b of [...raw,...salt]){v^=b;v=Math.imul(v,16777619);}return String(v>>>0); - } - async function makePinRecord(pin){const salt=crypto.getRandomValues(new Uint8Array(16));return `v1:${hex(salt)}:${await pinDigest(pin,salt)}`;} - async function verifyPin(pin,record){const parts=String(record||'').split(':');if(parts.length!==3||parts[0]!=='v1')return false;const salt=unhex(parts[1]);return (await pinDigest(pin,salt))===parts[2];} - - function pinKey(){const id=session()?.user?.id;return id?`sunCloudQuickPinV3:${id}`:'';} - function pinUnlockKey(){const id=session()?.user?.id;return id?`sunCloudQuickPinUnlockedV3:${id}`:'';} - async function setQuickPin(){ - const id=session()?.user?.id;if(!id)return; - const p1=prompt('Введите новый PIN (4–8 цифр):','');if(p1===null)return;if(!/^\d{4,8}$/.test(p1))return toast('PIN должен содержать 4–8 цифр.','warn'); - const p2=prompt('Повторите PIN:','');if(p1!==p2)return toast('PIN-коды не совпадают.','warn'); - localStorage.setItem(pinKey(),await makePinRecord(p1));sessionStorage.setItem(pinUnlockKey(),'1');toast('Быстрый PIN включён на этом устройстве.','success');injectQuickPinCard(); - } - function removeQuickPin(){const key=pinKey();if(!key)return;localStorage.removeItem(key);sessionStorage.removeItem(pinUnlockKey());toast('Быстрый PIN отключён.','success');injectQuickPinCard();} - function injectQuickPinCard(){ - const card=$('sunCloudV2Card');if(!card||!session()?.user)return; - let box=$('sunCloudQuickPinCardV3');if(!box){box=document.createElement('div');box.id='sunCloudQuickPinCardV3';box.className='sun-cloud-v2-box wide sun-cloud-pin-card';card.appendChild(box);} - const enabled=Boolean(pinKey()&&localStorage.getItem(pinKey())); - box.innerHTML=`

Быстрый вход на этом устройстве

Основной аккаунт — email и пароль. PIN только быстро разблокирует уже авторизованное устройство и не меняет облачные права.

${enabled?'':''}
`; - $('sunSetQuickPinV3').onclick=setQuickPin;$('sunRemoveQuickPinV3')?.addEventListener('click',removeQuickPin); - } - - function showPinLockIfNeeded(){ - const ss=session(),ws=workspace();if(!ss?.user||!ws)return; - const key=pinKey(), unlock=pinUnlockKey();if(!key||!localStorage.getItem(key)||sessionStorage.getItem(unlock)==='1'||$('sunCloudPinLockV3'))return; - const overlay=document.createElement('div');overlay.id='sunCloudPinLockV3';overlay.className='sun-cloud-auth-gate';overlay.innerHTML=`
Caterium

Caterium

${esc(ws.display_name||ss.user.email||'Пользователь')} · ${esc(ROLE_LABELS[ws.role]||ws.role)}
`;document.body.appendChild(overlay); - const unlockFn=async()=>{const v=$('sunCloudPinInputV3').value;if(await verifyPin(v,localStorage.getItem(key))){sessionStorage.setItem(unlock,'1');overlay.remove();applyNavPermissions();}else $('sunCloudPinErrorV3').textContent='Неверный PIN.';}; - $('sunCloudPinUnlockV3').onclick=unlockFn;$('sunCloudPinInputV3').addEventListener('keydown',e=>{if(e.key==='Enter')unlockFn();});$('sunCloudUsePasswordV3').onclick=async()=>{if(window.SunCloudV2?.signOut)return window.SunCloudV2.signOut();try{await client()?.auth.signOut();}catch(_){}location.reload();};setTimeout(()=>$('sunCloudPinInputV3')?.focus(),50); + function removeLegacyQuickPin(){ + $('sunCloudQuickPinCardV3')?.remove(); + $('sunCloudPinLockV3')?.remove(); + try{ + Object.keys(localStorage).filter(k=>k.startsWith('sunCloudQuickPinV3:')).forEach(k=>localStorage.removeItem(k)); + Object.keys(sessionStorage).filter(k=>k.startsWith('sunCloudQuickPinUnlockedV3:')).forEach(k=>sessionStorage.removeItem(k)); + }catch(_){} } const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23'; @@ -3874,7 +3844,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; gate.innerHTML=`
Caterium

Приглашение в ${esc(preview.workspace_name||'компанию')}

Для ${esc(preview.email||'другого email')}

Сейчас вы вошли как ${esc(ss.user.email||'')}. Для принятия приглашения нужно войти под адресом ${esc(preview.email||'')}.

`;gate.querySelector('#sunInviteSignOut').onclick=async()=>{try{await window.SunCloudV2?.signOut?.()}catch(_){}};return; } gate.innerHTML=`
Caterium

Вас пригласили в ${esc(preview.workspace_name||'компанию')}

Роль: ${esc(ROLE_LABELS[preview.role]||preview.role)}
${passwordField('sunInvitePasswordV27','Пароль','new-password')}${passwordField('sunInvitePassword2V27','Подтвердите пароль','new-password')}

Если аккаунт с этим 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');