diff --git a/docs/release-manifest.json b/docs/release-manifest.json index 773fce8..ebd38af 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v98-20260918-trial-extras", + "pwaCache": "v99-20260918-mobile-order", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, diff --git a/package.json b/package.json index 4356901..998d29b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "17.7.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/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js && node --check public/core/trial-demo.js && node --check public/core/proposal-layout.js", + "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js && node --check public/core/trial-demo.js && node --check public/core/proposal-layout.js && node --check public/core/mobile-order.js", "test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs", "check:release": "node tests/release-check.mjs", "check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db", diff --git a/public/core/mobile-order.js b/public/core/mobile-order.js new file mode 100644 index 0000000..e16ba11 --- /dev/null +++ b/public/core/mobile-order.js @@ -0,0 +1,38 @@ +(()=>{ + 'use strict'; + function boot(){ + const view=document.getElementById('new'),work=view?.querySelector('.work'); + const catalog=work?.querySelector(':scope > .catalog'),order=work?.querySelector(':scope > section.card'); + if(!work||!catalog||!order||document.getElementById('ctOrderTop'))return; + const mobile=matchMedia('(max-width:760px)'); + const button=document.createElement('button');button.id='ctOrderTop';button.type='button';button.hidden=true; + button.setAttribute('aria-label','Наверх к заказу');button.title='Наверх к заказу'; + button.innerHTML='Наверх'; + document.body.appendChild(button); + const style=document.createElement('style');style.textContent=` + #ctOrderTop{display:none} + @media(max-width:760px){ + #new.on{display:flex!important;flex-direction:column} + #new .work{order:-1;width:100%} + #new .work.sun-resizable-work>section.card.sun-order-pane{order:-1!important;grid-column:1!important;grid-row:1!important;position:static!important} + #new .work.sun-resizable-work>.catalog{order:0!important;grid-column:1!important;grid-row:2!important} + #ctOrderTop:not([hidden]){display:flex;align-items:center;justify-content:center;gap:6px;position:fixed;right:16px;bottom:calc(88px + env(safe-area-inset-bottom,0px));z-index:900;min-width:48px;min-height:48px;padding:10px 14px;border:1px solid #d7bb65;border-radius:24px;background:#f6e7af;color:#302919;box-shadow:0 4px 18px #0002;font:700 13px/1.2 Arial,sans-serif;cursor:pointer} + #ctOrderTop:focus-visible{outline:3px solid #546747;outline-offset:3px} + } + @media print{#ctOrderTop{display:none!important}} + `;document.head.appendChild(style); + function visibility(){button.hidden=!mobile.matches||!view.classList.contains('on')||document.body.classList.contains('sun-cloud-auth-required')||window.scrollY<300;} + function layout(){ + // Move the existing node, preserving inputs, listeners and the current draft. + if(mobile.matches){if(order.nextElementSibling!==catalog)work.insertBefore(order,catalog);} + else{const splitter=work.querySelector(':scope > .sun-work-splitter');const after=splitter||catalog;if(after.nextElementSibling!==order)after.insertAdjacentElement('afterend',order);} + visibility(); + } + let frame=0;function update(){if(frame)return;frame=requestAnimationFrame(()=>{frame=0;visibility()});} + button.addEventListener('click',()=>{window.scrollTo({top:0,left:0,behavior:'instant'});order.setAttribute('tabindex','-1');order.focus({preventScroll:true});visibility();}); + window.addEventListener('scroll',update,{passive:true});mobile.addEventListener('change',layout); + const observer=new MutationObserver(update);observer.observe(view,{attributes:true,attributeFilter:['class']});observer.observe(document.body,{attributes:true,attributeFilter:['class']}); + layout(); + } + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot(); +})(); diff --git a/public/core/performance.js b/public/core/performance.js index 0bd816f..23fb66c 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; const VERSION='17.7.3'; - const RELEASE='20260918-trial-extras'; + const RELEASE='20260918-mobile-order'; const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}}; function installAuthBoot(){ diff --git a/public/index.html b/public/index.html index e80934d..c6d5ad4 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ - - + diff --git a/public/service-worker.js b/public/service-worker.js index b0d2eb8..1cd81e4 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,7 +1,7 @@ -const CACHE='sun-catering-pwa-v98-20260918-trial-extras'; -const VERSION='20260918-trial-extras'; +const CACHE='sun-catering-pwa-v99-20260918-mobile-order'; +const VERSION='20260918-mobile-order'; const CORE=[ - './','./index.html',`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`, + './','./index.html',`./core/mobile-order.js?v=${VERSION}`,`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`, `./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/login-signature-v1776.css?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, './offer-gallery/001.jpg','./offer-gallery/002.jpg', './catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg', @@ -9,7 +9,7 @@ const CORE=[ './offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg' ]; const CRITICAL_FRESH=new Set([ - '/core/cloud-transport.js','/core/trial-demo.js','/core/proposal-layout.js', + '/core/mobile-order.js','/core/cloud-transport.js','/core/trial-demo.js','/core/proposal-layout.js', '/core/banquet-menu.js','/core/access-policy.js','/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/login-signature-v1776.css','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js' ]); self.addEventListener('install',event=>{ diff --git a/tests/release-check.mjs b/tests/release-check.mjs index 2db88d5..48cf28a 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent') check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer'); check(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('v98-20260918-trial-extras')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3'); -check(index.includes('20260918-trial-extras')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); +check(sw.includes('v99-20260918-mobile-order')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3'); +check(index.includes('20260918-mobile-order')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned'); check(performance.includes("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'); @@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30 check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match'); check(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('v98-20260918-trial-extras'),'release manifest points to current PWA cache'); +check(String(releaseManifest.pwaCache||'').includes('v99-20260918-mobile-order'),'release manifest points to current PWA cache'); check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3'); check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards'); @@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer'); check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes'); check(pkg.version==='17.7.3','package version is v17.7.3'); -check(index.includes('20260918-trial-extras'),'index cache bust is v17.7.3'); -check(sw.includes('v98-20260918-trial-extras')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); +check(index.includes('20260918-mobile-order'),'index cache bust is v17.7.3'); +check(sw.includes('v99-20260918-mobile-order')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist'); check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3'); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index 5994a1a..ca9298b 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos'); 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('20260918-trial-extras')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh'); -if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-trial-extras')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); +if(!sw.includes('20260918-mobile-order')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh'); +if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-mobile-order')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present'); if(!performance.includes("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'); diff --git a/tests/trial-demo.spec.mjs b/tests/trial-demo.spec.mjs index 5a20746..425b572 100644 --- a/tests/trial-demo.spec.mjs +++ b/tests/trial-demo.spec.mjs @@ -114,6 +114,32 @@ test('trial extras populate every tab and supplies reach shopping and stock',asy expect(await page.evaluate(()=>JSON.parse(localStorage.sunStock).find(p=>p.id==='demo-extras-v1-stock-water').qty)).toBe(3); }); +test('phone shows the order before boxes and the top arrow preserves the draft',async({page})=>{ + await ready(page); + const phone=page.viewportSize().width<=760; + const order=page.locator('#new .work > section.card'),catalog=page.locator('#new .work > .catalog'); + const bounds=await Promise.all([order.boundingBox(),catalog.boundingBox()]); + if(!phone){expect(bounds[1].x).toBeLessThan(bounds[0].x);await expect(page.locator('#ctOrderTop')).toBeHidden();return;} + expect(bounds[0].y+bounds[0].height).toBeLessThanOrEqual(bounds[1].y); + expect(await order.evaluate(el=>el.nextElementSibling?.classList.contains('catalog'))).toBe(true); + await page.locator('#deliveryCost').fill('600');await page.locator('#prepayment').fill('100'); + await page.locator('#tiles .tile').filter({hasText:'Брускетты с томатами'}).click(); + await expect(page.locator('#ctOrderTop')).toBeVisible(); + expect(await page.evaluate(()=>draft.lines[0].qty)).toBe(1); + await page.getByRole('button',{name:'Наверх к заказу',exact:true}).click(); + await expect.poll(()=>page.evaluate(()=>window.scrollY)).toBe(0); + await expect(page.locator('#deliveryCost')).toHaveValue('600');await expect(page.locator('#prepayment')).toHaveValue('100'); + expect(await page.evaluate(()=>draft.lines[0].qty)).toBe(1); + await expect(page.locator('#ctOrderTop')).toBeHidden(); + await page.setViewportSize({width:1440,height:900}); + await expect.poll(()=>order.evaluate(el=>el.previousElementSibling?.classList.contains('sun-work-splitter'))).toBe(true); + const wide=await Promise.all([order.boundingBox(),catalog.boundingBox()]);expect(wide[1].x).toBeLessThan(wide[0].x); + await page.setViewportSize({width:390,height:844}); + await expect.poll(()=>order.evaluate(el=>el.nextElementSibling?.classList.contains('catalog'))).toBe(true); + await expect(page.locator('#deliveryCost')).toHaveValue('600'); + expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true); +}); + test('owner onboarding preserves server demo when saving the company profile',async({page})=>{ await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:''}));await page.goto('/index.html'); await page.addScriptTag({url:'/core/sun-safe.js'});await page.addScriptTag({url:'/core/cloud-transport.js'});