Remove device PIN and fit calendar to mobile screens

This commit is contained in:
pavlov346346-source 2026-09-18 20:43:09 +03:00
parent 45e77958cb
commit 47ae8df39c
12 changed files with 103 additions and 61 deletions

View File

@ -11,7 +11,7 @@
"serverReady": true, "serverReady": true,
"workspaceAutoDiscovery": true, "workspaceAutoDiscovery": true,
"invitesTemporarilyDisabled": false, "invitesTemporarilyDisabled": false,
"pwaCache": "v108-20260918-sidebar-cleanup", "pwaCache": "v109-20260918-mobile-calendar",
"fullOfferDescriptions": true, "fullOfferDescriptions": true,
"dynamicOfferRows": true, "dynamicOfferRows": true,
"pdfOfferDescriptionFix": true, "pdfOfferDescriptionFix": true,

View File

@ -3793,43 +3793,13 @@ window.SUN_LEGACY_CATALOG_V175=[];
catch(e){toast(e.message||String(e),'error',7000);} catch(e){toast(e.message||String(e),'error',7000);}
} }
const hex=bytes=>[...bytes].map(x=>x.toString(16).padStart(2,'0')).join(''); function removeLegacyQuickPin(){
const unhex=value=>new Uint8Array(String(value||'').match(/.{1,2}/g)?.map(x=>parseInt(x,16))||[]); $('sunCloudQuickPinCardV3')?.remove();
async function pinDigest(pin,salt){ $('sunCloudPinLockV3')?.remove();
const raw=new TextEncoder().encode(String(pin||'')); try{
if(crypto?.subtle){ Object.keys(localStorage).filter(k=>k.startsWith('sunCloudQuickPinV3:')).forEach(k=>localStorage.removeItem(k));
const key=await crypto.subtle.importKey('raw',raw,'PBKDF2',false,['deriveBits']); Object.keys(sessionStorage).filter(k=>k.startsWith('sunCloudQuickPinUnlockedV3:')).forEach(k=>sessionStorage.removeItem(k));
const bits=await crypto.subtle.deriveBits({name:'PBKDF2',hash:'SHA-256',salt,iterations:120000},key,256); }catch(_){}
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 (48 цифр):','');if(p1===null)return;if(!/^\d{4,8}$/.test(p1))return toast('PIN должен содержать 48 цифр.','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=`<h3>Быстрый вход на этом устройстве</h3><p class="sun-cloud-v2-note">Основной аккаунт — email и пароль. PIN только быстро разблокирует уже авторизованное устройство и не меняет облачные права.</p><div class="sun-cloud-v2-actions"><button class="outline" type="button" id="sunSetQuickPinV3">${enabled?'Изменить PIN':'Включить PIN'}</button>${enabled?'<button class="outline" type="button" id="sunRemoveQuickPinV3">Отключить PIN</button>':''}</div>`;
$('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=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Caterium</h2><div class="hint">${esc(ws.display_name||ss.user.email||'Пользователь')} · ${esc(ROLE_LABELS[ws.role]||ws.role)}</div></div></div><label>PIN<input id="sunCloudPinInputV3" type="password" inputmode="numeric" autocomplete="current-password"></label><div class="sun-cloud-auth-actions"><button class="primary" id="sunCloudPinUnlockV3" type="button">Войти</button><button class="outline" id="sunCloudUsePasswordV3" type="button">Выйти из аккаунта</button></div><div class="sun-cloud-auth-error" id="sunCloudPinErrorV3"></div></div>`;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);
} }
const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23'; const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23';
@ -3874,7 +3844,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Приглашение в ${esc(preview.workspace_name||'компанию')}</h2><div class="hint">Для ${esc(preview.email||'другого email')}</div></div></div><p class="hint">Сейчас вы вошли как <b>${esc(ss.user.email||'')}</b>. Для принятия приглашения нужно войти под адресом <b>${esc(preview.email||'')}</b>.</p><div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteSignOut" type="button">Выйти и продолжить</button></div></div>`;gate.querySelector('#sunInviteSignOut').onclick=async()=>{try{await window.SunCloudV2?.signOut?.()}catch(_){}};return; gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Приглашение в ${esc(preview.workspace_name||'компанию')}</h2><div class="hint">Для ${esc(preview.email||'другого email')}</div></div></div><p class="hint">Сейчас вы вошли как <b>${esc(ss.user.email||'')}</b>. Для принятия приглашения нужно войти под адресом <b>${esc(preview.email||'')}</b>.</p><div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteSignOut" type="button">Выйти и продолжить</button></div></div>`;gate.querySelector('#sunInviteSignOut').onclick=async()=>{try{await window.SunCloudV2?.signOut?.()}catch(_){}};return;
} }
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Вас пригласили в ${esc(preview.workspace_name||'компанию')}</h2><div class="hint">Роль: ${esc(ROLE_LABELS[preview.role]||preview.role)}</div></div></div><label>Имя<input id="sunInviteNameV27" autocomplete="name" value="${esc(preview.display_name||'')}"></label><label>Email<input id="sunInviteEmailV27" type="email" value="${esc(preview.email||'')}" readonly></label>${passwordField('sunInvitePasswordV27','Пароль','new-password')}${passwordField('sunInvitePassword2V27','Подтвердите пароль','new-password')}<div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteJoinV27" type="button">Присоединиться</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div><p class="hint">Если аккаунт с этим email уже есть — введите его пароль. Если аккаунта ещё нет — придумайте новый пароль и повторите его.</p></div>`; gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Вас пригласили в ${esc(preview.workspace_name||'компанию')}</h2><div class="hint">Роль: ${esc(ROLE_LABELS[preview.role]||preview.role)}</div></div></div><label>Имя<input id="sunInviteNameV27" autocomplete="name" value="${esc(preview.display_name||'')}"></label><label>Email<input id="sunInviteEmailV27" type="email" value="${esc(preview.email||'')}" readonly></label>${passwordField('sunInvitePasswordV27','Пароль','new-password')}${passwordField('sunInvitePassword2V27','Подтвердите пароль','new-password')}<div class="sun-cloud-auth-actions"><button class="primary" id="sunInviteJoinV27" type="button">Присоединиться</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3"></div><p class="hint">Если аккаунт с этим email уже есть — введите его пароль. Если аккаунта ещё нет — придумайте новый пароль и повторите его.</p></div>`;
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 // 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(); 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 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)}; 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'&&registerMode)auth()});setMode(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'&&registerMode)auth()});setMode(false);
} }
@ -3928,19 +3898,19 @@ window.SUN_LEGACY_CATALOG_V175=[];
if(applying)return;applying=true; if(applying)return;applying=true;
try{ try{
const ws=workspace();if(ws?.id!==lastWorkspaceId){lastWorkspaceId=ws?.id||'';} 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;} }finally{applying=false;}
} }
window.addEventListener('sun:open-cloud-users',openModal); window.addEventListener('sun:open-cloud-users',openModal);
window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(onCloudChanged,0)); 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}));})(); (()=>{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(); 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); 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}; window.SunAdminRBACV3={VERSION,openUsers:openModal,apply:applyNavPermissions,hasPermission:has};
})(); })();

