Fix v17.6.5 stability patch workflow
This commit is contained in:
parent
250355663a
commit
cb087787b2
347
.github/workflows/patch-v1765-stability.yml
vendored
347
.github/workflows/patch-v1765-stability.yml
vendored
@ -21,351 +21,10 @@ jobs:
|
|||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Apply stability hardening
|
- name: Apply stability hardening
|
||||||
run: |
|
run: |
|
||||||
node <<'NODE'
|
node scripts/apply-v1765-stability.mjs
|
||||||
const fs=require('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);
|
|
||||||
const 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);
|
|
||||||
};
|
|
||||||
const 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);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- app-runtime.js: cloud timeouts, capped conflict retries, light support refresh,
|
|
||||||
// current stability version, log dedupe/backoff, calmer Developer polling.
|
|
||||||
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;
|
|
||||||
if(payload)await mirror(payload);`,
|
|
||||||
` async function onCloudSync(ev){
|
|
||||||
noteNetworkSuccess();
|
|
||||||
const payload=ev?.detail?.payload;
|
|
||||||
if(payload)await mirror(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);
|
|
||||||
|
|
||||||
// ---- hotfix: event-driven with slow safety poll, never force admin RPC each second.
|
|
||||||
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);
|
|
||||||
|
|
||||||
// ---- ops UX: lightweight support refresh, bounded boot polling, cleanable timers.
|
|
||||||
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);
|
|
||||||
|
|
||||||
// ---- performance: current loader version + batched image mutation scanning.
|
|
||||||
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);
|
|
||||||
|
|
||||||
// ---- UX fixer: remove unnecessary 2-second churn.
|
|
||||||
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);
|
|
||||||
|
|
||||||
// ---- index + service worker: force browsers off old v17.6.0 assets.
|
|
||||||
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);
|
|
||||||
|
|
||||||
// ---- release checks: enforce anti-freeze controls and cache/version integrity.
|
|
||||||
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);
|
|
||||||
|
|
||||||
// ---- Playwright: timer exceptions, current stability version and lightweight support refresh.
|
|
||||||
let spec=read('tests/app.spec.mjs');
|
|
||||||
spec += `\n
|
|
||||||
test('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);
|
|
||||||
|
|
||||||
// ---- release metadata and notes.
|
|
||||||
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`);
|
|
||||||
NODE
|
|
||||||
npm version 17.6.5 --no-git-tag-version --allow-same-version
|
npm version 17.6.5 --no-git-tag-version --allow-same-version
|
||||||
|
- name: Validate patched syntax
|
||||||
|
run: npm run check:syntax
|
||||||
- name: Commit stability patch
|
- name: Commit stability patch
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user