From 71d9cdc92eac48b61825fcb0fbe1a8170eee9026 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Tue, 8 Sep 2026 05:55:48 +0300 Subject: [PATCH] Caterium v17.6.5 stability hardening * Prepare v17.6.5 stability hardening patch * Move v17.6.5 stability patch logic to script * Fix v17.6.5 stability patch workflow * Harden Caterium v17.6.5 stability * Remove one-time v17.6.5 patch workflow * Remove one-time v17.6.5 patch script * Update static stability checks for v17.6.5 * Fix v17.6.5 E2E version expectation * Update v17.6.5 E2E version expectation * Remove one-time v17.6.5 test patch workflow * Capture failing v17.6.5 E2E diagnostics * Capture v17.6.5 E2E failures * Make v17.6.5 stability E2E deterministic * Make v17.6.5 E2E checks deterministic * Remove one-time deterministic E2E workflow * Remove one-time E2E diagnostic workflow * Remove temporary E2E diagnostics --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/release-manifest.json | 21 ++++-- docs/releases/V17.6.5-CHANGES.txt | 12 ++++ package-lock.json | 4 +- package.json | 2 +- public/app-runtime.js | 105 +++++++++++++++++++----------- public/core/hotfix-v1763.js | 4 +- public/core/ops-ux-v1762.js | 12 ++-- public/core/performance.js | 10 +-- public/core/ux-fixes-v1764.js | 2 +- public/index.html | 4 +- public/service-worker.js | 4 +- tests/app.spec.mjs | 31 ++++++++- tests/release-check.mjs | 14 ++-- tests/static-security.mjs | 10 ++- 14 files changed, 166 insertions(+), 69 deletions(-) create mode 100644 docs/releases/V17.6.5-CHANGES.txt diff --git a/docs/release-manifest.json b/docs/release-manifest.json index 49ff23e..2b4c489 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -1,6 +1,6 @@ { "app": "Caterium", - "version": "v17.6.4", + "version": "v17.6.5", "channel": "production", "schema": 17, "legacyStateKept": true, @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v69-20260907-v17-6-4-settings-orders-pdf-menu", + "pwaCache": "v70-20260908-v17-6-5-stability", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, @@ -99,7 +99,7 @@ "catalogCompositionTildaEndpoint": "getproduct", "catalogCompositionPremiumForceRefresh": true, "catalogCompositionCacheRequiresPremium": true, - "notes": "Production v17.6.4: chat photo compression and server memory counters; bounded Developer Console memory refresh; operations UX; Developer/SaaS hotfixes; one-minute order auto-completion and payment; per-offer PDF templates; styled Menu icon; GitHub QA and Cloudflare production deployment.", + "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.", "catalogPhotoSources": 113, "catalogPhotosStoredInCatalog": true, "catalogLegacyPhotosInCatalog": 60, @@ -237,7 +237,7 @@ "signupTrial": "14-day Full", "developerMfaInputSelectorFixed": true, "developerMfaRepeatedValidationToastsFixed": true, - "release": "20260907-v17-6-4-settings-orders-pdf-menu", + "release": "20260908-v17-6-5-stability", "registrationFlow": "email-password-confirm-company-auto-login", "emailConfirmationRequired": false, "employeeInviteLinks": false, @@ -313,5 +313,16 @@ "ordersAutoCompletedOriginalDatePreserved": true, "clientOfferTemplatePerOrder": true, "menuStyledSvgIcon": true, - "releaseIntegrityChecks": true + "releaseIntegrityChecks": true, + "cloudRpcTimeoutMs": 12000, + "cloudConflictMaxRetries": 4, + "errorLogDedupMinutes": 5, + "networkFailureBackoff": true, + "stabilityLoggerVersion": "17.6.5", + "developerAdminCheckTtlMs": 30000, + "supportRefreshLightweight": true, + "supportRefreshIntervalMs": 20000, + "indexCacheBustCurrent": true, + "backgroundImageMutationBatching": true, + "opsBootWaitBounded": true } diff --git a/docs/releases/V17.6.5-CHANGES.txt b/docs/releases/V17.6.5-CHANGES.txt new file mode 100644 index 0000000..9ac31a8 --- /dev/null +++ b/docs/releases/V17.6.5-CHANGES.txt @@ -0,0 +1,12 @@ +Caterium v17.6.5 — Stability hardening +Date: 2026-09-08 + +1. Fixed stale browser asset versioning: index.html now loads runtime/core assets with the current release token instead of v17.6.0. +2. Supabase application-state RPCs have a 12-second UI timeout; sync cannot remain stuck forever in “Синхронизация…”. +3. SUN_CONFLICT retries use exponential backoff and stop after four safe retries. +4. 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. +5. Developer-admin checks are cached/throttled and protected by an 8-second timeout. +6. Read-only support live refresh updates the company snapshot in place instead of clearing and re-entering the workspace every cycle. +7. Operations boot polling is bounded, background maintenance is slower/cleanable, and image mutation scanning is batched. +8. Added regression checks for idle timer errors, lightweight support refresh, Developer polling, cache version consistency and cloud stability guards. +9. PWA cache: v70 / 20260908-v17-6-5-stability. diff --git a/package-lock.json b/package-lock.json index 40555d8..b96c716 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "caterium-app", - "version": "17.6.4", + "version": "17.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "caterium-app", - "version": "17.6.4", + "version": "17.6.5", "devDependencies": { "@playwright/test": "^1.51.0", "http-server": "^14.1.1", diff --git a/package.json b/package.json index cd3cbfe..8012090 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "caterium-app", "private": true, - "version": "17.6.4", + "version": "17.6.5", "type": "module", "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/performance.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", diff --git a/public/app-runtime.js b/public/app-runtime.js index 3fa3cdd..10b22f5 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -1993,6 +1993,13 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс 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)} + } let suppressStorageTracking = false; let observedSignature = ''; let lastStatus = 'local'; @@ -2628,14 +2635,14 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс async function fetchRemoteRow() { if (!client || !workspace?.id) return null; - 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}),'Загрузка облачной базы'); if (error) throw error; return Array.isArray(data) ? (data[0] || null) : (data || null); } async function upsertPayload(payload, expectedRevision=null) { const args={p_workspace:workspace.id,p_payload:payload,p_client_id:clientId(),p_expected_revision:expectedRevision==null?null:Number(expectedRevision)}; - 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),'Сохранение облачной базы'); if (error) throw error; return Array.isArray(data) ? data[0] : data; } @@ -2692,7 +2699,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс } catch (error) { handleError(error,'Не удалось загрузить облачные данные.'); } } - async function syncNow({quiet=false}={}) { + async function syncNow({quiet=false,retryCount=0}={}) { if (isSyncing || !client || !session || !workspace?.id || !workspaceMigrated()) return; if (!navigator.onLine) { setStatus('offline','Офлайн — изменения ждут подключения.'); return; } isSyncing = true; @@ -2741,9 +2748,10 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс } catch (error) { const msg=String(error?.message||''); if(msg.includes('SUN_CONFLICT')){ - setStatus('pending','Облачная версия изменилась. Повторяю безопасную синхронизацию…'); + const nextRetry=Number(retryCount||0)+1; try{window.SunStabilityV17?.recordConflict?.(msg);}catch(_){} - setTimeout(()=>syncNow({quiet:true}),350); + 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,'Ошибка синхронизации.'); } finally { isSyncing=false; } @@ -3088,12 +3096,12 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс async function enterSupportWorkspace(workspaceId,name='Компания') { const id=String(workspaceId||'').trim();if(!id)throw new Error('Не выбрана компания.'); if(!client||!session?.user)throw new Error('Сначала войдите в аккаунт разработчика.'); - 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('Требуется аккаунт разработчика.'); const previousWorkspaceId=String(config.workspaceId||''); const previousLocalWorkspaceId=String(config.localWorkspaceId||workspace?.id||''); if(previousLocalWorkspaceId)await captureTenantLocal(previousLocalWorkspaceId); clearTenantLocal();config.localWorkspaceId='';saveConfig();unsubscribeRealtime(); - 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; const row=Array.isArray(result.data)?result.data[0]:result.data;if(!row?.payload)throw new Error('У компании нет облачной рабочей копии.'); await applyPayload(row.payload,true); supportMode={workspaceId:id,name:String(name||'Компания'),previousWorkspaceId,previousLocalWorkspaceId,enteredAt:new Date().toISOString()}; @@ -3103,6 +3111,17 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс 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() { if(!supportMode)return false; clearTenantLocal();supportMode=null;workspace=null;config.localWorkspaceId='';saveConfig(); @@ -3137,6 +3156,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс getWorkspace:()=>workspace?clone(workspace):null, getClientId:()=>clientId(), enterSupportWorkspace, + refreshSupportWorkspace, exitSupportWorkspace, isSupportMode:()=>Boolean(supportMode), getSupportMode:()=>supportMode?clone(supportMode):null, @@ -4177,12 +4197,22 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс 'use strict'; if (window.SunStabilityV17) return; - const VERSION = '17.0.0-local-test'; - const RELEASE = 'v17 Stability · Local Test'; + 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)}} const esc=window.SunSafe.escapeHTML; const toast=(t,k='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(t,k,ms)}catch(_){} console.log('[V17]',t)}; const cloud=()=>window.SunCloudV2; @@ -4232,39 +4262,33 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс return {url:location.href,online:navigator.onLine,userAgent:navigator.userAgent,release:RELEASE,cloudStatus:cloud()?.status?.()||null}; } async function queueError(message,stack='',level='error',extra={}){ - const item={id:crypto.randomUUID?crypto.randomUUID():`${Date.now()}-${Math.random()}`,workspaceId:workspace()?.id||null,clientId:clientId(),version:VERSION,level,message:String(message||'Unknown error').slice(0,4000),stack:String(stack||'').slice(0,12000),context:{...context(),...extra},createdAt:Date.now()}; - await idbPut('errors',item);flushErrors();refreshCardSoon(); + 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{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)} window.SunStabilityV17={VERSION,RELEASE,markPending,markSynced,recordConflict,queueError,renderCard,diagnostics,createBackup:createManualBackup,emergencyExport}; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot(); @@ -5142,7 +5167,8 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс const TABS=[['overview','Обзор'],['companies','Компании'],['accounts','Аккаунты'],['plans','Тарифы'],['features','Функции'],['system','Система'],['backups','Резервные копии'],['journal','Журнал']]; const FEATURES={orders:'Заказы',calendar:'Календарь',map:'Карта',clients:'Клиенты',catalog_view:'Каталог · просмотр',catalog_edit:'Каталог · редактирование',production:'Производство',shopping:'Закупки',stock:'Склад',routes:'Маршруты',mailings:'Рассылки',money:'Финансы',stats_basic:'Статистика',stats_advanced:'Расширенная статистика',team:'Команда',suppliers:'Поставщики',print:'Печать',settings:'Настройки',branding:'Фирменное оформление',client_offers:'Предложение клиенту',offer_templates:'Шаблоны предложения',backups:'Резервные копии',audit:'Журнал',users_manage:'Пользователи и права'}; const PLANS={basic:'Базовый',professional:'Профессиональный',full:'Полный'}; - 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; function installStyle(){if($('sunDeveloperV22Style'))return;const st=document.createElement('style');st.id='sunDeveloperV22Style';st.textContent=` #sunDeveloperNavV22{font-weight:800}.sun-dev-view{max-width:1460px;margin:0 auto;padding:24px}.sun-dev-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:14px}.sun-dev-head h1{margin:0}.sun-dev-head-actions{display:flex;gap:8px;flex-wrap:wrap}.sun-dev-tabs{display:flex;gap:7px;overflow-x:auto;padding:4px 2px 12px;scrollbar-width:thin;border-bottom:1px solid #e7e8ec;margin-bottom:18px}.sun-dev-tabs button{white-space:nowrap;border:1px solid #d8dbe2;background:#fff;border-radius:999px;padding:9px 14px;font-weight:750;cursor:pointer}.sun-dev-tabs button.on{background:#171815;color:#fff;border-color:#171815}.sun-dev-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.sun-dev-kpi,.sun-dev-card{background:#fff;border:1px solid #e2e4e9;border-radius:16px;padding:16px;box-shadow:0 8px 28px rgba(30,32,36,.04)}.sun-dev-kpi small{color:#6f7480;display:block;margin-bottom:7px}.sun-dev-kpi b{font-size:24px}.sun-dev-card h2,.sun-dev-card h3{margin:0 0 10px}.sun-dev-toolbar{display:flex;gap:8px;align-items:end;flex-wrap:wrap;margin-bottom:12px}.sun-dev-toolbar label{min-width:180px}.sun-dev-toolbar input,.sun-dev-toolbar select,.sun-dev-card input,.sun-dev-card select,.sun-dev-card textarea{width:100%;box-sizing:border-box;padding:9px 10px;border:1px solid #d6d9df;border-radius:9px;background:#fff}.sun-dev-table-wrap{overflow:auto;border:1px solid #e2e4e9;border-radius:14px;background:#fff}.sun-dev-table{width:100%;border-collapse:collapse;min-width:900px}.sun-dev-table th,.sun-dev-table td{text-align:left;padding:11px 12px;border-bottom:1px solid #eceef2;vertical-align:top;font-size:13px}.sun-dev-table th{background:#f7f8fa;position:sticky;top:0;z-index:1}.sun-dev-actions{display:flex;gap:6px;flex-wrap:wrap}.sun-dev-pill{display:inline-flex;padding:4px 8px;border-radius:999px;background:#f0f2f5;font-size:11px;font-weight:750;margin:2px}.sun-dev-pill.ok{background:#e8f7ef;color:#14653b}.sun-dev-pill.warn{background:#fff3d9;color:#7d5300}.sun-dev-pill.bad{background:#fde8e8;color:#9d2424}.sun-dev-muted{color:#777d88;font-size:12px}.sun-dev-section-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.sun-dev-modal{position:fixed;inset:0;background:rgba(20,22,26,.52);z-index:100000;display:flex;align-items:center;justify-content:center;padding:18px}.sun-dev-modal-card{background:#fff;border-radius:18px;max-width:620px;width:min(620px,100%);max-height:90vh;overflow:auto;padding:20px;box-shadow:0 24px 80px rgba(0,0,0,.28)}.sun-dev-modal-head{display:flex;justify-content:space-between;gap:12px;align-items:center}.sun-dev-modal-head h2{margin:0}.sun-dev-modal-close{border:0;background:#f1f2f4;border-radius:10px;width:36px;height:36px;font-size:22px;cursor:pointer}.sun-dev-mfa-qr{max-width:220px;width:100%;display:block;margin:12px auto;border:10px solid #fff}.sun-dev-support-banner{position:fixed;left:50%;transform:translateX(-50%);bottom:16px;z-index:99995;background:#171815;color:#fff;border-radius:14px;padding:10px 12px;box-shadow:0 12px 36px rgba(0,0,0,.28);display:flex;align-items:center;gap:10px;max-width:calc(100vw - 30px)}.sun-dev-support-banner b{white-space:nowrap}.sun-dev-support-banner button{background:#fff;color:#171815;border:0;border-radius:9px;padding:7px 10px;font-weight:800;cursor:pointer}.sun-dev-danger{border:1px solid #efc8c8!important;color:#9f2424!important;background:#fff!important}.sun-dev-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;background:#f5f6f8;border-radius:8px;padding:7px;overflow-wrap:anywhere}.sun-dev-empty{padding:26px;text-align:center;color:#747a84}.sun-dev-check{display:flex;align-items:center;gap:8px}.sun-dev-check input{width:18px!important;height:18px}.sun-dev-pre{white-space:pre-wrap;word-break:break-word;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;background:#f7f8fa;padding:12px;border-radius:10px;max-height:340px;overflow:auto} @@ -5153,7 +5179,12 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс async function rpc(name,args={}){const c=client();if(!c)throw new Error('Supabase не подключён.');const r=await c.rpc(name,args);if(r.error)throw r.error;return r.data} async function log(action,workspace=null,user=null,details={}){try{await rpc('sun_dev_log_event',{p_action:action,p_workspace:workspace,p_user:user,p_details:details})}catch(_){}} - async function checkPlatformAdmin(force=false){if(checking)return platformAdmin;if(platformAdmin&&!force)return true;const c=client(),ss=session();if(!c||!ss?.user){platformAdmin=false;syncChrome();return false}checking=true;try{const r=await c.rpc('sun_is_platform_admin');platformAdmin=!r.error&&r.data===true}catch(_){platformAdmin=false}finally{checking=false;syncChrome()}return platformAdmin} + async function checkPlatformAdmin(force=false){ + if(checking)return platformAdmin;const now=Date.now();if(!force&&lastAdminCheckAt&&now-lastAdminCheckAt{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 + } function isPlatformAdmin(){return platformAdmin===true} function ensureNav(){installStyle();const nav=document.querySelector('header nav');if(!nav||!platformAdmin)return null;$('sunSaaSAdminNavV16')?.remove();let b=$('sunDeveloperNavV22');if(!b){b=document.createElement('button');b.id='sunDeveloperNavV22';b.type='button';b.dataset.navLabel='Разработчик';b.textContent='Разработчик';nav.appendChild(b);b.addEventListener('click',()=>open(b))}b.hidden=false;b.style.display='';return b} @@ -5240,7 +5271,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс 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=`
Caterium

Аккаунт разработчика

${esc(ss.user.email||'')}

Этот аккаунт управляет платформой и не обязан иметь собственную рабочую компанию.

`;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 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)} window.SunDeveloperV22={VERSION,open,isPlatformAdmin,checkPlatformAdmin,ensureNav,activateTab,ensureSupportBanner}; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot(); diff --git a/public/core/hotfix-v1763.js b/public/core/hotfix-v1763.js index ed4e13e..bdd6f8f 100644 --- a/public/core/hotfix-v1763.js +++ b/public/core/hotfix-v1763.js @@ -20,7 +20,7 @@ const dev=window.SunDeveloperV22,cloud=window.SunCloudV2; if(!dev||!cloud?.getSession?.()?.user)return false; if(dev.isPlatformAdmin?.()===true)return true; - try{return await dev.checkPlatformAdmin?.(true)===true}catch(_){return false} + try{return await dev.checkPlatformAdmin?.(false)===true}catch(_){return false} } async function enhanceDeveloperGate(){ @@ -65,7 +65,7 @@ window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(maintain,0)); window.addEventListener('sun:cloud-state-applied',()=>setTimeout(maintain,0)); setTimeout(maintain,0); - setInterval(()=>{if(!document.hidden)maintain();},1000); + setInterval(()=>{if(!document.hidden)maintain();},10000); window.SunHotfixV1763={VERSION,patchDeveloperOpen,enhanceDeveloperGate}; })(); diff --git a/public/core/ops-ux-v1762.js b/public/core/ops-ux-v1762.js index 7b2b076..6657b7b 100644 --- a/public/core/ops-ux-v1762.js +++ b/public/core/ops-ux-v1762.js @@ -4,7 +4,7 @@ const VERSION='17.6.2'; const RELEASE='20260907-v17-6-2-ops-ux'; - const SUPPORT_POLL_MS=12000; + const SUPPORT_POLL_MS=20000; const ROUTE_BASE_KEY='sunRouteBaseV1'; const ROUTE_OVERRIDE_KEY='sunRouteStartOverridesV1'; const ROUTE_DEFAULT={address:'Ленинградская область, Ломоносовский район, деревня Телези, 20',lat:59.701058,lon:29.977269}; @@ -76,7 +76,7 @@ const c=cloud(),sm=support();if(!c||!sm||supportBusy||!navigator.onLine||document.hidden)return false; supportBusy=true;renderSupportStatus(); try{ - await c.enterSupportWorkspace(sm.workspaceId,sm.name||'Компания'); + if(typeof c.refreshSupportWorkspace==='function')await c.refreshSupportWorkspace();else await c.enterSupportWorkspace(sm.workspaceId,sm.name||'Компания'); supportLastAt=Date.now();patchSupportPermissions(); try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:c.getWorkspace?.(),support:true,refresh:true}}));}catch(_){} for(const name of ['render','renderOrders','renderStats','renderClients','sunRefreshStock']){try{window[name]?.()}catch(_){}} @@ -245,15 +245,17 @@ calendarMore:true }; } + let maintenanceTimer=0,waitTimer=0,waitAttempts=0; function boot(){ installStyles();patchSupportPermissions();installMenuPage();maintainSupport();syncMenuPermission();enhanceRoutePage(); document.addEventListener('click',e=>{const more=e.target.closest('#calendar .cal-more');if(more){e.preventDefault();e.stopImmediatePropagation();showCalendarDay(more.closest('.cal-day'));return}interceptRouteActions(e)},true); window.addEventListener('sun:cloud-permissions-changed',()=>{patchSupportPermissions();maintainSupport();syncMenuPermission();if(menuActive){renderMenuList();emptyMenuDetail();}}); window.addEventListener('sun:cloud-state-applied',()=>{maintainSupport();syncMenuPermission();if(menuActive)renderMenuList();setTimeout(enhanceRoutePage,80)}); const mo=new MutationObserver(records=>{let route=false,supportBanner=false;for(const r of records){for(const n of r.addedNodes){if(n.nodeType!==1)continue;if(n.id==='routeContent'||n.querySelector?.('#routeContent')||n.closest?.('#sun-routes-view'))route=true;if(n.id==='sunDevSupportBannerV22'||n.querySelector?.('#sunDevSupportBannerV22'))supportBanner=true;}}if(route)setTimeout(enhanceRoutePage,30);if(supportBanner)setTimeout(renderSupportStatus,20);});mo.observe(document.documentElement,{childList:true,subtree:true}); - setInterval(()=>{maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},4000); - window.SunOpsUXV1762={VERSION,RELEASE,refreshSupport,openMenu,showRouteOrder,enhanceRoutePage,checks:loadSelfChecks,disconnect:()=>mo.disconnect()}; + if(!maintenanceTimer)maintenanceTimer=setInterval(()=>{if(document.hidden)return;maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},8000); + 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}}}; } - 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)}; + window.addEventListener('sun:cloud-state-applied',()=>{if(!window.SunOpsUXV1762&&waitAttempts>=120){waitAttempts=0;waitForApp()}}); if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',waitForApp,{once:true});else waitForApp(); })(); diff --git a/public/core/performance.js b/public/core/performance.js index 444ee69..57c3604 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; - const VERSION='17.6.4'; - const RELEASE='20260907-v17-6-4-settings-orders-pdf-menu'; + const VERSION='17.6.5'; + const RELEASE='20260908-v17-6-5-stability'; const critical=img=>img.closest('header,.brand,#sunCloudAuthGate,.sun-auth-gate')||img.id==='sunLoginLogo'||img.classList.contains('sun-live-catalog-logo'); const tune=img=>{ if(!(img instanceof HTMLImageElement)||critical(img))return; @@ -13,6 +13,8 @@ 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);} const MAX_FILE=15*1024*1024,TARGET=2*1024*1024,MAX_SIDE=2048; const guardedInputs=new WeakSet(); @@ -103,12 +105,12 @@ } const start=()=>{ loadHotfix();loadOpsUX();loadUXFix();scan(document);startMemoryTimer(); - 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)}));}); mo.observe(document.documentElement,{childList:true,subtree:true}); document.addEventListener('click',e=>{if(e.target.closest('#sunDeveloperNavV22,[data-dev-tab],#sunDevRefresh'))scheduleMemoryRefresh(100,true)},true); window.addEventListener('sun:cloud-state-applied',()=>scheduleMemoryRefresh(180,true)); document.addEventListener('visibilitychange',()=>{if(!document.hidden&&developerVisible())scheduleMemoryRefresh(50,false)}); - window.SunPerformance={VERSION,scanImages:()=>scan(document),refreshDeveloperMemory:(force=true)=>enhanceDeveloperMemory({force}),loadHotfix,loadOpsUX,loadUXFix,disconnect:()=>{mo.disconnect();if(memoryTimer){clearInterval(memoryTimer);memoryTimer=0;}}}; + window.SunPerformance={VERSION,scanImages:()=>scan(document),refreshDeveloperMemory:(force=true)=>enhanceDeveloperMemory({force}),loadHotfix,loadOpsUX,loadUXFix,disconnect:()=>{mo.disconnect();if(memoryTimer){clearInterval(memoryTimer);memoryTimer=0}if(imageScanTimer){clearTimeout(imageScanTimer);imageScanTimer=0}pendingImageRoots.clear();}}; }; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start(); })(); diff --git a/public/core/ux-fixes-v1764.js b/public/core/ux-fixes-v1764.js index c32158d..ba1ebd7 100644 --- a/public/core/ux-fixes-v1764.js +++ b/public/core/ux-fixes-v1764.js @@ -197,7 +197,7 @@ function maintain(){patchOrdersRender();patchOfferTemplateApi();patchOfferOpenExports();decorateMenuIcon();if(activeOfferOrderId)$('sunClientOfferModal')?.classList.contains('on')?ensureOfferPicker():clearActiveOffer()} function startTimers(){ if(!autoTimer)autoTimer=setInterval(()=>{if(!document.hidden)autoCompleteOrders(Date.now(),{silent:false})},AUTO_POLL_MS); - if(!maintainTimer)maintainTimer=setInterval(()=>{if(!document.hidden)maintain()},2000); + if(!maintainTimer)maintainTimer=setInterval(()=>{if(!document.hidden)maintain()},5000); } function onClickCapture(e){ const direct=e.target?.closest?.('[data-sun-offer-id]');if(direct?.dataset?.sunOfferId)setActiveOffer(direct.dataset.sunOfferId); diff --git a/public/index.html b/public/index.html index 09d8425..1544a5c 100644 --- a/public/index.html +++ b/public/index.html @@ -71,7 +71,7 @@ button{touch-action:manipulation} #sunGlobalSearchBtn kbd{display:none!important} #sunSyncSettingsCard>div[style*="grid-template-columns"]{grid-template-columns:1fr!important} -}
Солнце Кейтеринг

Стоимость позиций0 ₽

Предоплата0 ₽

К оплате0 ₽

Заказы

МероприятиеДата и времяАдресСуммаСтатусДействия

Нажмите «Изменить», чтобы открыть заказ. Сумма, предоплата и остаток сохраняются вместе с заказом.

Склад

В этой версии каталог продуктов пополняется из составов боксов. Остатки и цены добавим следующим шагом.

Статистика

- + diff --git a/public/service-worker.js b/public/service-worker.js index de00dda..89d7c45 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,5 +1,5 @@ -const CACHE='sun-catering-pwa-v69-20260907-v17-6-4-settings-orders-pdf-menu'; -const VERSION='20260907-v17-6-4-settings-orders-pdf-menu'; +const CACHE='sun-catering-pwa-v70-20260908-v17-6-5-stability'; +const VERSION='20260908-v17-6-5-stability'; const CORE=[ './','./index.html', `./core/sun-safe.js?v=${VERSION}`,`./core/performance.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}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, diff --git a/tests/app.spec.mjs b/tests/app.spec.mjs index 33b21f7..e8ce463 100644 --- a/tests/app.spec.mjs +++ b/tests/app.spec.mjs @@ -155,10 +155,39 @@ test('Developer Console memory refresh is bounded and does not react to its own const result=await page.evaluate(()=>({count:window.__rpcCount,box:document.querySelectorAll('#sunDevMemoryV1761').length,version:document.getElementById('sunDevReleaseVersion')?.textContent})); expect(result.count).toBeLessThanOrEqual(2); expect(result.box).toBe(1); - expect(result.version).toBe('17.6.4'); + expect(result.version).toBe('17.6.5'); }); test('mobile body does not overflow viewport', async ({ page }, testInfo) => { test.skip(testInfo.project.name!=='mobile-390'); await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await page.waitForTimeout(500); const dims=await page.evaluate(()=>({innerWidth,scrollWidth:document.documentElement.scrollWidth,bodyWidth:document.body.scrollWidth})); expect(dims.scrollWidth).toBeLessThanOrEqual(dims.innerWidth+2); expect(dims.bodyWidth).toBeLessThanOrEqual(dims.innerWidth+2); }); + + +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([]); + const runtimeSource=fs.readFileSync(path.join(process.cwd(),'public','app-runtime.js'),'utf8'); + expect(runtimeSource).toContain("const VERSION = '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(_){}document.getElementById('sunOpsUXV1762Script')?.remove();window.SunOpsUXV1762=undefined;window.__lightRefresh=0;window.__fullEnter=0;let nav=document.querySelector('header nav');if(!nav){const header=document.createElement('header');nav=document.createElement('nav');header.appendChild(nav);document.body.prepend(header)}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'); + await page.waitForFunction(()=>Boolean(window.SunOpsUXV1762),null,{timeout:5000}); + 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); +}); \ No newline at end of file diff --git a/tests/release-check.mjs b/tests/release-check.mjs index 94ef35b..5c8b07b 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -14,7 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent') check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer'); check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox'); check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4'); -check(sw.includes('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'); +check(index.includes('20260908-v17-6-5-stability')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.6.5'); check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned'); check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned'); check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded'); @@ -24,7 +25,7 @@ check(performance.includes('hotfix-v1763.js')&&performance.includes('SunHotfixV1 check(hotfix.includes('patchDeveloperOpen')&&hotfix.includes('enhanceDeveloperGate')&&hotfix.includes('data-saas-admin'),'developer gate and SaaS click hotfix is versioned'); check(hotfix.includes('source instanceof HTMLElement')&&hotfix.includes('stopImmediatePropagation'),'SaaS event object cannot reach Developer Console as a nav button'); check(performance.includes('ops-ux-v1762.js'),'operations UX module is loaded by performance core'); -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'); check(ops.includes('sun-menu-editor-v1762')&&ops.includes('Премиум боксы'),'full-page menu editor is versioned'); check(ops.includes('showCalendarDay')&&ops.includes('.cal-more'),'calendar overflow panel is versioned'); check(ops.includes("ROUTE_BASE_KEY='sunRouteBaseV1'")&&ops.includes('saveRouteStart'),'configurable route origin is versioned'); @@ -38,6 +39,11 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30 check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match'); check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json'); check(releaseManifest.channel==='production','release manifest channel is production'); -check(String(releaseManifest.pwaCache||'').includes('v17-6-4'),'release manifest points to current PWA cache'); -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(String(releaseManifest.pwaCache||'').includes('v17-6-5'),'release manifest points to current PWA cache'); +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'); +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.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'); +check(runtime.includes('refreshSupportWorkspace')&&runtime.includes('sun_dev_support_snapshot'),'cloud exposes lightweight read-only support refresh'); +check(runtime.includes('DEV_ADMIN_TTL_MS=30000')&&hotfix.includes('checkPlatformAdmin?.(false)')&&hotfix.includes('},10000);'),'developer access checks are throttled'); +check(performance.includes('pendingImageRoots')&&performance.includes('queueImageScan')&&ux.includes('},5000);'),'background DOM maintenance is batched/throttled'); if(bad)process.exit(1); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index 41668fe..41837c8 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -27,16 +27,20 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos'); const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x)); if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed'); -if(!sw.includes('v17-6-4-settings-orders-pdf-menu')||!sw.includes('ux-fixes-v1764.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated'); +if(!sw.includes('v17-6-5-stability')||!sw.includes('ux-fixes-v1764.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.6.5'); +if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260908-v17-6-5-stability'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present'); if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present'); if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed'); if(!performance.includes('MEMORY_REFRESH_MS=30000')||!performance.includes('MEMORY_TIMEOUT_MS=8000')||!performance.includes('memoryPromise'))fail('Developer Console bounded refresh controls missing');else ok('Developer Console bounded refresh controls present'); +if(!performance.includes('pendingImageRoots')||!performance.includes('queueImageScan'))fail('batched image mutation scanning missing');else ok('image mutation scanning is batched'); if(!performance.includes('ux-fixes-v1764.js')||!performance.includes('SunUXFixV1764'))fail('v17.6.4 UX fix loader missing');else ok('v17.6.4 UX fix loader present'); if(!performance.includes('hotfix-v1763.js')||!performance.includes('SunHotfixV1763'))fail('v17.6.3 hotfix loader missing');else ok('v17.6.3 hotfix loader present'); if(!performance.includes('ops-ux-v1762.js')||!performance.includes('SunOpsUXV1762'))fail('ops UX loader missing');else ok('ops UX loader present'); for(const marker of ['patchDeveloperOpen','enhanceDeveloperGate','data-saas-admin','stopImmediatePropagation','instanceof HTMLElement']) if(!hotfix.includes(marker))fail(`developer/SaaS hotfix marker missing: ${marker}`);else ok(`developer/SaaS hotfix marker: ${marker}`); -for(const marker of ['SUPPORT_POLL_MS=12000','supportReadPermission','sun-menu-editor-v1762','showCalendarDay',"ROUTE_BASE_KEY='sunRouteBaseV1'",'showRouteOrder','routeOpenYandex']) if(!ops.includes(marker)) fail(`ops UX marker missing: ${marker}`); else ok(`ops UX marker: ${marker}`); +for(const marker of ['SUPPORT_POLL_MS=20000','supportReadPermission','refreshSupportWorkspace','sun-menu-editor-v1762','showCalendarDay',"ROUTE_BASE_KEY='sunRouteBaseV1'",'showRouteOrder','routeOpenYandex']) if(!ops.includes(marker)) fail(`ops UX marker missing: ${marker}`); else ok(`ops UX marker: ${marker}`); for(const marker of ["AUTO_DELAY_MS=60*1000","order.prepayment=total","order.status='Отдан заказчику'",'sunAutoCompletedAt','classificationDate','persistOfferTemplate','clientOfferTemplateId','offerTemplateId','sun-v1764-menu-icon']) if(!ux.includes(marker))fail(`v17.6.4 UX marker missing: ${marker}`);else ok(`v17.6.4 UX marker: ${marker}`); +for(const marker of ['CLOUD_RPC_TIMEOUT_MS=12000','CLOUD_CONFLICT_MAX_RETRIES=4','refreshSupportWorkspace',"const VERSION = '17.6.5'",'ERROR_DEDUPE_MS=5*60*1000','DEV_ADMIN_TTL_MS=30000']) if(!runtime.includes(marker))fail(`v17.6.5 stability marker missing: ${marker}`);else ok(`v17.6.5 stability marker: ${marker}`); +if(!hotfix.includes('checkPlatformAdmin?.(false)')||!hotfix.includes('},10000);'))fail('Developer fallback polling is still aggressive');else ok('Developer fallback polling is throttled'); if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops+hotfix+ux)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops+hotfix+ux))fail('possible secret in ops/hotfix/ux module');else ok('ops/hotfix/ux modules have no hard-coded secret'); -if(process.exitCode)process.exit(process.exitCode); +if(process.exitCode)process.exit(process.exitCode); \ No newline at end of file