diff --git a/package.json b/package.json
index 520c5e1..6040cb8 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
"name": "caterium-app",
"private": true,
- "version": "17.6.2",
+ "version": "17.6.3",
"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/ops-ux-v1762.js && node --check public/core/pdf-engine.js",
+ "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/pdf-engine.js",
"test:static": "node tests/static-security.mjs",
"check:release": "node tests/release-check.mjs",
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release",
diff --git a/public/core/hotfix-v1763.js b/public/core/hotfix-v1763.js
new file mode 100644
index 0000000..ed4e13e
--- /dev/null
+++ b/public/core/hotfix-v1763.js
@@ -0,0 +1,71 @@
+(()=>{
+ 'use strict';
+ if(window.SunHotfixV1763)return;
+
+ const VERSION='17.6.3';
+ const $=id=>document.getElementById(id);
+ const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+ let checkingGate=false;
+
+ function patchDeveloperOpen(){
+ const dev=window.SunDeveloperV22;
+ if(!dev||dev.__sunV1763SafeOpen||typeof dev.open!=='function')return false;
+ const original=dev.open.bind(dev);
+ dev.open=(source=null)=>original(source instanceof HTMLElement?source:null);
+ Object.defineProperty(dev,'__sunV1763SafeOpen',{value:true,configurable:true});
+ return true;
+ }
+
+ async function platformAdmin(){
+ 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}
+ }
+
+ async function enhanceDeveloperGate(){
+ if(checkingGate)return false;
+ const cloud=window.SunCloudV2,dev=window.SunDeveloperV22;
+ if(!cloud||!dev)return false;
+ const session=cloud.getSession?.(),workspace=cloud.getWorkspace?.();
+ if(!session?.user||workspace)return false;
+ checkingGate=true;
+ try{
+ if(!await platformAdmin())return false;
+ const gate=$('sunCloudAuthGateV3'),card=gate?.querySelector('.sun-cloud-auth-card');
+ if(!gate||!card)return false;
+ if(card.dataset.dev1763==='1')return true;
+ gate.dataset.saasEnhanced='1';
+ card.dataset.devEnhanced='1';
+ card.dataset.dev1763='1';
+ card.innerHTML=`

Аккаунт разработчика
${esc(session.user.email||'')}
Этот аккаунт управляет платформой и не обязан иметь собственную рабочую компанию.
`;
+ card.querySelector('[data-open-dev-v1763]')?.addEventListener('click',()=>{patchDeveloperOpen();dev.open?.();});
+ card.querySelector('[data-signout-v1763]')?.addEventListener('click',()=>cloud.signOut?.());
+ return true;
+ }finally{checkingGate=false;}
+ }
+
+ function interceptSaasAdmin(event){
+ const button=event.target?.closest?.('[data-saas-admin]');
+ if(!button)return;
+ const dev=window.SunDeveloperV22;
+ if(!dev)return;
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ patchDeveloperOpen();
+ dev.open?.();
+ }
+
+ function maintain(){
+ patchDeveloperOpen();
+ enhanceDeveloperGate().catch(()=>{});
+ }
+
+ document.addEventListener('click',interceptSaasAdmin,true);
+ 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);
+
+ window.SunHotfixV1763={VERSION,patchDeveloperOpen,enhanceDeveloperGate};
+})();
diff --git a/public/core/performance.js b/public/core/performance.js
index 68cf4dc..ab7af74 100644
--- a/public/core/performance.js
+++ b/public/core/performance.js
@@ -53,7 +53,7 @@
const input=e.target;if(!(input instanceof HTMLInputElement)||input.id!=='sunChatFilesV29'||guardedInputs.has(input)||typeof DataTransfer==='undefined')return;
e.preventDefault();e.stopImmediatePropagation();guardChatFiles(input).catch(err=>{console.error('[Caterium photo compression]',err);guardedInputs.add(input);input.dispatchEvent(new Event('change',{bubbles:true}));guardedInputs.delete(input);});
},true);
- window.SunAttachmentGuard={VERSION:'17.6.2',MAX_FILE,TARGET,MAX_SIDE,compressImage,prepareAttachment};
+ window.SunAttachmentGuard={VERSION:'17.6.3',MAX_FILE,TARGET,MAX_SIDE,compressImage,prepareAttachment};
let memoryData=null,memoryAt=0,memoryLoading=false;
const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'');
@@ -65,23 +65,27 @@
async function enhanceDeveloperMemory(){
const view=document.getElementById('sun-developer-console-v22');if(!view?.classList.contains('on'))return;
const body=document.getElementById('sunDevBody');if(!body)return;
- const version=document.getElementById('sunDevReleaseVersion');if(version)version.textContent='17.6.2';
+ const version=document.getElementById('sunDevReleaseVersion');if(version)version.textContent='17.6.3';
const d=await loadMemory();if(!d||!view.classList.contains('on'))return;
let box=document.getElementById('sunDevMemoryV1761');if(!box){box=document.createElement('div');box.id='sunDevMemoryV1761';box.className='sun-dev-grid';box.style.marginBottom='12px';body.prepend(box);}
box.innerHTML=`Память сервера${esc(d.server_size||d.database_size||'—')}
База PostgreSQL${esc(d.database_size||'—')}
Файлы Storage${esc(d.storage_size||'—')}
Объектов Storage${Number(d.storage_objects||0)}
`;
}
+ function loadHotfix(){
+ if(window.SunHotfixV1763||document.getElementById('sunHotfixV1763Script'))return;
+ const script=document.createElement('script');script.id='sunHotfixV1763Script';script.src='core/hotfix-v1763.js?v=20260907-v17-6-3-developer-hotfix';script.async=true;script.onerror=()=>console.error('[Caterium] Не загрузился модуль hotfix-v1763.js');document.head.appendChild(script);
+ }
function loadOpsUX(){
if(window.SunOpsUXV1762||document.getElementById('sunOpsUXV1762Script'))return;
- const script=document.createElement('script');script.id='sunOpsUXV1762Script';script.src='core/ops-ux-v1762.js?v=20260907-v17-6-2-ops-ux';script.async=true;script.onerror=()=>console.error('[Caterium] Не загрузился модуль ops-ux-v1762.js');document.head.appendChild(script);
+ const script=document.createElement('script');script.id='sunOpsUXV1762Script';script.src='core/ops-ux-v1762.js?v=20260907-v17-6-3-developer-hotfix';script.async=true;script.onerror=()=>console.error('[Caterium] Не загрузился модуль ops-ux-v1762.js');document.head.appendChild(script);
}
const start=()=>{
- loadOpsUX();scan(document);
+ loadHotfix();loadOpsUX();scan(document);
const mo=new MutationObserver(records=>{records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory();});
mo.observe(document.documentElement,{childList:true,subtree:true});
document.addEventListener('click',e=>{if(e.target.closest('#sunDeveloperNavV22,[data-dev-tab],#sunDevRefresh'))setTimeout(enhanceDeveloperMemory,100)},true);
window.addEventListener('sun:cloud-state-applied',()=>setTimeout(enhanceDeveloperMemory,150));
setInterval(()=>{if(document.getElementById('sun-developer-console-v22')?.classList.contains('on'))enhanceDeveloperMemory();},5000);
- window.SunPerformance={scanImages:()=>scan(document),refreshDeveloperMemory:enhanceDeveloperMemory,loadOpsUX,disconnect:()=>mo.disconnect()};
+ window.SunPerformance={scanImages:()=>scan(document),refreshDeveloperMemory:enhanceDeveloperMemory,loadHotfix,loadOpsUX,disconnect:()=>mo.disconnect()};
};
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start();
})();
diff --git a/public/service-worker.js b/public/service-worker.js
index 5962654..80beccd 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,8 +1,8 @@
-const CACHE='sun-catering-pwa-v67-20260907-v17-6-2-ops-ux';
-const VERSION='20260907-v17-6-2-ops-ux';
+const CACHE='sun-catering-pwa-v68-20260907-v17-6-3-developer-hotfix';
+const VERSION='20260907-v17-6-3-developer-hotfix';
const CORE=[
'./','./index.html',
- `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
+ `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
'./catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg',
'./sun-logo.png','./caterium-login-logo.png','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest',
diff --git a/tests/app.spec.mjs b/tests/app.spec.mjs
index aef0992..08e22e1 100644
--- a/tests/app.spec.mjs
+++ b/tests/app.spec.mjs
@@ -65,6 +65,32 @@ test('v17.6.2 operations UX boots with menu and route features', async ({ page }
expect(checks.calendarMore).toBeTruthy();
expect(await page.locator('header nav button', {hasText:'Меню'}).count()).toBeGreaterThan(0);
});
+test('v17.6.3 developer gate bypasses workspace loading and SaaS click is safe', async ({ page }) => {
+ await page.goto('/index.html', { waitUntil:'domcontentloaded' });
+ await page.evaluate(()=>{
+ document.body.innerHTML='';
+ window.__devOpenArgs=[];
+ window.SunCloudV2={
+ getSession:()=>({user:{id:'dev',email:'developer@example.com'}}),
+ getWorkspace:()=>null,
+ status:()=>({membershipsLoading:true,membershipsLoaded:false}),
+ signOut:()=>{}
+ };
+ window.SunDeveloperV22={
+ open:arg=>{window.__devOpenArgs.push(arg===null?'null':arg?.constructor?.name||typeof arg)},
+ isPlatformAdmin:()=>true,
+ checkPlatformAdmin:async()=>true
+ };
+ });
+ const content=fs.readFileSync(path.join(process.cwd(),'public','core','hotfix-v1763.js'),'utf8');
+ await page.addScriptTag({content});
+ await page.waitForFunction(()=>document.querySelector('[data-open-dev-v1763]')&&Boolean(window.SunHotfixV1763),null,{timeout:5000});
+ expect(await page.locator('#sunCloudAuthGateV3 h2').textContent()).toBe('Аккаунт разработчика');
+ await page.locator('[data-saas-admin]').click();
+ await page.locator('[data-open-dev-v1763]').click();
+ const args=await page.evaluate(()=>window.__devOpenArgs);
+ expect(args).toEqual(['null','null']);
+});
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}));
diff --git a/tests/release-check.mjs b/tests/release-check.mjs
index 91636f0..aeb2fed 100644
--- a/tests/release-check.mjs
+++ b/tests/release-check.mjs
@@ -3,16 +3,19 @@ import path from 'node:path';
const root=process.cwd(), pub=path.join(root,'public');
const read=p=>fs.readFileSync(path.join(pub,p),'utf8');
let bad=0;const check=(v,m)=>{console.log(`${v?'OK':'FAIL'}: ${m}`);if(!v)bad++};
-const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),ops=read('core/ops-ux-v1762.js');
+const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),ops=read('core/ops-ux-v1762.js'),hotfix=read('core/hotfix-v1763.js');
check(index.includes('core/stability-v1760.css'),'mobile stability stylesheet loaded');
check(css.includes('overflow-x:hidden')&&css.includes('.cats'),'mobile overflow guard present');
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-2-ops-ux')&&sw.includes('ops-ux-v1762.js'),'service worker cache is v17.6.2');
+check(sw.includes('v17-6-3-developer-hotfix')&&sw.includes('hotfix-v1763.js'),'service worker cache is v17.6.3');
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('hotfix-v1763.js')&&performance.includes('SunHotfixV1763'),'developer/SaaS hotfix is loaded');
+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('sun-menu-editor-v1762')&&ops.includes('Премиум боксы'),'full-page menu editor is versioned');
@@ -20,5 +23,5 @@ check(ops.includes('showCalendarDay')&&ops.includes('.cal-more'),'calendar overf
check(ops.includes("ROUTE_BASE_KEY='sunRouteBaseV1'")&&ops.includes('saveRouteStart'),'configurable route origin is versioned');
check(ops.includes('showRouteOrder')&&ops.includes('data-route-open'),'route order modal is versioned');
check(fs.existsSync(path.join(root,'supabase/functions/caterium-create-employee/index.ts')),'employee Edge Function source is versioned');
-check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30,}/i.test(index+runtime+performance+ops),'no client secret-like token');
+check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30,}/i.test(index+runtime+performance+ops+hotfix),'no client secret-like token');
if(bad)process.exit(1);
diff --git a/tests/static-security.mjs b/tests/static-security.mjs
index 4a20c73..5b9f8a5 100644
--- a/tests/static-security.mjs
+++ b/tests/static-security.mjs
@@ -6,7 +6,7 @@ const readPub=p=>fs.readFileSync(path.join(pub,p),'utf8');
const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
const fail=m=>{console.error('FAIL:',m);process.exitCode=1};
const ok=m=>console.log('OK:',m);
-const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),ops=readPub('core/ops-ux-v1762.js');
+const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),ops=readPub('core/ops-ux-v1762.js'),hotfix=readPub('core/hotfix-v1763.js');
if(!html.includes('core/sun-safe.js'))fail('SunSafe must load before legacy modules');else ok('shared SunSafe loaded');
if(html.includes('offer-gallery-data.js')||fs.existsSync(path.join(pub,'offer-gallery-data.js')))fail('blocking offer-gallery-data.js still present');else ok('base64 gallery removed');
for(const raw of ['${b.name}','${o.event}','${o.address||','value="${x[0]}"','value="${x[2]}"']) if(legacy.includes(raw)) fail(`legacy bootstrap contains raw HTML interpolation ${raw}`);
@@ -27,10 +27,12 @@ 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-2-ops-ux')||!sw.includes('ops-ux-v1762.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated');
+if(!sw.includes('v17-6-3-developer-hotfix')||!sw.includes('hotfix-v1763.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated');
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('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}`);
-if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops))fail('possible secret in ops UX module');else ok('ops UX module has no hard-coded secret');
+if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops+hotfix)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops+hotfix))fail('possible secret in ops/hotfix module');else ok('ops/hotfix modules have no hard-coded secret');
if(process.exitCode)process.exit(process.exitCode);