caterium-app/scripts/apply-v1765-stability.mjs
2026-09-08 05:44:25 +03:00

254 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from 'node:fs';
const RELEASE='20260908-v17-6-5-stability';
const VERSION='17.6.5';
const read=f=>fs.readFileSync(f,'utf8');
const write=(f,s)=>fs.writeFileSync(f,s);
function once(s,oldText,newText,label){
const n=s.split(oldText).length-1;
if(n!==1)throw new Error(`${label}: expected 1 match, got ${n}`);
return s.replace(oldText,newText);
}
function range(s,start,end,replacement,label){
const a=s.indexOf(start);if(a<0)throw new Error(`${label}: start not found`);
const b=s.indexOf(end,a+start.length);if(b<0)throw new Error(`${label}: end not found`);
return s.slice(0,a)+replacement+s.slice(b);
}
let runtime=read('public/app-runtime.js');
runtime=once(runtime,
` let syncTimer = null;
let isSyncing = false;
let dirty = false;`,
` let syncTimer = null;
let isSyncing = false;
let dirty = false;
const CLOUD_RPC_TIMEOUT_MS=12000;
const CLOUD_CONFLICT_MAX_RETRIES=4;
async function sunCloudAwait(promise,label='Облачный запрос',timeout=CLOUD_RPC_TIMEOUT_MS){
let timer=0;
try{return await Promise.race([Promise.resolve(promise),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(label+': превышено время ожидания ('+Math.round(timeout/1000)+' сек.)')),timeout)})]);}
finally{if(timer)clearTimeout(timer)}
}`, 'cloud timeout helper');
runtime=once(runtime,
` const {data,error} = await client.rpc('sun_fetch_app_state',{p_workspace:workspace.id});`,
` const {data,error} = await sunCloudAwait(client.rpc('sun_fetch_app_state',{p_workspace:workspace.id}),'Загрузка облачной базы');`, 'fetch cloud timeout');
runtime=once(runtime,
` const {data,error} = await client.rpc('sun_save_app_state_v17',args);`,
` const {data,error} = await sunCloudAwait(client.rpc('sun_save_app_state_v17',args),'Сохранение облачной базы');`, 'save cloud timeout');
runtime=once(runtime,` async function syncNow({quiet=false}={}) {`,` async function syncNow({quiet=false,retryCount=0}={}) {`,'sync retry parameter');
runtime=once(runtime,
` if(msg.includes('SUN_CONFLICT')){
setStatus('pending','Облачная версия изменилась. Повторяю безопасную синхронизацию…');
try{window.SunStabilityV17?.recordConflict?.(msg);}catch(_){}
setTimeout(()=>syncNow({quiet:true}),350);
}else handleError(error,'Ошибка синхронизации.');`,
` if(msg.includes('SUN_CONFLICT')){
const nextRetry=Number(retryCount||0)+1;
try{window.SunStabilityV17?.recordConflict?.(msg);}catch(_){}
if(nextRetry>CLOUD_CONFLICT_MAX_RETRIES){handleError(error,'Не удалось синхронизировать после нескольких безопасных повторов.');}
else{const delay=Math.min(5000,400*Math.pow(2,nextRetry-1));setStatus('pending','Облачная версия изменилась. Повтор '+nextRetry+'/'+CLOUD_CONFLICT_MAX_RETRIES+' через '+(Math.round(delay/100)/10)+' сек.…');setTimeout(()=>syncNow({quiet:true,retryCount:nextRetry}),delay);}
}else handleError(error,'Ошибка синхронизации.');`, 'capped conflict retry');
runtime=once(runtime,
` const admin=await client.rpc('sun_is_platform_admin');if(admin.error)throw admin.error;if(admin.data!==true)throw new Error('Требуется аккаунт разработчика.');`,
` const admin=await sunCloudAwait(client.rpc('sun_is_platform_admin'),'Проверка доступа разработчика',8000);if(admin.error)throw admin.error;if(admin.data!==true)throw new Error('Требуется аккаунт разработчика.');`, 'support admin timeout');
runtime=once(runtime,
` const result=await client.rpc('sun_dev_support_snapshot',{p_workspace:id});if(result.error)throw result.error;`,
` const result=await sunCloudAwait(client.rpc('sun_dev_support_snapshot',{p_workspace:id}),'Загрузка компании в режиме поддержки',12000);if(result.error)throw result.error;`, 'support snapshot timeout');
runtime=once(runtime,
` renderCloudUI();return clone(supportMode);
}
async function exitSupportWorkspace() {`,
` renderCloudUI();return clone(supportMode);
}
async function refreshSupportWorkspace(){
if(!supportMode||!client||!session?.user)return false;
const id=String(supportMode.workspaceId||workspace?.id||'').trim();if(!id)return false;
const result=await sunCloudAwait(client.rpc('sun_dev_support_snapshot',{p_workspace:id}),'Обновление данных компании',12000);if(result.error)throw result.error;
const row=Array.isArray(result.data)?result.data[0]:result.data;if(!row?.payload)throw new Error('У компании нет облачной рабочей копии.');
await applyPayload(row.payload,true);dirty=false;config.lastSync=new Date().toISOString();saveConfig();
setStatus('ready','Режим поддержки · '+(supportMode.name||'Компания')+' · только просмотр');
try{window.dispatchEvent(new CustomEvent('sun:cloud-state-applied',{detail:{workspaceId:id,support:true,refresh:true}}));}catch(_){}
renderCloudUI();return true;
}
async function exitSupportWorkspace() {`, 'light support refresh');
runtime=once(runtime,` enterSupportWorkspace,
exitSupportWorkspace,`,` enterSupportWorkspace,
refreshSupportWorkspace,
exitSupportWorkspace,`,'export light support refresh');
runtime=once(runtime,
` const VERSION = '17.0.0-local-test';
const RELEASE = 'v17 Stability · Local Test';
const DB_NAME = 'SunStabilityV17';
const DB_VERSION = 1;
const DAILY_KEY = 'sunV17DailyBackup';
const $ = id => document.getElementById(id);`,
` const VERSION = '17.6.5';
const RELEASE = 'v17.6.5 Stability';
const DB_NAME = 'SunStabilityV17';
const DB_VERSION = 1;
const DAILY_KEY = 'sunV17DailyBackup';
const ERROR_DEDUPE_MS=5*60*1000;
const STABILITY_RPC_TIMEOUT_MS=10000;
const NETWORK_BACKOFF_MAX_MS=5*60*1000;
const errorSeen=new Map();
let flushBusy=false,mirrorBusy=false,backupBusy=false,initialBusy=false,networkFailures=0,nextNetworkAttemptAt=0;
const $ = id => document.getElementById(id);
const networkish=e=>/failed to fetch|networkerror|load failed|network|timeout|время ожидания/i.test(String(e?.message||e||''));
function noteNetworkFailure(){networkFailures=Math.min(networkFailures+1,8);nextNetworkAttemptAt=Date.now()+Math.min(NETWORK_BACKOFF_MAX_MS,5000*Math.pow(2,networkFailures-1));}
function noteNetworkSuccess(){networkFailures=0;nextNetworkAttemptAt=0;}
function networkReady(){return navigator.onLine&&Date.now()>=nextNetworkAttemptAt;}
async function stabilityRpc(promise,label){let timer=0;try{return await Promise.race([Promise.resolve(promise),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(label+': timeout')),STABILITY_RPC_TIMEOUT_MS)})]);}finally{if(timer)clearTimeout(timer)}}`, 'stability release and backoff state');
runtime=range(runtime,` async function queueError(message,stack='',level='error',extra={}){`,` async function mirror(payload){`,
` async function queueError(message,stack='',level='error',extra={}){
const text=String(message||'Unknown error').slice(0,4000),wid=workspace()?.id||null,key=level+'|'+(wid||'')+'|'+text,now=Date.now();
const seen=errorSeen.get(key)||0;if(now-seen<ERROR_DEDUPE_MS)return false;errorSeen.set(key,now);
const item={id:crypto.randomUUID?crypto.randomUUID():Date.now()+'-'+Math.random(),workspaceId:wid,clientId:clientId(),version:VERSION,level,message:text,stack:String(stack||'').slice(0,12000),context:{...context(),...extra},createdAt:now};
await idbPut('errors',item);if(networkReady())flushErrors();refreshCardSoon();return true;
}
async function flushErrors(){
const c=client(),ws=workspace();if(flushBusy||!c||!networkReady())return;flushBusy=true;
try{const items=await idbAll('errors');for(const item of items.slice(0,20)){try{const {error}=await stabilityRpc(c.rpc('sun_v17_log_error',{p_workspace:item.workspaceId||ws?.id||null,p_client_id:item.clientId,p_app_version:item.version,p_level:item.level,p_message:item.message,p_stack:item.stack,p_context:item.context}),'Журнал ошибок');if(error)throw error;await idbDelete('errors',item.id);noteNetworkSuccess();}catch(e){if(networkish(e))noteNetworkFailure();break;}}}
finally{flushBusy=false}
}
`, 'deduplicated error queue');
runtime=range(runtime,` async function mirror(payload){`,` async function ensureDailyBackup(){`,
` async function mirror(payload){
const c=client(),ws=workspace();if(mirrorBusy||!c||!ws?.id||!payload||!networkReady())return null;mirrorBusy=true;
try{const {data,error}=await stabilityRpc(c.rpc('sun_v17_mirror_legacy',{p_workspace:ws.id,p_payload:payload,p_client_id:clientId()}),'Зеркалирование v17');if(error)throw error;noteNetworkSuccess();lastMirrorAt=new Date().toISOString();await idbPut('meta',{key:'mirror:'+ws.id,at:Date.now(),result:data||{}});return data;}
catch(e){if(networkish(e))noteNetworkFailure();await queueError('Ошибка зеркалирования v17: '+(e.message||e),e.stack||'','warn');return null}
finally{mirrorBusy=false}
}
`, 'mirror backoff');
runtime=range(runtime,` async function ensureDailyBackup(){`,` async function onCloudSync(ev){`,
` async function ensureDailyBackup(){
const c=client(),ws=workspace();if(backupBusy||!c||!ws?.id||!networkReady())return;backupBusy=true;
try{const today=new Date().toISOString().slice(0,10);let state={};try{state=JSON.parse(localStorage.getItem(DAILY_KEY)||'{}')||{}}catch(_){}if(state[ws.id]===today)return;const {error}=await stabilityRpc(c.rpc('sun_v17_create_backup',{p_workspace:ws.id,p_kind:'daily',p_label:'Автоматическая копия '+today}),'Ежедневная резервная копия');if(error)throw error;await stabilityRpc(c.rpc('sun_v17_prune_backups',{p_workspace:ws.id}),'Очистка старых резервных копий');noteNetworkSuccess();state[ws.id]=today;localStorage.setItem(DAILY_KEY,JSON.stringify(state));}
catch(e){if(networkish(e))noteNetworkFailure();await queueError('Не удалось создать ежедневную резервную копию: '+(e.message||e),e.stack||'','warn')}
finally{backupBusy=false}
}
`, 'backup backoff');
runtime=once(runtime,` async function onCloudSync(ev){
const payload=ev?.detail?.payload;`,` async function onCloudSync(ev){
noteNetworkSuccess();
const payload=ev?.detail?.payload;`,'network success on cloud sync');
runtime=range(runtime,` async function initialMirrorIfPossible(){`,` function boot(){`,
` async function initialMirrorIfPossible(){
const c=client(),ws=workspace();if(initialBusy||!c||!ws?.id||!networkReady())return;initialBusy=true;
try{const payload=await cloud()?.snapshotLocal?.();if(payload)await mirror(payload);await ensureDailyBackup();await flushErrors()}catch(e){await queueError(e.message||String(e),e.stack||'','warn')}
finally{initialBusy=false}
}
`, 'single-flight initial mirror');
runtime=once(runtime,
` function boot(){if(booted)return;booted=true;installStyle();installErrorHooks();observeSettings();window.addEventListener('sun:cloud-sync-complete',onCloudSync);window.addEventListener('online',()=>{flushErrors();setTimeout(initialMirrorIfPossible,700)});window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(initialMirrorIfPossible,600));setTimeout(initialMirrorIfPossible,2200);setInterval(()=>{if(navigator.onLine)flushErrors()},60000)}`,
` function boot(){if(booted)return;booted=true;installStyle();installErrorHooks();observeSettings();window.addEventListener('sun:cloud-sync-complete',onCloudSync);window.addEventListener('online',()=>{noteNetworkSuccess();flushErrors();setTimeout(initialMirrorIfPossible,900)});window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(initialMirrorIfPossible,900));setTimeout(initialMirrorIfPossible,2600);setInterval(()=>{if(networkReady())flushErrors()},60000)}`, 'stability boot throttling');
runtime=once(runtime,` let platformAdmin=false,checking=false,activeTab=localStorage.getItem(TAB_KEY)||'overview',lastCompanies=[];`,` let platformAdmin=false,checking=false,activeTab=localStorage.getItem(TAB_KEY)||'overview',lastCompanies=[],lastAdminCheckAt=0;
const DEV_ADMIN_TTL_MS=30000,DEV_ADMIN_TIMEOUT_MS=8000;`,'developer admin cache state');
runtime=range(runtime,` async function checkPlatformAdmin(force=false){`,` function isPlatformAdmin(){`,
` async function checkPlatformAdmin(force=false){
if(checking)return platformAdmin;const now=Date.now();if(!force&&lastAdminCheckAt&&now-lastAdminCheckAt<DEV_ADMIN_TTL_MS)return platformAdmin;
const c=client(),ss=session();if(!c||!ss?.user){platformAdmin=false;lastAdminCheckAt=now;syncChrome();return false}checking=true;let timer=0;
try{const r=await Promise.race([Promise.resolve(c.rpc('sun_is_platform_admin')),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error('Developer access timeout')),DEV_ADMIN_TIMEOUT_MS)})]);platformAdmin=!r.error&&r.data===true;}
catch(_){platformAdmin=false}finally{clearTimeout(timer);lastAdminCheckAt=Date.now();checking=false;syncChrome()}return platformAdmin
}
`, 'developer admin bounded check');
runtime=once(runtime,
` function boot(){installStyle();let tries=0;const timer=setInterval(async()=>{tries++;if(client()&&session()?.user){await checkPlatformAdmin(tries%6===0);enhanceDeveloperGate();ensureSupportBanner()}else if(!session()?.user){platformAdmin=false;syncChrome()}if(tries>240)clearInterval(timer)},1500);window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>checkPlatformAdmin(true),80));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)}`, 'developer polling throttling');
write('public/app-runtime.js',runtime);
let hotfix=read('public/core/hotfix-v1763.js');
hotfix=once(hotfix,"try{return await dev.checkPlatformAdmin?.(true)===true}catch(_){return false}","try{return await dev.checkPlatformAdmin?.(false)===true}catch(_){return false}",'hotfix cached admin check');
hotfix=once(hotfix," setInterval(()=>{if(!document.hidden)maintain();},1000);"," setInterval(()=>{if(!document.hidden)maintain();},10000);",'hotfix slow fallback poll');
write('public/core/hotfix-v1763.js',hotfix);
let ops=read('public/core/ops-ux-v1762.js');
ops=once(ops," const SUPPORT_POLL_MS=12000;"," const SUPPORT_POLL_MS=20000;",'support poll cadence');
ops=once(ops," await c.enterSupportWorkspace(sm.workspaceId,sm.name||'Компания');"," if(typeof c.refreshSupportWorkspace==='function')await c.refreshSupportWorkspace();else await c.enterSupportWorkspace(sm.workspaceId,sm.name||'Компания');",'light support refresh use');
ops=once(ops," function boot(){\n installStyles();patchSupportPermissions();installMenuPage();maintainSupport();syncMenuPermission();enhanceRoutePage();"," let maintenanceTimer=0,waitTimer=0,waitAttempts=0;\n function boot(){\n installStyles();patchSupportPermissions();installMenuPage();maintainSupport();syncMenuPermission();enhanceRoutePage();",'ops timer state');
ops=once(ops," setInterval(()=>{maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},4000);"," if(!maintenanceTimer)maintenanceTimer=setInterval(()=>{if(document.hidden)return;maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},8000);",'ops maintenance cadence');
ops=once(ops," window.SunOpsUXV1762={VERSION,RELEASE,refreshSupport,openMenu,showRouteOrder,enhanceRoutePage,checks:loadSelfChecks,disconnect:()=>mo.disconnect()};"," window.SunOpsUXV1762={VERSION,RELEASE,refreshSupport,openMenu,showRouteOrder,enhanceRoutePage,checks:loadSelfChecks,disconnect:()=>{mo.disconnect();if(maintenanceTimer){clearInterval(maintenanceTimer);maintenanceTimer=0}if(supportTimer){clearInterval(supportTimer);supportTimer=0}if(waitTimer){clearTimeout(waitTimer);waitTimer=0}}};",'ops timer cleanup');
ops=once(ops," const waitForApp=()=>{if(window.SunCloudV2&&document.querySelector('header nav')&&typeof window.editBox==='function')boot();else setTimeout(waitForApp,120)};"," const waitForApp=()=>{if(window.SunCloudV2&&document.querySelector('header nav')&&typeof window.editBox==='function'){if(waitTimer){clearTimeout(waitTimer);waitTimer=0}boot();return}waitAttempts++;if(waitAttempts>=120){console.warn('[Caterium ops UX] app prerequisites timed out; waiting for a cloud-state event');return}waitTimer=setTimeout(waitForApp,250)};\n window.addEventListener('sun:cloud-state-applied',()=>{if(!window.SunOpsUXV1762&&waitAttempts>=120){waitAttempts=0;waitForApp()}});",'bounded ops boot wait');
write('public/core/ops-ux-v1762.js',ops);
let perf=read('public/core/performance.js');
perf=once(perf," const VERSION='17.6.4';\n const RELEASE='20260907-v17-6-4-settings-orders-pdf-menu';",` const VERSION='${VERSION}';\n const RELEASE='${RELEASE}';`,'performance release');
perf=once(perf,
` const scan=root=>{
if(root instanceof HTMLImageElement)tune(root);
root?.querySelectorAll?.('img').forEach(tune);
};`,
` const scan=root=>{
if(root instanceof HTMLImageElement)tune(root);
root?.querySelectorAll?.('img').forEach(tune);
};
const pendingImageRoots=new Set();let imageScanTimer=0;
function queueImageScan(root){if(!root||root.nodeType!==1)return;for(const existing of [...pendingImageRoots]){if(existing===root||existing.contains?.(root))return;if(root.contains?.(existing))pendingImageRoots.delete(existing)}pendingImageRoots.add(root);if(imageScanTimer)return;imageScanTimer=setTimeout(()=>{imageScanTimer=0;const roots=[...pendingImageRoots];pendingImageRoots.clear();roots.forEach(scan)},80);}`, 'batched image scan helper');
perf=once(perf," const mo=new MutationObserver(records=>{records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));});"," const mo=new MutationObserver(records=>{records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)queueImageScan(n)}));});",'batched mutation image scan');
perf=once(perf,"disconnect:()=>{mo.disconnect();if(memoryTimer){clearInterval(memoryTimer);memoryTimer=0;}}","disconnect:()=>{mo.disconnect();if(memoryTimer){clearInterval(memoryTimer);memoryTimer=0}if(imageScanTimer){clearTimeout(imageScanTimer);imageScanTimer=0}pendingImageRoots.clear();}",'performance cleanup');
write('public/core/performance.js',perf);
let ux=read('public/core/ux-fixes-v1764.js');
ux=once(ux," if(!maintainTimer)maintainTimer=setInterval(()=>{if(!document.hidden)maintain()},2000);"," if(!maintainTimer)maintainTimer=setInterval(()=>{if(!document.hidden)maintain()},5000);",'UX maintenance cadence');
write('public/core/ux-fixes-v1764.js',ux);
let index=read('public/index.html');
const OLD='20260907-v17-6-0-stability-security';
const count=index.split(OLD).length-1;if(count<5)throw new Error(`index cache token: expected >=5 matches, got ${count}`);
index=index.split(OLD).join(RELEASE);write('public/index.html',index);
let sw=read('public/service-worker.js');
sw=once(sw,"const CACHE='sun-catering-pwa-v69-20260907-v17-6-4-settings-orders-pdf-menu';\nconst VERSION='20260907-v17-6-4-settings-orders-pdf-menu';",`const CACHE='sun-catering-pwa-v70-${RELEASE}';\nconst VERSION='${RELEASE}';`,'service worker v70');
write('public/service-worker.js',sw);
let checks=read('tests/release-check.mjs');
checks=once(checks,"check(sw.includes('v17-6-4-settings-orders-pdf-menu')&&sw.includes('ux-fixes-v1764.js'),'service worker cache is v17.6.4');",`check(sw.includes('v17-6-5-stability')&&sw.includes('ux-fixes-v1764.js'),'service worker cache is v17.6.5');\ncheck(index.includes('${RELEASE}')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.6.5');`,'release SW check');
checks=once(checks,"check(ops.includes('supportReadPermission')&&ops.includes('SUPPORT_POLL_MS=12000'),'developer support read-only live refresh is versioned');","check(ops.includes('supportReadPermission')&&ops.includes('SUPPORT_POLL_MS=20000')&&ops.includes('refreshSupportWorkspace'),'developer support read-only refresh is lightweight and bounded');",'release support check');
checks=once(checks,"check(String(releaseManifest.pwaCache||'').includes('v17-6-4'),'release manifest points to current PWA cache');","check(String(releaseManifest.pwaCache||'').includes('v17-6-5'),'release manifest points to current PWA cache');",'manifest cache check');
checks=once(checks,"check(['17.6.2','17.6.3','17.6.4'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist for v17.6.2 through v17.6.4');","check(['17.6.2','17.6.3','17.6.4','17.6.5'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.6.5');\ncheck(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');\ncheck(runtime.includes(\"const VERSION = '17.6.5'\")&&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');\ncheck(runtime.includes('refreshSupportWorkspace')&&runtime.includes('sun_dev_support_snapshot'),'cloud exposes lightweight read-only support refresh');\ncheck(runtime.includes('DEV_ADMIN_TTL_MS=30000')&&hotfix.includes('checkPlatformAdmin?.(false)')&&hotfix.includes('},10000);'),'developer access checks are throttled');\ncheck(performance.includes('pendingImageRoots')&&performance.includes('queueImageScan')&&ux.includes('},5000);'),'background DOM maintenance is batched/throttled');",'stability release checks');
write('tests/release-check.mjs',checks);
let spec=read('tests/app.spec.mjs');
spec += `\n\ntest('v17.6.5 stays free of timer page errors during idle', async ({ page }, testInfo) => {
test.skip(testInfo.project.name!=='desktop');
const errors=[];page.on('pageerror',e=>errors.push(String(e)));
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.waitForTimeout(5500);
expect(errors).toEqual([]);
expect(await page.evaluate(()=>window.SunStabilityV17?.VERSION)).toBe('17.6.5');
});
test('v17.6.5 support refresh uses lightweight cloud API', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.evaluate(()=>{try{window.SunOpsUXV1762?.disconnect?.()}catch(_){}window.SunOpsUXV1762=undefined;window.__lightRefresh=0;window.__fullEnter=0;window.SunCloudV2={getSupportMode:()=>({workspaceId:'support-test',name:'Тест'}),isSupportMode:()=>true,hasPermission:()=>false,getSession:()=>({user:{id:'dev'}}),refreshSupportWorkspace:async()=>{window.__lightRefresh++;return true},enterSupportWorkspace:async()=>{window.__fullEnter++;return true}};window.editBox=window.editBox||(()=>{});});
await injectCore(page,'ops-ux-v1762.js','SunOpsUXV1762');
await page.evaluate(()=>window.SunOpsUXV1762.refreshSupport(true));
const counts=await page.evaluate(()=>({light:window.__lightRefresh,full:window.__fullEnter}));
expect(counts.light).toBe(1);expect(counts.full).toBe(0);
});
test('v17.6.5 developer hotfix does not poll admin every second', async ({ page }) => {
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
await page.evaluate(()=>{window.SunHotfixV1763=undefined;window.__devChecks=0;window.SunCloudV2={getSession:()=>({user:{id:'dev'}}),getWorkspace:()=>null};window.SunDeveloperV22={open:()=>{},isPlatformAdmin:()=>false,checkPlatformAdmin:async()=>{window.__devChecks++;return false}};});
await injectCore(page,'hotfix-v1763.js','SunHotfixV1763');
await page.waitForTimeout(2600);
expect(await page.evaluate(()=>window.__devChecks)).toBeLessThanOrEqual(2);
});\n`;
write('tests/app.spec.mjs',spec);
const manifestPath='docs/release-manifest.json';
const manifest=JSON.parse(read(manifestPath));
Object.assign(manifest,{version:'v17.6.5',channel:'production',pwaCache:`v70-${RELEASE}`,release:RELEASE,cloudRpcTimeoutMs:12000,cloudConflictMaxRetries:4,errorLogDedupMinutes:5,networkFailureBackoff:true,stabilityLoggerVersion:'17.6.5',developerAdminCheckTtlMs:30000,supportRefreshLightweight:true,supportRefreshIntervalMs:20000,indexCacheBustCurrent:true,backgroundImageMutationBatching:true,opsBootWaitBounded:true,notes:'Stability hardening: cache-busting corrected from v17.6.0, cloud RPC timeouts and capped conflict retries, deduplicated network error logging with backoff, lightweight support refresh, calmer Developer/background polling.'});
write(manifestPath,JSON.stringify(manifest,null,2)+'\n');
write('docs/releases/V17.6.5-CHANGES.txt',`Caterium v17.6.5 — Stability hardening\nDate: 2026-09-08\n\n1. Fixed stale browser asset versioning: index.html now loads runtime/core assets with the current release token instead of v17.6.0.\n2. Supabase application-state RPCs have a 12-second UI timeout; sync cannot remain stuck forever in “Синхронизация…”.\n3. SUN_CONFLICT retries use exponential backoff and stop after four safe retries.\n4. Stability logging reports the real app version, deduplicates repeated identical errors for five minutes, prevents concurrent mirror/backup/error flush runs and backs off on network failures.\n5. Developer-admin checks are cached/throttled and protected by an 8-second timeout.\n6. Read-only support live refresh updates the company snapshot in place instead of clearing and re-entering the workspace every cycle.\n7. Operations boot polling is bounded, background maintenance is slower/cleanable, and image mutation scanning is batched.\n8. Added regression checks for idle timer errors, lightweight support refresh, Developer polling, cache version consistency and cloud stability guards.\n9. PWA cache: v70 / ${RELEASE}.\n`);
console.log('v17.6.5 stability patch applied');