From 2f7b8b9ea91c61322a96f94d568f5f96b31e7608 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Wed, 16 Sep 2026 18:01:55 +0300 Subject: [PATCH] feat: add banquet menu constructor tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new "Банкетное меню" catalog tab (category 6) alongside Боксы/ Премиум/Посуда for composing wedding/banquet/anniversary menus. Unlike the other tabs, dishes here are priced and weighed per guest, grouped by menu section (catalogSection), and clicking a checkbox doesn't add to the order directly -- it toggles inclusion in a live summary panel showing a running per-guest price table for the whole composed menu. The item editor gets two new fields (menu section, weight per guest) shown only for this category, reusing the existing generic item CRUD (editBox/saveBox) rather than building a parallel admin UI. Also re-bumped index.html's script cache-busting query string, which the previous whitelist-fix commit changed the content of app-runtime.js without updating -- the same stale-cache bug fixed earlier in the session, now closed for directly-tagged scripts too. Co-Authored-By: Claude Sonnet 5 --- public/app-runtime.js | 61 ++++++++++++++++++++++++++++++++++++--- public/index.html | 4 +-- tests/release-check.mjs | 4 +-- tests/static-security.mjs | 2 +- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/public/app-runtime.js b/public/app-runtime.js index 2dd8054..a497832 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -284,6 +284,29 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс @media print{.sun-doc-title-with-qr{gap:8mm}.sun-doc-qr{width:25mm;height:25mm}.sun-receipt .sun-receipt-party{padding:1.5mm 2mm;margin:1mm 0}.sun-receipt .sun-receipt-party-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:.5mm 2mm}} `; document.head.appendChild(style); + const banquetStyle=document.createElement('style'); + banquetStyle.id='sun-banquet-constructor-style'; + banquetStyle.textContent=` + #tiles .sun-banquet-constructor{grid-column:1/-1;display:flex;flex-direction:column;gap:16px} + .sun-banquet-list{min-width:0} + .sun-banquet-group{margin-bottom:6px} + .sun-banquet-group h4{margin:16px 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:#a3806f;border-bottom:1px solid #efe1d8;padding-bottom:5px} + .sun-banquet-group:first-child h4{margin-top:0} + .sun-banquet-row{display:flex;align-items:center;gap:10px;padding:8px 6px;border-radius:9px;cursor:pointer} + .sun-banquet-row:hover{background:#faf5f0} + .sun-banquet-row input[type=checkbox]{width:17px;height:17px;flex:0 0 auto;accent-color:#c0554f} + .sun-banquet-row-name{flex:1 1 auto;font-size:13px;font-weight:600;color:#2c2620;display:flex;flex-direction:column;gap:2px;min-width:0} + .sun-banquet-row-name small{font-weight:500;color:#8a7f74;font-size:11px} + .sun-banquet-row-price{font-weight:700;color:#c0554f;font-size:13px;white-space:nowrap} + .sun-banquet-edit{border:none;background:none;color:#b1a599;cursor:pointer;font-size:13px;padding:2px 6px;flex:0 0 auto} + .sun-banquet-edit:hover{color:#5b4f45} + .sun-banquet-summary{background:#fff;border:1px solid #efe1d8;border-radius:16px;padding:16px 18px} + .sun-banquet-summary h3{margin:0 0 10px;font-size:14px} + .sun-banquet-summary-row{display:flex;justify-content:space-between;gap:12px;padding:6px 0;font-size:12.5px;border-bottom:1px dashed #f1e7de} + .sun-banquet-summary-total{display:flex;justify-content:space-between;align-items:center;margin-top:10px;padding-top:10px;border-top:2px solid #2c2620;font-size:16px;font-weight:800} + @media(min-width:820px){#tiles .sun-banquet-constructor{flex-direction:row;align-items:flex-start}.sun-banquet-list{flex:1 1 auto}.sun-banquet-summary{flex:0 0 260px;position:sticky;top:12px}} + `; + document.head.appendChild(banquetStyle); // ---------------- Catalog categories + search ---------------- const CAT_KEY='sunCatalogCategoriesV2'; @@ -293,7 +316,8 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс {id:3,name:'Напитки',accent:'#4f9bd8',prep:false,hidden:false,order:2}, {id:2,name:'Дополнения',accent:'#56a889',prep:false,hidden:false,order:3}, {id:1,name:'Посуда',accent:'#a78bfa',prep:false,hidden:false,order:4}, - {id:4,name:'Доставка',accent:'#cf9f83',prep:false,hidden:false,order:5} + {id:4,name:'Доставка',accent:'#cf9f83',prep:false,hidden:false,order:5}, + {id:6,name:'Банкетное меню',accent:'#c0554f',prep:false,hidden:false,order:6} ]; function readCats(){ let saved=[];try{saved=JSON.parse(localStorage.getItem(CAT_KEY)||'[]');if(!Array.isArray(saved))saved=[]}catch(_){saved=[]} @@ -386,6 +410,20 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс } function ensureBoxPiecesEditor(){const weight=ensureBoxWeightEditor(),dialog=document.querySelector('#editor .dialog');if(!dialog)return null;let label=$('sunBoxPiecesLabel');if(!label){label=document.createElement('label');label.id='sunBoxPiecesLabel';label.className='sun-box-pieces-field';label.innerHTML='Количество канапе / шт. в боксеЭто общее количество готовых единиц в одном боксе. Не добавляйте его отдельной строкой в «Состав / ТТК».';weight?.insertAdjacentElement('afterend',label)}return label} function syncBoxWeightEditor(category,item){const label=ensureBoxWeightEditor(),input=$('boxWeightGrams'),piecesLabel=ensureBoxPiecesEditor(),piecesInput=$('boxPiecesCount');if(!label||!input)return;const isBox=[0,5].includes(Number(category));label.style.display=isBox?'flex':'none';if(piecesLabel)piecesLabel.style.display=isBox?'flex':'none';if(isBox){input.value=parseWeightGrams(item?.weight)||'';if(piecesInput)piecesInput.value=inferBoxPieces(item)||''}} + function ensureBanquetEditor(){ + const price=$('boxPrice'),dialog=document.querySelector('#editor .dialog');if(!price||!dialog)return null; + let section=$('sunBanquetSectionLabel'); + if(!section){section=document.createElement('label');section.id='sunBanquetSectionLabel';section.className='sun-box-weight-field';section.innerHTML='Раздел меню';price.closest('label')?.insertAdjacentElement('afterend',section)} + let weight=$('sunBanquetWeightLabel'); + if(!weight){weight=document.createElement('label');weight.id='sunBanquetWeightLabel';weight.className='sun-box-weight-field';weight.innerHTML='Вес на 1 гостя, г';section.insertAdjacentElement('afterend',weight)} + return {section,weight}; + } + function syncBanquetEditor(category,item){ + const fields=ensureBanquetEditor();if(!fields)return;const isBanquet=Number(category)===BANQUET_CATEGORY; + fields.section.style.display=isBanquet?'flex':'none';fields.weight.style.display=isBanquet?'flex':'none'; + const caption=$('boxPriceCaption');if(caption)caption.textContent=isBanquet?'Цена на 1 гостя, ₽':'Цена, ₽'; + if(isBanquet){if($('banquetSection'))$('banquetSection').value=item?.catalogSection||'';if($('banquetWeightGrams'))$('banquetWeightGrams').value=parseWeightGrams(item?.weight)||''} + } function installIngredientEditorV175(){window.renderIngredients=()=>{if(!edited)return;normalizeBoxItem(edited);const root=$('ingredients');if(!root)return;const head='
Вид / названиеКол-воЕд.Вес 1 шт., г
';root.innerHTML=head+edited.ingredients.map((row,i)=>{const w=ingredientUnitWeight(row);return `
`}).join('')};window.addIngredient=()=>{if(!edited)return;if(!Array.isArray(edited.ingredients))edited.ingredients=[];edited.ingredients.push(['Новая позиция',1,'шт.',0]);window.renderIngredients();};} installIngredientEditorV175(); function renderOrderLines(){ @@ -398,9 +436,24 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс function boxNumber(item){const explicit=Number(item?.boxNumber);if(Number.isInteger(explicit)&&explicit>=0)return explicit;const name=String(item?.name||'');const match=name.match(/(?:бокс\s*)?№\s*0*(\d+)/i)||name.match(/^\s*№\s*0*(\d+)/i);return match?Number(match[1]):null} function wantedBoxNumber(query){const raw=String(query||'').trim();if(!raw)return null;const match=raw.match(/^(?:бокс\s*)?(?:№\s*)?0*(\d+)\s*$/i);return match?Number(match[1]):null} function itemMatches(item,query){if(!query)return true;if(Number(activeCat)===0){const wanted=wantedBoxNumber(query);return wanted!==null&&boxNumber(item)===wanted}const q=query.trim().toLowerCase();const parts=[item.name,item.catalogSection,item.weight,...(Array.isArray(item.composition)?item.composition:[]),...(Array.isArray(item.ingredients)?item.ingredients.map(x=>x?.[0]):[])];return parts.join(' ').toLowerCase().includes(q)} + const BANQUET_CATEGORY=6; + function banquetSelection(){if(!Array.isArray(draft.banquetSelection))draft.banquetSelection=[];return draft.banquetSelection} + window.toggleBanquetItem=id=>{const sel=banquetSelection();const i=sel.indexOf(String(id));if(i>=0)sel.splice(i,1);else sel.push(String(id));renderCatalogV5();}; + function renderBanquetView(items){ + const sel=new Set(banquetSelection().map(String)); + const groups=new Map(); + items.forEach(item=>{const g=String(item.catalogSection||'Без раздела').trim()||'Без раздела';if(!groups.has(g))groups.set(g,[]);groups.get(g).push(item)}); + const groupsHtml=[...groups.entries()].map(([section,list])=>`

