feat: add banquet menu constructor tab

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 <noreply@anthropic.com>
This commit is contained in:
pavlov346346-source 2026-09-16 18:01:55 +03:00
parent 7446a551f6
commit 2f7b8b9ea9
4 changed files with 62 additions and 9 deletions

View File

@ -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='<span>Количество канапе / шт. в боксе</span><input id="boxPiecesCount" type="number" min="0" step="1" inputmode="numeric" placeholder="Например, 30"><small>Это общее количество готовых единиц в одном боксе. Не добавляйте его отдельной строкой в «Состав / ТТК».</small>';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='<span>Раздел меню</span><input id="banquetSection" type="text" placeholder="Например: Рыбные закуски">';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='<span>Вес на 1 гостя, г</span><input id="banquetWeightGrams" type="number" min="0" step="1" inputmode="numeric" placeholder="Например, 30">';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='<div class="sun-ingredient-head"><span>Вид / название</span><span>Кол-во</span><span>Ед.</span><span>Вес 1 шт., г</span><span></span></div>';root.innerHTML=head+edited.ingredients.map((row,i)=>{const w=ingredientUnitWeight(row);return `<div class="ingredient sun-ingredient-v175"><input value="${esc(cleanIngredientName(row[0]))}" onchange="edited.ingredients[${i}][0]=this.value"><input type="number" min="0" step="0.01" value="${Number(row[1]||0)}" onchange="edited.ingredients[${i}][1]=+this.value"><input value="${esc(row[2]||'шт.')}" onchange="edited.ingredients[${i}][2]=this.value"><input type="number" min="0" step="0.1" inputmode="decimal" placeholder="г" value="${w||''}" onchange="edited.ingredients[${i}][3]=Math.max(0,+this.value||0)"><button type="button" onclick="edited.ingredients.splice(${i},1);renderIngredients()">×</button></div>`}).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])=>`<div class="sun-banquet-group"><h4>${esc(section)}</h4>${list.map(item=>`<label class="sun-banquet-row"><input type="checkbox" onchange="toggleBanquetItem('${esc(item.id)}')" ${sel.has(String(item.id))?'checked':''}><span class="sun-banquet-row-name">${esc(item.name)}${item.weight?`<small>${esc(item.weight)}</small>`:''}</span><span class="sun-banquet-row-price">${money(item.price||0)}</span><button type="button" class="sun-banquet-edit" onclick="event.preventDefault();editBox('${esc(item.id)}')" title="Изменить">✎</button></label>`).join('')}</div>`).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=>`<div class="sun-banquet-summary-row"><span>${esc(item.name)}</span><b>${money(item.price||0)}</b></div>`).join('');
const summaryHtml=`<div class="sun-banquet-summary"><h3>Меню на 1 гостя</h3>${selectedItems.length?summaryRows:'<p class="empty">Отметьте блюда слева.</p>'}<div class="sun-banquet-summary-total"><span>Итого на 1 гостя</span><b>${money(total)}</b></div></div>`;
return `<div class="sun-banquet-constructor"><div class="sun-banquet-list"><button class="tile add" type="button" onclick="editBox(null)"><br>Добавить блюдо</button>${groups.size?groupsHtml:'<div class="catalog-empty">В разделе «Банкетное меню» пока нет позиций.</div>'}</div>${summaryHtml}</div>`;
}
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=>`<button class="tile" type="button" onclick="add('${esc(item.id)}')" title="Добавить в заказ">${item.photo?`<img src="${esc(item.photo)}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='sun-logo.png'">`:'<div class="ph"><img src="sun-logo.png" class="sun-ph-logo" alt="Логотип Солнце Кейтеринг"></div>'}<span>${esc(item.name)}</span>${item.catalogSection?`<small class="tile-section">${esc(item.catalogSection)}</small>`:''}${(item.weight||inferBoxPieces(item))?`<small class="tile-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}<span class="tile-price-wrap"><small class="tile-price">${money(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${Number(item.oldPrice||0)>Number(item.price||0)?`<span class="sale-badge">АКЦИЯ</span>`:''}</button>`).join('');
const addText=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');
const empty=items.length?'':`<div class="catalog-empty">${catalogQuery?'По вашему запросу ничего не найдено.':`В разделе «${esc(cat.name)}» пока нет позиций.`}</div>`;
@ -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)})}

File diff suppressed because one or more lines are too long

View File

@ -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');

View File

@ -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');