Recover login from empty API responses with one authenticated session
This commit is contained in:
parent
1ae56fb57b
commit
d4e02cab8b
9
docs/LOGIN-RECOVERY-20260918.md
Normal file
9
docs/LOGIN-RECOVERY-20260918.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Recover login when the API proxy returns no data
|
||||||
|
|
||||||
|
The API proxy intermittently returned HTTP 200 with an empty HTML response for authentication and workspace membership requests. The same Supabase backend returned a valid session and the existing workspace directly. The former membership loader treated malformed responses like missing workspaces, while an existing login gate kept its loading message. A second authentication client used by the fallback also shared the same session storage key.
|
||||||
|
|
||||||
|
The shared transport now validates JSON responses and uses the same backend directly for failed reads and password login. Authorization and the single SDK session are preserved. Only GET/HEAD and explicitly allowlisted read RPCs can be retried, plus password login. Database writes are not replayed. Both channels have bounded waits; permission and credential errors are returned without fallback.
|
||||||
|
|
||||||
|
Membership requests for the same account share one in-flight operation. Late responses from a different account are ignored. Missing RPC compatibility is used only for a missing function, not network errors. A failed membership request produces a retry/exit screen and cannot be mistaken for a developer account with no company. Successful login still opens the normal application.
|
||||||
|
|
||||||
|
Regression coverage includes empty/invalid proxy responses, identical request bodies and authorization on fallback, non-replayed writes, credential rejection, concurrent membership loads, recovery UI, and a real Supabase SDK login against controlled endpoint responses. The full flow is checked on desktop and mobile without changing production credentials or authentication requirements.
|
||||||
@ -11,7 +11,7 @@
|
|||||||
"serverReady": true,
|
"serverReady": true,
|
||||||
"workspaceAutoDiscovery": true,
|
"workspaceAutoDiscovery": true,
|
||||||
"invitesTemporarilyDisabled": false,
|
"invitesTemporarilyDisabled": false,
|
||||||
"pwaCache": "v89-20260917-calendar-compact",
|
"pwaCache": "v90-20260918-login-recovery",
|
||||||
"fullOfferDescriptions": true,
|
"fullOfferDescriptions": true,
|
||||||
"dynamicOfferRows": true,
|
"dynamicOfferRows": true,
|
||||||
"pdfOfferDescriptionFix": true,
|
"pdfOfferDescriptionFix": true,
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
"version": "17.7.3",
|
"version": "17.7.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js",
|
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js",
|
||||||
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs",
|
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs",
|
||||||
"check:release": "node tests/release-check.mjs",
|
"check:release": "node tests/release-check.mjs",
|
||||||
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db",
|
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db",
|
||||||
|
|||||||
@ -2113,23 +2113,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const DEFAULT_SUPABASE_KEY = 'sb_publishable_CAxfhMKrduJjuk_5ybCQLg_TqSGWGoy';
|
const DEFAULT_SUPABASE_KEY = 'sb_publishable_CAxfhMKrduJjuk_5ybCQLg_TqSGWGoy';
|
||||||
const SUPABASE_API_PROXY = 'https://api.caterium.ru';
|
const SUPABASE_API_PROXY = 'https://api.caterium.ru';
|
||||||
const PROXY_FETCH_TIMEOUT_MS = 7000;
|
const PROXY_FETCH_TIMEOUT_MS = 7000;
|
||||||
function supabaseProxyFetch(input, init) {
|
const supabaseProxyFetch=window.CateriumCloudTransport.create({upstream:DEFAULT_SUPABASE_URL,proxy:SUPABASE_API_PROXY,timeout:PROXY_FETCH_TIMEOUT_MS});
|
||||||
try {
|
|
||||||
const url = typeof input === 'string' ? input : input?.url;
|
|
||||||
if (url && url.indexOf(DEFAULT_SUPABASE_URL) === 0) {
|
|
||||||
const proxied = SUPABASE_API_PROXY + url.slice(DEFAULT_SUPABASE_URL.length);
|
|
||||||
input = typeof input === 'string' ? proxied : new Request(proxied, input);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timer = setTimeout(() => controller.abort(), PROXY_FETCH_TIMEOUT_MS);
|
|
||||||
const externalSignal = init?.signal;
|
|
||||||
if (externalSignal) {
|
|
||||||
if (externalSignal.aborted) controller.abort();
|
|
||||||
else externalSignal.addEventListener('abort', () => controller.abort(), {once:true});
|
|
||||||
}
|
|
||||||
return fetch(input, {...init, signal: controller.signal}).finally(() => clearTimeout(timer));
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = loadConfig();
|
let config = loadConfig();
|
||||||
let client = null;
|
let client = null;
|
||||||
@ -2140,6 +2124,8 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
let supportMode = null;
|
let supportMode = null;
|
||||||
let membershipsLoading = false;
|
let membershipsLoading = false;
|
||||||
let membershipsLoaded = false;
|
let membershipsLoaded = false;
|
||||||
|
let membershipError = '';
|
||||||
|
let membershipLoad = null;
|
||||||
let realtimeChannel = null;
|
let realtimeChannel = null;
|
||||||
let syncTimer = null;
|
let syncTimer = null;
|
||||||
let isSyncing = false;
|
let isSyncing = false;
|
||||||
@ -3123,7 +3109,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const signature=`${config.url}|${config.key}`;
|
const signature=`${config.url}|${config.key}`;
|
||||||
if(!client||signature!==clientSignature){
|
if(!client||signature!==clientSignature){
|
||||||
client=window.supabase.createClient(config.url,config.key,{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:true},global:{fetch:supabaseProxyFetch}});clientSignature=signature;
|
client=window.supabase.createClient(config.url,config.key,{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:true},global:{fetch:supabaseProxyFetch}});clientSignature=signature;
|
||||||
client.auth.onAuthStateChange((_event,next)=>{if(session?.user?.id!==next?.user?.id)window.CateriumBranding?.resetSidebar();session=next;membershipsLoading=Boolean(next?.user);membershipsLoaded=!next?.user;setTimeout(async()=>{try{if(!next?.user){const sw=await switchTenantLocal('');if(sw.changed){location.reload();return;}}else{const sw=await loadMemberships();if(sw){location.reload();return;}}}finally{renderCloudUI();updatePill();}},0);});
|
client.auth.onAuthStateChange((_event,next)=>{const changed=session?.user?.id!==next?.user?.id;if(changed)window.CateriumBranding?.resetSidebar();session=next;if(!changed&&(membershipsLoading||(membershipsLoaded&&!membershipError)))return;membershipsLoading=Boolean(next?.user);membershipsLoaded=!next?.user;setTimeout(async()=>{try{if(!next?.user){const sw=await switchTenantLocal('');if(sw.changed){location.reload();return;}}else{const sw=await loadMemberships();if(sw){location.reload();return;}}}finally{renderCloudUI();updatePill();}},0);});
|
||||||
}
|
}
|
||||||
const {data,error}=await client.auth.getSession();if(error)throw error;session=data.session||null;
|
const {data,error}=await client.auth.getSession();if(error)throw error;session=data.session||null;
|
||||||
const tenantSwitched=session?await loadMemberships():(await switchTenantLocal('')).changed;
|
const tenantSwitched=session?await loadMemberships():(await switchTenantLocal('')).changed;
|
||||||
@ -3158,18 +3144,26 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
}catch(error){handleError(error,signUp?'Не удалось создать аккаунт.':'Не удалось войти.');}
|
}catch(error){handleError(error,signUp?'Не удалось создать аккаунт.':'Не удалось войти.');}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMemberships() {
|
function loadMemberships() {
|
||||||
memberships=[];workspace=null;membershipsLoaded=false;
|
const userId=session?.user?.id||'';
|
||||||
if(!client||!session?.user)return false;
|
if(membershipLoad?.userId===userId)return membershipLoad.promise;
|
||||||
|
if(!client||!userId){memberships=[];workspace=null;membershipsLoading=false;membershipsLoaded=true;membershipError='';return Promise.resolve(false)}
|
||||||
|
const task={userId,promise:null};membershipLoad=task;
|
||||||
|
task.promise=runMembershipLoad(task).finally(()=>{if(membershipLoad!==task)return;membershipLoad=null;membershipsLoading=false;membershipsLoaded=true;try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:clone(workspace)}}))}catch(_){}});
|
||||||
|
return task.promise;
|
||||||
|
}
|
||||||
|
async function runMembershipLoad(task) {
|
||||||
|
const activeClient=client,current=()=>membershipLoad===task&&client===activeClient&&session?.user?.id===task.userId;
|
||||||
|
memberships=[];workspace=null;membershipsLoaded=false;membershipError='';
|
||||||
membershipsLoading=true;
|
membershipsLoading=true;
|
||||||
try{
|
try{
|
||||||
let rows=[];
|
let rows=[];
|
||||||
const rpc=await sunCloudAwait(client.rpc('sun_my_workspaces'),'Загрузка рабочей базы');
|
const rpc=await sunCloudAwait(client.rpc('sun_my_workspaces'),'Загрузка рабочей базы');
|
||||||
if(!rpc.error&&Array.isArray(rpc.data)){
|
if(!rpc.error&&Array.isArray(rpc.data)){
|
||||||
rows=rpc.data.map(x=>({workspace_id:x.id,name:x.name,role:x.role,display_name:x.display_name,is_active:x.is_active,permissions:x.permissions}));
|
rows=rpc.data.map(x=>({workspace_id:x.id,name:x.name,role:x.role,display_name:x.display_name,is_active:x.is_active,permissions:x.permissions}));
|
||||||
}else{
|
}else if(rpc.error&&['PGRST202','42883'].includes(rpc.error.code)){
|
||||||
// Compatibility fallback for an older server schema.
|
// Compatibility fallback for an older server schema.
|
||||||
const direct=await sunCloudAwait(client.from('sun_workspace_members').select('workspace_id,role,display_name,is_active,permissions').eq('user_id',session.user.id),'Загрузка рабочей базы');
|
const direct=await sunCloudAwait(activeClient.from('sun_workspace_members').select('workspace_id,role,display_name,is_active,permissions').eq('user_id',task.userId),'Загрузка рабочей базы');
|
||||||
if(direct.error)throw rpc.error||direct.error;
|
if(direct.error)throw rpc.error||direct.error;
|
||||||
const active=(direct.data||[]).filter(x=>x.is_active!==false);
|
const active=(direct.data||[]).filter(x=>x.is_active!==false);
|
||||||
if(active.length){
|
if(active.length){
|
||||||
@ -3177,21 +3171,24 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const w=await sunCloudAwait(client.from('sun_workspaces').select('id,name').in('id',ids),'Загрузка рабочей базы');if(w.error)throw w.error;(w.data||[]).forEach(x=>names[x.id]=x.name);
|
const w=await sunCloudAwait(client.from('sun_workspaces').select('id,name').in('id',ids),'Загрузка рабочей базы');if(w.error)throw w.error;(w.data||[]).forEach(x=>names[x.id]=x.name);
|
||||||
rows=active.map(x=>({...x,name:names[x.workspace_id]||'Рабочая база'}));
|
rows=active.map(x=>({...x,name:names[x.workspace_id]||'Рабочая база'}));
|
||||||
}
|
}
|
||||||
}
|
}else throw rpc.error||new Error('Не удалось прочитать список компаний. Повторите загрузку.');
|
||||||
|
if(!current())return false;
|
||||||
memberships=rows.filter(x=>x.is_active!==false).map(x=>({id:x.workspace_id,name:x.name||'Рабочая база',role:x.role,display_name:x.display_name||'',is_active:x.is_active!==false,permissions:x.permissions||{}}));
|
memberships=rows.filter(x=>x.is_active!==false).map(x=>({id:x.workspace_id,name:x.name||'Рабочая база',role:x.role,display_name:x.display_name||'',is_active:x.is_active!==false,permissions:x.permissions||{}}));
|
||||||
if(memberships.length&&!memberships.some(x=>x.id===config.workspaceId))config.workspaceId=memberships[0].id;
|
if(memberships.length&&!memberships.some(x=>x.id===config.workspaceId))config.workspaceId=memberships[0].id;
|
||||||
if(!memberships.length)config.workspaceId='';
|
if(!memberships.length)config.workspaceId='';
|
||||||
saveConfig();selectWorkspace();
|
saveConfig();selectWorkspace();
|
||||||
const switched=await switchTenantLocal(workspace?.id||'');
|
const switched=await switchTenantLocal(workspace?.id||'');
|
||||||
|
if(!current())return false;
|
||||||
membershipsLoaded=true;
|
membershipsLoaded=true;
|
||||||
if(switched.changed)return true;
|
if(switched.changed)return true;
|
||||||
try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:clone(workspace),user:clone(session.user)}}));}catch(_){}
|
try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:clone(workspace),user:clone(session.user)}}));}catch(_){}
|
||||||
return false;
|
return false;
|
||||||
}catch(error){
|
}catch(error){
|
||||||
membershipsLoaded=true;
|
if(!current())return false;
|
||||||
setStatus('error',error?.message||'Не удалось загрузить рабочую базу.');
|
membershipError=error?.message||'Не удалось загрузить рабочую базу.';
|
||||||
|
setStatus('error',membershipError);
|
||||||
return false;
|
return false;
|
||||||
}finally{membershipsLoading=false;}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectWorkspace(){workspace=memberships.find(x=>x.id===config.workspaceId)||memberships[0]||null;if(workspace&&config.workspaceId!==workspace.id){config.workspaceId=workspace.id;saveConfig();}}
|
function selectWorkspace(){workspace=memberships.find(x=>x.id===config.workspaceId)||memberships[0]||null;if(workspace&&config.workspaceId!==workspace.id){config.workspaceId=workspace.id;saveConfig();}}
|
||||||
@ -3360,7 +3357,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
setAutoSyncDeveloper,
|
setAutoSyncDeveloper,
|
||||||
snapshotLocal:()=>collectLocalPayload(),
|
snapshotLocal:()=>collectLocalPayload(),
|
||||||
reloadMemberships:async()=>{await loadMemberships();renderCloudUI();return workspace?clone(workspace):null;},
|
reloadMemberships:async()=>{await loadMemberships();renderCloudUI();return workspace?clone(workspace):null;},
|
||||||
status:()=>({connected:Boolean(client),signedIn:Boolean(session),supportMode:supportMode?clone(supportMode):null,workspace:workspace?clone(workspace):null,membershipsLoading,membershipsLoaded,membershipCount:memberships.length,dirty,lastStatus,lastError})
|
status:()=>({connected:Boolean(client),signedIn:Boolean(session),supportMode:supportMode?clone(supportMode):null,workspace:workspace?clone(workspace):null,membershipsLoading,membershipsLoaded,membershipError,membershipCount:memberships.length,dirty,lastStatus,lastError})
|
||||||
};
|
};
|
||||||
|
|
||||||
async function boot(){
|
async function boot(){
|
||||||
@ -3761,9 +3758,17 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
}
|
}
|
||||||
if(st.signedIn&&ws){$('sunCloudAuthGateV3')?.remove();document.body.classList.remove('sun-cloud-auth-required');try{localStorage.removeItem(LOCAL_ONLY_KEY)}catch(_){}return;}
|
if(st.signedIn&&ws){$('sunCloudAuthGateV3')?.remove();document.body.classList.remove('sun-cloud-auth-required');try{localStorage.removeItem(LOCAL_ONLY_KEY)}catch(_){}return;}
|
||||||
if(!st.signedIn&&window.CateriumAccessPolicy?.emergencyLocalActive()){$('sunCloudAuthGateV3')?.remove();document.body.classList.remove('sun-cloud-auth-required');return;}
|
if(!st.signedIn&&window.CateriumAccessPolicy?.emergencyLocalActive()){$('sunCloudAuthGateV3')?.remove();document.body.classList.remove('sun-cloud-auth-required');return;}
|
||||||
if($('sunCloudAuthGateV3')){if(st.signedIn&&st.membershipsLoading){const e=$('sunGateErrorV3');if(e)e.textContent='Загружаю рабочую базу…';}return;}
|
const gateState=st.signedIn?(st.membershipsLoading?'loading':st.membershipError?'error':'missing'):'login';
|
||||||
|
const currentGate=$('sunCloudAuthGateV3');
|
||||||
|
if(currentGate){if(st.signedIn&¤tGate.dataset.authState!==gateState)currentGate.remove();else{if(st.signedIn){const e=$('sunGateErrorV3');if(e)e.textContent=st.membershipsLoading?'Загружаю рабочую базу…':st.membershipError||e.textContent;}return;}}
|
||||||
document.body.classList.add('sun-cloud-auth-required');
|
document.body.classList.add('sun-cloud-auth-required');
|
||||||
const gate=document.createElement('div');gate.id='sunCloudAuthGateV3';gate.className='sun-cloud-auth-gate';
|
const gate=document.createElement('div');gate.id='sunCloudAuthGateV3';gate.className='sun-cloud-auth-gate';gate.dataset.authState=gateState;
|
||||||
|
if(st.signedIn&&st.membershipError){
|
||||||
|
gate.innerHTML=`<div class="sun-cloud-auth-card"><h2>Не удалось загрузить рабочую базу</h2><p class="hint">${esc(ss?.user?.email||'Аккаунт авторизован')}</p><p class="hint">Вход выполнен, но сервер не вернул данные компании. Повторите загрузку.</p><div class="sun-cloud-auth-error" id="sunGateErrorV3">${esc(st.membershipError)}</div><div class="sun-cloud-auth-actions"><button class="primary" id="sunGateRetryWorkspaceV3" type="button">Повторить загрузку</button><button class="outline" id="sunGateSignOutV3" type="button">Выйти</button></div></div>`;
|
||||||
|
document.body.appendChild(gate);
|
||||||
|
gate.querySelector('#sunGateRetryWorkspaceV3').onclick=async()=>{const button=gate.querySelector('#sunGateRetryWorkspaceV3');button.disabled=true;try{const found=await cloud()?.reloadMemberships?.();if(found){location.reload();return;}}finally{gate.remove();ensureAuthGate();}};
|
||||||
|
gate.querySelector('#sunGateSignOutV3').onclick=()=>cloud()?.signOut?.();return;
|
||||||
|
}
|
||||||
if(st.signedIn&&ss?.user&&!ws){
|
if(st.signedIn&&ss?.user&&!ws){
|
||||||
const pending=getPendingRegistration(ss.user.email||'');
|
const pending=getPendingRegistration(ss.user.email||'');
|
||||||
gate.innerHTML=`<div class="sun-cloud-auth-card"><div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>${pending?'Создаю компанию':st.membershipsLoading?'Загружаю рабочую базу':'Аккаунт готов'}</h2><div class="hint">${esc(ss.user.email||'Аккаунт авторизован')}</div></div></div><p class="hint">${pending?'Регистрация завершена. Сейчас подготовим вашу рабочую компанию.':st.membershipsLoading?'Проверяю доступы этого аккаунта на сервере.':'У этого аккаунта пока нет компании. Укажите название, чтобы создать её.'}</p>${!pending&&!st.membershipsLoading?'<label>Название компании<input id="sunGateRecoveryCompanyV25" autocomplete="organization" value="Моя компания"></label>':''}<div class="sun-cloud-auth-actions"><button class="primary" id="sunGateRetryWorkspaceV3" type="button">${pending?'Продолжить':st.membershipsLoading?'Проверить ещё раз':'Создать компанию'}</button><button class="outline" id="sunGateSignOutV3" type="button">Выйти</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3">${pending?'Подготавливаю компанию…':st.membershipsLoading?'Загружаю рабочую базу…':''}</div></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>${pending?'Создаю компанию':st.membershipsLoading?'Загружаю рабочую базу':'Аккаунт готов'}</h2><div class="hint">${esc(ss.user.email||'Аккаунт авторизован')}</div></div></div><p class="hint">${pending?'Регистрация завершена. Сейчас подготовим вашу рабочую компанию.':st.membershipsLoading?'Проверяю доступы этого аккаунта на сервере.':'У этого аккаунта пока нет компании. Укажите название, чтобы создать её.'}</p>${!pending&&!st.membershipsLoading?'<label>Название компании<input id="sunGateRecoveryCompanyV25" autocomplete="organization" value="Моя компания"></label>':''}<div class="sun-cloud-auth-actions"><button class="primary" id="sunGateRetryWorkspaceV3" type="button">${pending?'Продолжить':st.membershipsLoading?'Проверить ещё раз':'Создать компанию'}</button><button class="outline" id="sunGateSignOutV3" type="button">Выйти</button></div><div class="sun-cloud-auth-error" id="sunGateErrorV3">${pending?'Подготавливаю компанию…':st.membershipsLoading?'Загружаю рабочую базу…':''}</div></div>`;
|
||||||
@ -4688,7 +4693,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
function seedWorkspace(){clearWorkspaceLocalData();localStorage.setItem('sunBoxes','[]');localStorage.setItem('sunOrders','[]');}
|
function seedWorkspace(){clearWorkspaceLocalData();localStorage.setItem('sunBoxes','[]');localStorage.setItem('sunOrders','[]');}
|
||||||
|
|
||||||
function enhanceOnboardingGate(){
|
function enhanceOnboardingGate(){
|
||||||
const gate=$('sunCloudAuthGateV3'),ss=session(),ws=workspace(),st=cloud()?.status?.();if(window.SunDeveloperV22?.isPlatformAdmin?.())return;if(!gate||!ss?.user||ws||st?.membershipsLoading||st?.membershipsLoaded===false||gate.dataset.saasEnhanced==='1')return;
|
const gate=$('sunCloudAuthGateV3'),ss=session(),ws=workspace(),st=cloud()?.status?.();if(window.SunDeveloperV22?.isPlatformAdmin?.())return;if(!gate||!ss?.user||ws||st?.membershipsLoading||st?.membershipError||st?.membershipsLoaded===false||gate.dataset.saasEnhanced==='1')return;
|
||||||
gate.dataset.saasEnhanced='1';const card=gate.querySelector('.sun-cloud-auth-card');if(!card)return;
|
gate.dataset.saasEnhanced='1';const card=gate.querySelector('.sun-cloud-auth-card');if(!card)return;
|
||||||
card.innerHTML=`<div class="sun-cloud-auth-brand"><img src="caterium-mark-light.svg" alt=""><div><h2>Создайте компанию</h2><div class="hint">${esc(ss.user.email||'Аккаунт авторизован')} · 14 дней Полного тарифа бесплатно</div></div></div><p class="hint">У этого аккаунта пока нет рабочей базы. Приглашения временно отключены на период локального тестирования.</p><label>Название компании<input id="sunSaaSCompanyNameV16" value="Моя кейтеринговая компания" autocomplete="organization"></label><div class="sun-saas-onboarding-tabs"><button class="sun-saas-onboarding-choice" type="button" data-create="empty"><b>Новая компания</b><small>Пустой каталог, без заказов и клиентов. Добавьте свои блюда, боксы и фотографии.</small></button></div><div class="sun-cloud-auth-actions" style="margin-top:14px"><button class="outline" type="button" data-signout>Выйти</button></div><div class="sun-cloud-auth-error" id="sunSaaSGateErrorV16"></div>`;
|
card.innerHTML=`<div class="sun-cloud-auth-brand"><img src="caterium-mark-light.svg" alt=""><div><h2>Создайте компанию</h2><div class="hint">${esc(ss.user.email||'Аккаунт авторизован')} · 14 дней Полного тарифа бесплатно</div></div></div><p class="hint">У этого аккаунта пока нет рабочей базы. Приглашения временно отключены на период локального тестирования.</p><label>Название компании<input id="sunSaaSCompanyNameV16" value="Моя кейтеринговая компания" autocomplete="organization"></label><div class="sun-saas-onboarding-tabs"><button class="sun-saas-onboarding-choice" type="button" data-create="empty"><b>Новая компания</b><small>Пустой каталог, без заказов и клиентов. Добавьте свои блюда, боксы и фотографии.</small></button></div><div class="sun-cloud-auth-actions" style="margin-top:14px"><button class="outline" type="button" data-signout>Выйти</button></div><div class="sun-cloud-auth-error" id="sunSaaSGateErrorV16"></div>`;
|
||||||
const err=$('sunSaaSGateErrorV16');
|
const err=$('sunSaaSGateErrorV16');
|
||||||
@ -5359,7 +5364,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
|
|
||||||
async function renderJournal(body){const [activity,errors]=await Promise.all([rpc('sun_dev_list_activity',{p_limit:150}),rpc('sun_dev_list_errors',{p_workspace:null,p_limit:100})]);body.innerHTML=`<div class="sun-dev-section-grid"><div class="sun-dev-card" style="overflow:auto"><h2>Действия разработчика</h2><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Дата</th><th>Действие</th><th>Компания / аккаунт</th><th>Детали</th></tr></thead><tbody>${(activity||[]).map(a=>`<tr><td>${fmtDate(a.created_at)}</td><td><b>${esc(a.action)}</b><div class="sun-dev-muted">${esc(a.actor_email||'')}</div></td><td>${esc(a.workspace_name||a.target_email||'—')}</td><td class="sun-dev-muted">${esc(JSON.stringify(a.details||{}))}</td></tr>`).join('')||'<tr><td colspan="4">Записей пока нет.</td></tr>'}</tbody></table></div></div><div class="sun-dev-card" style="overflow:auto"><h2>Ошибки приложения</h2><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Дата</th><th>Компания</th><th>Версия</th><th>Ошибка</th></tr></thead><tbody>${(errors||[]).map(e=>`<tr><td>${fmtDate(e.created_at)}</td><td>${esc(e.workspace_name||'—')}</td><td>${esc(e.app_version||'—')}</td><td><b>${esc(e.level||'error')}</b><div>${esc(e.message||'')}</div></td></tr>`).join('')||'<tr><td colspan="4">Ошибок нет.</td></tr>'}</tbody></table></div></div></div>`}
|
async function renderJournal(body){const [activity,errors]=await Promise.all([rpc('sun_dev_list_activity',{p_limit:150}),rpc('sun_dev_list_errors',{p_workspace:null,p_limit:100})]);body.innerHTML=`<div class="sun-dev-section-grid"><div class="sun-dev-card" style="overflow:auto"><h2>Действия разработчика</h2><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Дата</th><th>Действие</th><th>Компания / аккаунт</th><th>Детали</th></tr></thead><tbody>${(activity||[]).map(a=>`<tr><td>${fmtDate(a.created_at)}</td><td><b>${esc(a.action)}</b><div class="sun-dev-muted">${esc(a.actor_email||'')}</div></td><td>${esc(a.workspace_name||a.target_email||'—')}</td><td class="sun-dev-muted">${esc(JSON.stringify(a.details||{}))}</td></tr>`).join('')||'<tr><td colspan="4">Записей пока нет.</td></tr>'}</tbody></table></div></div><div class="sun-dev-card" style="overflow:auto"><h2>Ошибки приложения</h2><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Дата</th><th>Компания</th><th>Версия</th><th>Ошибка</th></tr></thead><tbody>${(errors||[]).map(e=>`<tr><td>${fmtDate(e.created_at)}</td><td>${esc(e.workspace_name||'—')}</td><td>${esc(e.app_version||'—')}</td><td><b>${esc(e.level||'error')}</b><div>${esc(e.message||'')}</div></td></tr>`).join('')||'<tr><td colspan="4">Ошибок нет.</td></tr>'}</tbody></table></div></div></div>`}
|
||||||
|
|
||||||
function enhanceDeveloperGate(){const gate=$('sunCloudAuthGateV3'),ss=session(),ws=cloud()?.getWorkspace?.(),st=cloud()?.status?.();if(!platformAdmin||!gate||!ss?.user||ws||st?.membershipsLoading||st?.membershipsLoaded===false)return;const card=gate.querySelector('.sun-cloud-auth-card');if(!card||card.dataset.devEnhanced==='1')return;card.dataset.devEnhanced='1';gate.dataset.saasEnhanced='1';card.innerHTML=`<div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Аккаунт разработчика</h2><div class="hint">${esc(ss.user.email||'')}</div></div></div><p class="hint">Этот аккаунт управляет платформой и не обязан иметь собственную рабочую компанию.</p><div class="sun-cloud-auth-actions"><button class="primary" type="button" data-open-dev>Открыть кабинет разработчика</button><button class="outline" type="button" data-signout>Выйти</button></div>`;card.querySelector('[data-open-dev]').onclick=()=>open();card.querySelector('[data-signout]').onclick=()=>cloud()?.signOut?.()}
|
function enhanceDeveloperGate(){const gate=$('sunCloudAuthGateV3'),ss=session(),ws=cloud()?.getWorkspace?.(),st=cloud()?.status?.();if(!platformAdmin||!gate||!ss?.user||ws||st?.membershipsLoading||st?.membershipError||st?.membershipsLoaded===false)return;const card=gate.querySelector('.sun-cloud-auth-card');if(!card||card.dataset.devEnhanced==='1')return;card.dataset.devEnhanced='1';gate.dataset.saasEnhanced='1';card.innerHTML=`<div class="sun-cloud-auth-brand"><img src="caterium-login-logo.png" alt="Caterium"><div><h2>Аккаунт разработчика</h2><div class="hint">${esc(ss.user.email||'')}</div></div></div><p class="hint">Этот аккаунт управляет платформой и не обязан иметь собственную рабочую компанию.</p><div class="sun-cloud-auth-actions"><button class="primary" type="button" data-open-dev>Открыть кабинет разработчика</button><button class="outline" type="button" data-signout>Выйти</button></div>`;card.querySelector('[data-open-dev]').onclick=()=>open();card.querySelector('[data-signout]').onclick=()=>cloud()?.signOut?.()}
|
||||||
function syncChrome(){if(platformAdmin){ensureNav();enhanceDeveloperGate()}else{$('sunDeveloperNavV22')?.remove()}ensureSupportBanner()}
|
function syncChrome(){if(platformAdmin){ensureNav();enhanceDeveloperGate()}else{$('sunDeveloperNavV22')?.remove()}ensureSupportBanner()}
|
||||||
function boot(){installStyle();let tries=0;const timer=setInterval(async()=>{tries++;if(document.hidden)return;if(client()&&session()?.user){await checkPlatformAdmin(false);enhanceDeveloperGate();ensureSupportBanner()}else if(!session()?.user){platformAdmin=false;syncChrome()}if(tries>40)clearInterval(timer)},3000);window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>checkPlatformAdmin(true),120));window.addEventListener('sun:cloud-state-applied',()=>ensureSupportBanner());document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;if(String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки'&&platformAdmin)setTimeout(()=>{$('sunSaaSSettingsCardV16')?.querySelector('[data-saas-admin]')?.remove()},120)},true);setTimeout(()=>checkPlatformAdmin(true),900)}
|
function boot(){installStyle();let tries=0;const timer=setInterval(async()=>{tries++;if(document.hidden)return;if(client()&&session()?.user){await checkPlatformAdmin(false);enhanceDeveloperGate();ensureSupportBanner()}else if(!session()?.user){platformAdmin=false;syncChrome()}if(tries>40)clearInterval(timer)},3000);window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>checkPlatformAdmin(true),120));window.addEventListener('sun:cloud-state-applied',()=>ensureSupportBanner());document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;if(String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки'&&platformAdmin)setTimeout(()=>{$('sunSaaSSettingsCardV16')?.querySelector('[data-saas-admin]')?.remove()},120)},true);setTimeout(()=>checkPlatformAdmin(true),900)}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
(()=>{
|
(()=>{
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const VERSION='17.8.4-auth-proxy-fallback';
|
const VERSION='17.8.5-single-session-login';
|
||||||
const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23';
|
const PENDING_REGISTRATION_KEY='sunPendingRegistrationV23';
|
||||||
const DIRECT_SUPABASE_URL='https://usfjwhztqoopzzfmfbis.supabase.co';
|
let busy=false;
|
||||||
const DIRECT_SUPABASE_KEY='sb_publishable_CAxfhMKrduJjuk_5ybCQLg_TqSGWGoy';
|
|
||||||
let busy=false,directClient=null;
|
|
||||||
|
|
||||||
const $=id=>document.getElementById(id);
|
const $=id=>document.getElementById(id);
|
||||||
const cloud=()=>window.SunCloudV2||null;
|
const cloud=()=>window.SunCloudV2||null;
|
||||||
const client=()=>cloud()?.getClient?.()||null;
|
const client=()=>cloud()?.getClient?.()||null;
|
||||||
const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??'');
|
const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??'');
|
||||||
|
|
||||||
function directAuthClient(){if(directClient)return directClient;try{if(!window.supabase?.createClient)return null;directClient=window.supabase.createClient(DIRECT_SUPABASE_URL,DIRECT_SUPABASE_KEY,{auth:{persistSession:true,autoRefreshToken:true,detectSessionInUrl:true}});return directClient}catch(_){return null}}
|
|
||||||
function isProxyServerError(error){const text=`${error?.message||error||''} ${error?.status||''} ${error?.statusCode||''}`;return /(?:http\s*)?500|internal server error|failed to fetch|networkerror|load failed/i.test(text)}
|
|
||||||
async function signInWithFallback(c,email,password){let first=null;try{first=await c.auth.signInWithPassword({email,password})}catch(error){if(!isProxyServerError(error))throw error;first={error}}if(first?.data?.session)return{authClient:c,result:first};if(first?.error&&!isProxyServerError(first.error))throw first.error;const fallback=directAuthClient();if(!fallback)throw first?.error||new Error('Сервис входа временно недоступен.');const second=await fallback.auth.signInWithPassword({email,password});if(second.error)throw second.error;return{authClient:fallback,result:second}}
|
|
||||||
|
|
||||||
function redirectUrl(){try{const u=new URL(location.href);u.hash='';return u.toString();}catch(_){return location.href.split('#')[0];}}
|
function redirectUrl(){try{const u=new URL(location.href);u.hash='';return u.toString();}catch(_){return location.href.split('#')[0];}}
|
||||||
function savePendingRegistration(email,promoCode){try{localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({email:String(email||'').trim().toLowerCase(),companyName:'Новая компания',promoCode:String(promoCode||'').trim().toUpperCase(),createdAt:new Date().toISOString()}));}catch(_){}}
|
function savePendingRegistration(email,promoCode){try{localStorage.setItem(PENDING_REGISTRATION_KEY,JSON.stringify({email:String(email||'').trim().toLowerCase(),companyName:'Новая компания',promoCode:String(promoCode||'').trim().toUpperCase(),createdAt:new Date().toISOString()}));}catch(_){}}
|
||||||
@ -46,10 +41,10 @@
|
|||||||
if(!email||password.length<6){setError(gate,'Введите email и пароль минимум из 6 символов.');return;}
|
if(!email||password.length<6){setError(gate,'Введите email и пароль минимум из 6 символов.');return;}
|
||||||
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 signed=await signInWithFallback(c,email,password),result=signed.result,authClient=signed.authClient;
|
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 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 authClient.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,authClient===c?'Вход выполнен. Открываю Caterium…':'Вход выполнен через резервный канал. Открываю Caterium…');
|
setError(gate,'Вход выполнен. Открываю Caterium…');
|
||||||
setTimeout(()=>location.reload(),120);
|
setTimeout(()=>location.reload(),120);
|
||||||
}catch(error){setError(gate,String(error?.message||error||'Не удалось войти.'));if(button)button.disabled=false;busy=false;}
|
}catch(error){setError(gate,String(error?.message||error||'Не удалось войти.'));if(button)button.disabled=false;busy=false;}
|
||||||
}
|
}
|
||||||
@ -110,4 +105,4 @@
|
|||||||
const init=()=>{neutralizeLegacyCompanyUI();setTimeout(()=>{const gate=$('sunCloudAuthGateV3');if(gate&&pendingRegistration())finishOwnerOnboarding(gate);},500)};
|
const init=()=>{neutralizeLegacyCompanyUI();setTimeout(()=>{const gate=$('sunCloudAuthGateV3');if(gate&&pendingRegistration())finishOwnerOnboarding(gate);},500)};
|
||||||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
|
||||||
window.CateriumAuthSecurityV1774=Object.freeze({VERSION,redirectUrl,ensureRegistrationFields});
|
window.CateriumAuthSecurityV1774=Object.freeze({VERSION,redirectUrl,ensureRegistrationFields});
|
||||||
})();
|
})();
|
||||||
|
|||||||
35
public/core/cloud-transport.js
Normal file
35
public/core/cloud-transport.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
(()=>{
|
||||||
|
'use strict';
|
||||||
|
const READ_RPCS=new Set(['sun_my_workspaces','sun_fetch_app_state','sun_is_platform_admin']);
|
||||||
|
function create({upstream,proxy,timeout=7000,fallbackTimeout=4000}){
|
||||||
|
const origin=new URL(upstream).origin;
|
||||||
|
return async function(input,init={}){
|
||||||
|
const original=new Request(input,init),url=new URL(original.url),isBackend=url.origin===origin;
|
||||||
|
const read=original.method==='GET'||original.method==='HEAD'||(original.method==='POST'&&url.pathname.startsWith('/rest/v1/rpc/')&&READ_RPCS.has(url.pathname.slice('/rest/v1/rpc/'.length)));
|
||||||
|
const passwordLogin=original.method==='POST'&&url.pathname==='/auth/v1/token'&&url.searchParams.get('grant_type')==='password';
|
||||||
|
const safeFallback=isBackend&&(read||passwordLogin);
|
||||||
|
const expectJson=original.method!=='HEAD'&&(passwordLogin||url.pathname.startsWith('/rest/v1/rpc/')||(read&&(url.pathname.startsWith('/rest/v1/')||url.pathname.startsWith('/auth/v1/'))));
|
||||||
|
async function attempt(target,limit){
|
||||||
|
const controller=new AbortController(),abort=()=>controller.abort(original.signal.reason);
|
||||||
|
if(original.signal.aborted)abort();else original.signal.addEventListener('abort',abort,{once:true});
|
||||||
|
const timer=setTimeout(()=>controller.abort(),limit);
|
||||||
|
try{
|
||||||
|
const response=await fetch(new Request(target,original.clone()),{signal:controller.signal});
|
||||||
|
if(expectJson&&response.ok&&response.status!==204){
|
||||||
|
const text=await response.clone().text();
|
||||||
|
try{if(!text.trim()||!response.headers.get('content-type')?.includes('json'))throw new Error();JSON.parse(text)}
|
||||||
|
catch(_){throw new Error('Сервис вернул пустой или некорректный ответ. Повторите загрузку.')}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}finally{clearTimeout(timer);original.signal.removeEventListener('abort',abort)}
|
||||||
|
}
|
||||||
|
try{
|
||||||
|
const response=await attempt(isBackend?proxy+url.pathname+url.search:original.url,timeout);
|
||||||
|
if(!safeFallback||response.status<500)return response;
|
||||||
|
}catch(error){if(!safeFallback||original.signal.aborted)throw error}
|
||||||
|
// Same backend, same authorization, one SDK session. Never replay writes.
|
||||||
|
return attempt(original.url,fallbackTimeout);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
window.CateriumCloudTransport=Object.freeze({create});
|
||||||
|
})();
|
||||||
@ -1,7 +1,7 @@
|
|||||||
(()=>{
|
(()=>{
|
||||||
'use strict';
|
'use strict';
|
||||||
const VERSION='17.7.3';
|
const VERSION='17.7.3';
|
||||||
const RELEASE='20260917-calendar-compact';
|
const RELEASE='20260918-login-recovery';
|
||||||
|
|
||||||
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(){
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
|||||||
const CACHE='sun-catering-pwa-v89-20260917-calendar-compact';
|
const CACHE='sun-catering-pwa-v90-20260918-login-recovery';
|
||||||
const VERSION='20260917-calendar-compact';
|
const VERSION='20260918-login-recovery';
|
||||||
const CORE=[
|
const CORE=[
|
||||||
'./','./index.html',`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
'./','./index.html',`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
||||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||||
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
|
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
|
||||||
'./catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg',
|
'./catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg',
|
||||||
@ -9,6 +9,7 @@ const CORE=[
|
|||||||
'./offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg'
|
'./offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg'
|
||||||
];
|
];
|
||||||
const CRITICAL_FRESH=new Set([
|
const CRITICAL_FRESH=new Set([
|
||||||
|
'/core/cloud-transport.js',
|
||||||
'/core/banquet-menu.js','/core/access-policy.js','/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js'
|
'/core/banquet-menu.js','/core/access-policy.js','/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js'
|
||||||
]);
|
]);
|
||||||
self.addEventListener('install',event=>{
|
self.addEventListener('install',event=>{
|
||||||
|
|||||||
@ -18,6 +18,7 @@ test('workspace changes isolate client caches and ignore delayed responses from
|
|||||||
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><body></body></html>'}));
|
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><body></body></html>'}));
|
||||||
await page.goto('/index.html');
|
await page.goto('/index.html');
|
||||||
await page.addScriptTag({url:'/core/sun-safe.js'});
|
await page.addScriptTag({url:'/core/sun-safe.js'});
|
||||||
|
await page.addScriptTag({url:'/core/cloud-transport.js'});
|
||||||
const runtime=fs.readFileSync('public/app-runtime.js','utf8');
|
const runtime=fs.readFileSync('public/app-runtime.js','utf8');
|
||||||
let cloud=runtime.slice(runtime.indexOf('/* ===== MODULE: cloud-sync-v2.js'),runtime.indexOf('/* ===== MODULE: admin-rbac-v3.js'));
|
let cloud=runtime.slice(runtime.indexOf('/* ===== MODULE: cloud-sync-v2.js'),runtime.indexOf('/* ===== MODULE: admin-rbac-v3.js'));
|
||||||
cloud=cloud.replace('async function boot(){','async function boot(){return;');
|
cloud=cloud.replace('async function boot(){','async function boot(){return;');
|
||||||
|
|||||||
82
tests/login-recovery.spec.mjs
Normal file
82
tests/login-recovery.spec.mjs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import {test,expect} from '@playwright/test';
|
||||||
|
|
||||||
|
test('cloud reads and password login recover from empty proxy responses without replaying writes',async({page})=>{
|
||||||
|
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><body></body></html>'}));
|
||||||
|
await page.goto('/index.html');await page.addScriptTag({url:'/core/cloud-transport.js'});
|
||||||
|
const result=await page.evaluate(async()=>{
|
||||||
|
const calls=[],upstream='https://backend.example.invalid',proxy='https://proxy.example.invalid';let scenario='read';
|
||||||
|
window.fetch=async request=>{calls.push({url:request.url,body:await request.text(),auth:request.headers.get('authorization')});if(scenario==='denied')return new Response('{"error":"denied"}',{status:401});if(scenario==='write')return new Response('',{status:503});return request.url.startsWith(proxy)?new Response('',{headers:{'content-type':'text/html'}}):new Response('[{"id":"company"}]',{headers:{'content-type':'application/json'}})};
|
||||||
|
const send=window.CateriumCloudTransport.create({upstream,proxy});
|
||||||
|
const read=await (await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',headers:{Authorization:'Bearer test-token'},body:'{}'})).json();
|
||||||
|
const readCalls=calls.splice(0);
|
||||||
|
await send(upstream+'/auth/v1/token?grant_type=password',{method:'POST',body:'{"email":"test@example.invalid","password":"test"}'});const authCalls=calls.splice(0);
|
||||||
|
scenario='write';const write=await send(upstream+'/rest/v1/rpc/sun_save_app_state_v17',{method:'POST',body:'{}'});const writeCalls=calls.splice(0);
|
||||||
|
scenario='empty-write';let writeError='';try{await send(upstream+'/rest/v1/rpc/sun_save_app_state_v17',{method:'POST',body:'{}'})}catch(e){writeError=e.message}const emptyWriteCalls=calls.splice(0);
|
||||||
|
scenario='denied';const denied=await send(upstream+'/auth/v1/token?grant_type=password',{method:'POST',body:'{}'});const deniedCalls=calls.splice(0);
|
||||||
|
return {read,readCalls,authCalls,write:write.status,writeCalls,writeError,emptyWriteCalls,denied:denied.status,deniedCalls};
|
||||||
|
});
|
||||||
|
expect(result.read).toEqual([{id:'company'}]);expect(result.readCalls.map(c=>c.url)).toEqual(['https://proxy.example.invalid/rest/v1/rpc/sun_my_workspaces','https://backend.example.invalid/rest/v1/rpc/sun_my_workspaces']);
|
||||||
|
expect(result.readCalls.every(c=>c.auth==='Bearer test-token'&&c.body==='{}')).toBe(true);
|
||||||
|
expect(result.authCalls).toHaveLength(2);expect(result.authCalls[0].body).toBe(result.authCalls[1].body);
|
||||||
|
expect(result.write).toBe(503);expect(result.writeCalls).toHaveLength(1);expect(result.writeError).toContain('пустой');expect(result.emptyWriteCalls).toHaveLength(1);
|
||||||
|
expect(result.denied).toBe(401);expect(result.deniedCalls).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('membership loading is shared and an invalid response remains an error until retry succeeds',async({page})=>{
|
||||||
|
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><body></body></html>'}));await page.goto('/index.html');
|
||||||
|
await page.addScriptTag({url:'/core/sun-safe.js'});await page.addScriptTag({url:'/core/cloud-transport.js'});
|
||||||
|
const runtime=fs.readFileSync('public/app-runtime.js','utf8');let cloud=runtime.slice(runtime.indexOf('/* ===== MODULE: cloud-sync-v2.js'),runtime.indexOf('/* ===== MODULE: admin-rbac-v3.js'));
|
||||||
|
cloud=cloud.replace('async function boot(){','async function boot(){return;').replace('window.SunCloudV2={',`window.SunCloudV2={testLoad:loadMemberships,testInit:c=>{client=c;session={user:{id:'test-user'}};config.tenantStorageReady=true;config.localWorkspaceId='company';config.workspaceId='company'},`);
|
||||||
|
await page.addScriptTag({content:'var orders=[];var boxes=[];'+cloud});
|
||||||
|
const result=await page.evaluate(async()=>{
|
||||||
|
let calls=0,resolve;const c=window.SunCloudV2;c.testInit({rpc:()=>{calls++;return new Promise(r=>resolve=r)}});
|
||||||
|
const a=c.testLoad(),b=c.testLoad();resolve({data:'',error:null});await Promise.all([a,b]);const failed=c.status();
|
||||||
|
const retry=c.testLoad();resolve({data:[{id:'company',name:'Test',role:'admin',is_active:true}],error:null});await retry;
|
||||||
|
return {calls,failed,ready:c.status()};
|
||||||
|
});
|
||||||
|
expect(result.calls).toBe(2);expect(result.failed.membershipsLoading).toBe(false);expect(result.failed.membershipError).toContain('список компаний');expect(result.failed.workspace).toBeNull();
|
||||||
|
expect(result.ready.membershipError).toBe('');expect(result.ready.workspace.id).toBe('company');expect(result.ready.membershipsLoading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a failed company load replaces the stale login form with an actionable retry screen',async({page})=>{
|
||||||
|
await page.route('**://api.caterium.ru/**',r=>r.abort());await page.route('**://*.supabase.co/**',r=>r.abort());
|
||||||
|
await page.goto('/index.html',{waitUntil:'domcontentloaded'});await expect(page.locator('#sunGateEmailV3')).toBeVisible();
|
||||||
|
await page.evaluate(()=>{
|
||||||
|
window.loginState={connected:true,signedIn:true,membershipsLoading:true,membershipsLoaded:false};window.retryCount=0;
|
||||||
|
window.SunCloudV2={getSession:()=>({user:{id:'test',email:'test@example.invalid'}}),getWorkspace:()=>null,getClient:()=>null,status:()=>window.loginState,hasPermission:()=>false,reloadMemberships:async()=>{window.retryCount++;return null}};
|
||||||
|
window.dispatchEvent(new Event('sun:cloud-permissions-changed'));
|
||||||
|
});
|
||||||
|
await expect(page.locator('#sunGateEmailV3')).toHaveCount(0);
|
||||||
|
await page.evaluate(()=>{window.loginState={...window.loginState,membershipsLoading:false,membershipsLoaded:true,membershipError:'Ответ сервера не получен'};window.dispatchEvent(new Event('sun:cloud-permissions-changed'))});
|
||||||
|
await expect(page.getByRole('heading',{name:'Не удалось загрузить рабочую базу'})).toBeVisible();await expect(page.locator('#sunGateErrorV3')).toHaveText('Ответ сервера не получен');
|
||||||
|
await page.getByRole('button',{name:'Повторить загрузку',exact:true}).click();expect(await page.evaluate(()=>window.retryCount)).toBe(1);
|
||||||
|
await expect(page.getByRole('button',{name:'Повторить загрузку',exact:true})).toBeEnabled();await expect(page.locator('body > header')).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('real SDK login opens the ordinary app when the proxy returns empty successful responses',async({page})=>{
|
||||||
|
const userId='11111111-1111-4111-8111-111111111111',workspaceId='22222222-2222-4222-8222-222222222222',expires=Math.floor(Date.now()/1000)+3600;
|
||||||
|
const user={id:userId,aud:'authenticated',role:'authenticated',email:'test@example.invalid',email_confirmed_at:new Date().toISOString(),app_metadata:{provider:'email'},user_metadata:{}};
|
||||||
|
const token=[{alg:'HS256',typ:'JWT'},{sub:userId,role:'authenticated',aud:'authenticated',exp:expires,iat:expires-3600,aal:'aal1'},'test'].map(x=>typeof x==='string'?x:Buffer.from(JSON.stringify(x)).toString('base64url')).join('.');
|
||||||
|
const seen=[],warnings=[];page.on('console',m=>{if(m.type()==='warning')warnings.push(m.text())});
|
||||||
|
const handle=async route=>{
|
||||||
|
const request=route.request(),url=new URL(request.url());seen.push(url.host+url.pathname);
|
||||||
|
const headers={'access-control-allow-origin':'*'};
|
||||||
|
if(request.method()==='OPTIONS')return route.fulfill({status:204,headers});
|
||||||
|
if(url.host==='api.caterium.ru')return route.fulfill({status:200,contentType:'text/html',body:'',headers});
|
||||||
|
let body=null;
|
||||||
|
if(url.pathname==='/auth/v1/token')body={access_token:token,refresh_token:'test-refresh',token_type:'bearer',expires_in:3600,expires_at:expires,user};
|
||||||
|
else if(url.pathname==='/auth/v1/user')body=user;
|
||||||
|
else if(url.pathname.endsWith('/sun_my_workspaces'))body=[{id:workspaceId,name:'Test workspace',role:'admin',is_active:true,permissions:{}}];
|
||||||
|
else if(url.pathname.endsWith('/sun_is_platform_admin'))body=true;
|
||||||
|
return route.fulfill({status:200,contentType:'application/json',body:JSON.stringify(body),headers});
|
||||||
|
};
|
||||||
|
await page.route('**://api.caterium.ru/**',handle);await page.route('**://*.supabase.co/**',handle);
|
||||||
|
await page.goto('/index.html',{waitUntil:'domcontentloaded'});await page.waitForFunction(()=>window.CateriumAuthSecurityV1774&&window.SunCloudV2?.getClient());
|
||||||
|
await page.locator('#sunGateEmailV3').fill(user.email);await page.locator('#sunGatePasswordV3').fill('test-password');await page.locator('#sunGateSubmitV3').click();
|
||||||
|
await expect(page.locator('#sunCloudAuthGateV3')).toHaveCount(0,{timeout:20000});await expect(page.locator('body > header')).toBeVisible();
|
||||||
|
expect(await page.evaluate(()=>window.SunCloudV2.getWorkspace()?.id)).toBe(workspaceId);
|
||||||
|
expect(seen.some(s=>s==='api.caterium.ru/rest/v1/rpc/sun_my_workspaces')).toBe(true);
|
||||||
|
expect(seen.some(s=>s.endsWith('.supabase.co/rest/v1/rpc/sun_my_workspaces'))).toBe(true);
|
||||||
|
expect(warnings.some(s=>s.includes('Multiple GoTrueClient'))).toBe(false);
|
||||||
|
});
|
||||||
@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
import {fileURLToPath} from 'node:url';
|
import {fileURLToPath} from 'node:url';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir:'.',
|
testDir:'.',
|
||||||
testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs'],
|
testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs'],
|
||||||
timeout:30000,
|
timeout:30000,
|
||||||
use:{baseURL:'http://127.0.0.1:4173'},
|
use:{baseURL:'http://127.0.0.1:4173'},
|
||||||
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
|
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
|
||||||
|
|||||||
@ -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('v89-20260917-calendar-compact')&&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('v90-20260918-login-recovery')&&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('20260917-calendar-compact')&&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-login-recovery')&&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('v89-20260917-calendar-compact'),'release manifest points to current PWA cache');
|
check(String(releaseManifest.pwaCache||'').includes('v90-20260918-login-recovery'),'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=12000')&&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=12000')&&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('20260917-calendar-compact'),'index cache bust is v17.7.3');
|
check(index.includes('20260918-login-recovery'),'index cache bust is v17.7.3');
|
||||||
check(sw.includes('v89-20260917-calendar-compact')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
check(sw.includes('v90-20260918-login-recovery')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
||||||
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
||||||
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
||||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
||||||
|
|||||||
@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
|
|||||||
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
||||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||||
if(!sw.includes('20260917-calendar-compact')||!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-login-recovery')||!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('20260917-calendar-compact')||!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-login-recovery')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||||
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
||||||
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
||||||
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user