${esc(section)}

${list.map(item=>``).join('')}
`).join(''); + const selectedItems=items.filter(item=>sel.has(String(item.id))); + const total=selectedItems.reduce((s,i)=>s+Number(i.price||0),0); + const summaryRows=selectedItems.map(item=>`
${esc(item.name)}${money(item.price||0)}
`).join(''); + const summaryHtml=`

Меню на 1 гостя

${selectedItems.length?summaryRows:'

Отметьте блюда слева.

'}
Итого на 1 гостя${money(total)}
`; + return `
${groups.size?groupsHtml:'
В разделе «Банкетное меню» пока нет позиций.
'}
${summaryHtml}
`; + } function renderCatalogV5(){ renderCategoryTabs();ensureCatalogTools();const catalogSearch=$('sunCatalogSearch');if(catalogSearch){const boxMode=[0,5].includes(Number(activeCat));catalogSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';catalogSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#new .catalog h1');if(title)title.textContent=catName(activeCat); const all=boxes.filter(item=>Number(item.category||0)===Number(activeCat)),items=all.filter(item=>itemMatches(item,catalogQuery));const cat=catById(activeCat)||{name:'Каталог',prep:true}; + if(Number(activeCat)===BANQUET_CATEGORY){$('tiles').innerHTML=renderBanquetView(items);renderOrderLines();updateOrderSummary();return;} const cards=items.map(item=>``).join(''); const addText=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию'); const empty=items.length?'':`
${catalogQuery?'По вашему запросу ничего не найдено.':`В разделе «${esc(cat.name)}» пока нет позиций.`}
`; @@ -423,11 +476,11 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс window.openManager=()=>{managerQuery='';ensureManagerToolbar();if($('sunManagerSearch'))$('sunManagerSearch').value='';renderManagerV5();window.modal?.('manager');}; window.editBox=id=>{ edited=id?structuredClone(boxes.find(x=>String(x.id)===String(id))):{id:'',name:Number(activeCat)===0?'Новый бокс':(Number(activeCat)===5?'Новая премиум позиция':'Новая позиция'),category:Number(activeCat),price:0,oldPrice:0,ingredients:[]};if(!edited)return; - if(!Array.isArray(edited.ingredients))edited.ingredients=[];normalizeBoxItem(edited);syncEditorCategoryOptions();$('itemCategory').value=String(edited.category??activeCat);$('boxName').value=edited.name||'';$('boxPrice').value=Number(edited.price||0);if($('boxOldPrice'))$('boxOldPrice').value=Number(edited.oldPrice||0);if($('boxCompositionText'))$('boxCompositionText').value=(Array.isArray(edited.composition)?edited.composition:[]).join('\n');syncBoxWeightEditor(edited.category??activeCat,edited);$('editPhoto').style.display=edited.photo?'block':'none';$('editPhoto').src=edited.photo||'';$('deleteBox').style.display=edited.id?'block':'none'; + if(!Array.isArray(edited.ingredients))edited.ingredients=[];normalizeBoxItem(edited);syncEditorCategoryOptions();$('itemCategory').value=String(edited.category??activeCat);$('boxName').value=edited.name||'';$('boxPrice').value=Number(edited.price||0);if($('boxOldPrice'))$('boxOldPrice').value=Number(edited.oldPrice||0);if($('boxCompositionText'))$('boxCompositionText').value=(Array.isArray(edited.composition)?edited.composition:[]).join('\n');syncBoxWeightEditor(edited.category??activeCat,edited);syncBanquetEditor(edited.category??activeCat,edited);$('editPhoto').style.display=edited.photo?'block':'none';$('editPhoto').src=edited.photo||'';$('deleteBox').style.display=edited.id?'block':'none'; const cat=catById(edited.category)||{name:'Каталог',prep:true};$('editTitle').textContent=edited.id?`Изменить: ${cat.name}`:(Number(edited.category)===0?'Новый бокс':(Number(edited.category)===5?'Новая премиум позиция':'Новая позиция'));const zone=document.querySelector('#editor .ingredient-zone');if(zone)zone.style.display=cat.prep?'block':'none';window.renderIngredients?.();window.modal?.('editor'); }; - if($('itemCategory'))$('itemCategory').onchange=function(){if(!edited)return;edited.category=Number(this.value);const c=catById(edited.category)||{name:'Каталог',prep:true};const zone=document.querySelector('#editor .ingredient-zone');if(zone)zone.style.display=c.prep?'block':'none';syncBoxWeightEditor(edited.category,edited);$('editTitle').textContent=edited.id?`Изменить: ${c.name}`:(Number(edited.category)===0?'Новый бокс':(Number(edited.category)===5?'Новая премиум позиция':'Новая позиция'));}; - window.saveBox=()=>{if(!edited)return;edited.name=$('boxName').value.trim();edited.price=Math.max(0,Number($('boxPrice').value||0));edited.oldPrice=Math.max(0,Number($('boxOldPrice')?.value||0));if(!(edited.oldPrice>edited.price))delete edited.oldPrice;edited.sale=Boolean(edited.oldPrice&&edited.oldPrice>edited.price);edited.category=Number($('itemCategory').value);if([0,5].includes(edited.category)){const grams=Math.max(0,Math.round(Number($('boxWeightGrams')?.value||0)));edited.weight=grams?`${grams} г`:'';edited.pieces=Math.max(0,Math.round(Number($('boxPiecesCount')?.value||0)));}edited.ingredients=Array.isArray(edited.ingredients)?edited.ingredients:[];if([0,5].includes(edited.category)&&$('boxCompositionText')){const next=$('boxCompositionText').value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean);const before=JSON.stringify(Array.isArray(edited.composition)?edited.composition:[]);edited.composition=next;if(JSON.stringify(next)!==before)edited.compositionSource='manual';}normalizeBoxItem(edited,{cleanup:true});if(!edited.name)return alert('Введите название.');if(!edited.id){edited.id=(typeof sunUUID==='function'?sunUUID():`item-${Date.now()}`);boxes.push(edited)}else{const i=boxes.findIndex(x=>String(x.id)===String(edited.id));if(i>=0)boxes[i]=edited}if(!catById(edited.category)?.hidden)activeCat=edited.category;persist();closeModal('editor');closeModal('manager');renderCatalogV5();renderOrders?.();window.sunClientOfferCatalogChanged?.(edited.id);}; + if($('itemCategory'))$('itemCategory').onchange=function(){if(!edited)return;edited.category=Number(this.value);const c=catById(edited.category)||{name:'Каталог',prep:true};const zone=document.querySelector('#editor .ingredient-zone');if(zone)zone.style.display=c.prep?'block':'none';syncBoxWeightEditor(edited.category,edited);syncBanquetEditor(edited.category,edited);$('editTitle').textContent=edited.id?`Изменить: ${c.name}`:(Number(edited.category)===0?'Новый бокс':(Number(edited.category)===5?'Новая премиум позиция':'Новая позиция'));}; + window.saveBox=()=>{if(!edited)return;edited.name=$('boxName').value.trim();edited.price=Math.max(0,Number($('boxPrice').value||0));edited.oldPrice=Math.max(0,Number($('boxOldPrice')?.value||0));if(!(edited.oldPrice>edited.price))delete edited.oldPrice;edited.sale=Boolean(edited.oldPrice&&edited.oldPrice>edited.price);edited.category=Number($('itemCategory').value);if([0,5].includes(edited.category)){const grams=Math.max(0,Math.round(Number($('boxWeightGrams')?.value||0)));edited.weight=grams?`${grams} г`:'';edited.pieces=Math.max(0,Math.round(Number($('boxPiecesCount')?.value||0)));}if(edited.category===BANQUET_CATEGORY){edited.catalogSection=($('banquetSection')?.value||'').trim();const grams=Math.max(0,Math.round(Number($('banquetWeightGrams')?.value||0)));edited.weight=grams?`${grams} г`:'';}edited.ingredients=Array.isArray(edited.ingredients)?edited.ingredients:[];if([0,5].includes(edited.category)&&$('boxCompositionText')){const next=$('boxCompositionText').value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean);const before=JSON.stringify(Array.isArray(edited.composition)?edited.composition:[]);edited.composition=next;if(JSON.stringify(next)!==before)edited.compositionSource='manual';}normalizeBoxItem(edited,{cleanup:true});if(!edited.name)return alert('Введите название.');if(!edited.id){edited.id=(typeof sunUUID==='function'?sunUUID():`item-${Date.now()}`);boxes.push(edited)}else{const i=boxes.findIndex(x=>String(x.id)===String(edited.id));if(i>=0)boxes[i]=edited}if(!catById(edited.category)?.hidden)activeCat=edited.category;persist();closeModal('editor');closeModal('manager');renderCatalogV5();renderOrders?.();window.sunClientOfferCatalogChanged?.(edited.id);}; window.removeBox=()=>{if(!edited?.id||!confirm(`Удалить «${edited.name}» из каталога?`))return;boxes=boxes.filter(x=>String(x.id)!==String(edited.id));draft.lines=(draft.lines||[]).filter(x=>String(x.id)!==String(edited.id));persist();closeModal('editor');closeModal('manager');renderCatalogV5();renderOrders?.();}; function fileAsDataUrl(file){return new Promise((resolve,reject)=>{const reader=new FileReader();reader.onerror=()=>reject(reader.error||new Error('FileReader error'));reader.onload=()=>resolve(String(reader.result||''));reader.readAsDataURL(file)})} diff --git a/public/index.html b/public/index.html index 33dcc89..32cbc3a 100644 --- a/public/index.html +++ b/public/index.html @@ -95,7 +95,7 @@ button{touch-action:manipulation} #sunGlobalSearchBtn kbd{display:none!important} #sunSyncSettingsCard>div[style*="grid-template-columns"]{grid-template-columns:1fr!important} -}
Caterium

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

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

К оплате0 ₽

Заказы

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

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

Склад

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

Статистика

- + diff --git a/tests/release-check.mjs b/tests/release-check.mjs index 3e57ea4..4877597 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -15,7 +15,7 @@ check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no P 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('v81-20260912-account-center-loader')&&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('20260916-v18-1-0-cache-bust-fix')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); +check(index.includes('20260916-v18-1-2-banquet-menu-tab')&&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'); @@ -65,7 +65,7 @@ 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('20260916-v18-1-0-cache-bust-fix'),'index cache bust is v17.7.3'); +check(index.includes('20260916-v18-1-2-banquet-menu-tab'),'index cache bust is v17.7.3'); check(sw.includes('v81-20260912-account-center-loader')&&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'); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index e0f7321..0139c1e 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -29,7 +29,7 @@ if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60 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('20260912-account-center-loader')||!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('20260916-v18-1-0-cache-bust-fix')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); +if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260916-v18-1-2-banquet-menu-tab')||!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');