View File

@ -42,7 +42,7 @@
busy=true;const button=$('sunGateSubmitV3');if(button)button.disabled=true;setError(gate,'Выполняю вход…'); busy=true;const button=$('sunGateSubmitV3');if(button)button.disabled=true;setError(gate,'Выполняю вход…');
try{ try{
const result=await c.auth.signInWithPassword({email,password});if(result.error)throw result.error; 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('Сессия входа не сохранилась. Повторите вход.'); const persisted=await c.auth.getSession();if(persisted.error)throw persisted.error;if(!persisted.data?.session?.user)throw new Error('Сессия входа не сохранилась. Повторите вход.');
setError(gate,'Вход выполнен. Открываю Caterium…'); setError(gate,'Вход выполнен. Открываю Caterium…');
setTimeout(()=>location.reload(),120); setTimeout(()=>location.reload(),120);

View File

@ -1,7 +1,7 @@
(()=>{ (()=>{
'use strict'; 'use strict';
const VERSION='17.7.3'; 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}}; const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
function installAuthBoot(){ function installAuthBoot(){

View File

@ -22,7 +22,7 @@
{"id":"proposal","category":"Документы","title":"Предложение клиенту и PDF","keywords":"кп коммерческое предложение pdf скачать печать шаблон оформление документ","steps":["Сохраните заказ с правильным составом, количеством, ценами и доставкой.","Нажмите «Предложение клиенту». Дождитесь подготовки просмотра.","Проверьте шаблон, данные компании, фотографии и итоговые суммы. Используйте редактирование и обновление при необходимости.","Перед отправкой откройте итоговый PDF и проверьте все страницы."],"note":"При медленной загрузке фотографий предложение может открыться без части изображений. После восстановления связи обновите просмотр. Отправка документа клиенту выполняется вами.","related":["branding","pdf-problem","edit-order"]}, {"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":"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":"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":"routes","category":"Планирование","title":"Маршруты доставки","keywords":"маршрут карта курьер адрес доставка поездка","steps":["Заполните полный адрес и время в заказах.","Откройте «Маршруты» или «Карта» и проверьте выбранные доставки.","Перед выездом сверяйте адрес, контакты, время и комментарий курьеру с заказом."],"note":"Карта не заменяет проверку подъезда, пропуска и условий разгрузки. Уточняйте их у клиента.","related":["delivery","clients","calendar"]},
{"id":"permissions","category":"Компания и доступ","title":"Сотрудники, роли и недоступные разделы","keywords":"команда права сотрудник приглашение доступ нет кнопки владелец администратор","steps":["Владелец управляет сотрудниками через «Настройки» → «Пользователи и права».","Назначайте каждому сотруднику только необходимые права.","Если раздел или действие недоступны, проверьте роль и тариф компании.","Не передавайте пароль владельца сотрудникам. Для каждого нужна отдельная учётная запись."],"note":"Поддержка и будущий помощник не должны предоставлять данные в обход прав пользователя.","related":["login","subscription","chat"]}, {"id":"permissions","category":"Компания и доступ","title":"Сотрудники, роли и недоступные разделы","keywords":"команда права сотрудник приглашение доступ нет кнопки владелец администратор","steps":["Владелец управляет сотрудниками через «Настройки» → «Пользователи и права».","Назначайте каждому сотруднику только необходимые права.","Если раздел или действие недоступны, проверьте роль и тариф компании.","Не передавайте пароль владельца сотрудникам. Для каждого нужна отдельная учётная запись."],"note":"Поддержка и будущий помощник не должны предоставлять данные в обход прав пользователя.","related":["login","subscription","chat"]},
{"id":"subscription","category":"Компания и доступ","title":"Где посмотреть тариф и пробный период","keywords":"подписка тариф оплата продление пробный срок лимиты","steps":["Откройте «Настройки» → «Тариф и подписка».","Посмотрите активный тариф, статус, срок доступа и лимиты сотрудников.","При недоступной функции проверьте условия тарифа и роль пользователя."],"note":"Условия будущих автосписаний и лицензионное соглашение находятся в разработке. Этот справочник не подтверждает подключение банковской оплаты или автоматического продления.","related":["trial","permissions"]}, {"id":"subscription","category":"Компания и доступ","title":"Где посмотреть тариф и пробный период","keywords":"подписка тариф оплата продление пробный срок лимиты","steps":["Откройте «Настройки» → «Тариф и подписка».","Посмотрите активный тариф, статус, срок доступа и лимиты сотрудников.","При недоступной функции проверьте условия тарифа и роль пользователя."],"note":"Условия будущих автосписаний и лицензионное соглашение находятся в разработке. Этот справочник не подтверждает подключение банковской оплаты или автоматического продления.","related":["trial","permissions"]},

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
const CACHE='sun-catering-pwa-v108-20260918-sidebar-cleanup'; const CACHE='sun-catering-pwa-v109-20260918-mobile-calendar';
const VERSION='20260918-sidebar-cleanup'; const VERSION='20260918-mobile-calendar';
const CORE=[ const CORE=[
'./vendor/supabase-2.112.4.min.js', './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}`, `./core/help-center.js?v=${VERSION}`,`./core/help-center.css?v=${VERSION}`,`./help/knowledge-v1.json?v=${VERSION}`,

View File

@ -1,6 +1,22 @@
import fs from 'node:fs'; import fs from 'node:fs';
import {test,expect} from '@playwright/test'; 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})=>{ 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.addInitScript(()=>localStorage.setItem('sunLocalOnlyModeV1','1'));
await page.route('**://api.caterium.ru/**',r=>r.abort()); await page.route('**://api.caterium.ru/**',r=>r.abort());

View File

@ -1,6 +1,34 @@
import {test,expect} from '@playwright/test'; import {test,expect} from '@playwright/test';
import {bootCalendar,printCalendar} from './calendar-fixture.mjs'; 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})=>{ 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 dates=await bootCalendar(page,context);
const day=i=>page.locator(`.cal-day[data-date="${dates[i]}"]`); const day=i=>page.locator(`.cal-day[data-date="${dates[i]}"]`);

View File

@ -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}, webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
projects:[ projects:[
{name:'iphone-pdf',testMatch:['proposal-quality.spec.mjs'],grep:/all six selections|transparent wide|an actual offer downloads/,use:{...devices['iPhone 13'],serviceWorkers:'block'}}, {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:'desktop',use:{...devices['Desktop Chrome']}},
{name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}} {name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}}
] ]

View File

@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent')
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer'); check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox'); check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4'); check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
check(sw.includes('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(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-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(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('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned'); check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded'); check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match'); check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json'); check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
check(releaseManifest.channel==='production','release manifest channel is production'); check(releaseManifest.channel==='production','release manifest channel is production');
check(String(releaseManifest.pwaCache||'').includes('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(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries'); check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer'); check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes'); check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
check(pkg.version==='17.7.3','package version is v17.7.3'); check(pkg.version==='17.7.3','package version is v17.7.3');
check(index.includes('20260918-sidebar-cleanup'),'index cache bust is v17.7.3'); check(index.includes('20260918-mobile-calendar'),'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(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(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active'); check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');

View File

@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos'); if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x)); const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed'); if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
if(!sw.includes('20260918-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(!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-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(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('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present'); if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed'); if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');