From 03cb1e9286c4b0b74389a52f9956ba514fa269e7 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Tue, 8 Sep 2026 07:21:43 +0300 Subject: [PATCH] Add v17.6.7 classic offer patcher --- .github/scripts/patch-v1767-classic-offer.mjs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/scripts/patch-v1767-classic-offer.mjs diff --git a/.github/scripts/patch-v1767-classic-offer.mjs b/.github/scripts/patch-v1767-classic-offer.mjs new file mode 100644 index 0000000..d4abb5f --- /dev/null +++ b/.github/scripts/patch-v1767-classic-offer.mjs @@ -0,0 +1,97 @@ +import fs from 'node:fs'; + +const VERSION='17.6.7'; +const RELEASE='20260908-v17-6-7-classic-offer-pdf'; +const read=f=>fs.readFileSync(f,'utf8'); +const write=(f,s)=>fs.writeFileSync(f,s); +function replaceOnce(s,a,b,label){const n=s.split(a).length-1;if(n!==1)throw new Error(`${label}: expected 1, got ${n}`);return s.replace(a,b)} + +// Fix and harden the new classic cover module. +let classic=read('public/core/classic-offer-pdf-v1767.js'); +classic=classic.replace("if(hero)coverImage(ctx,540,170,406,670,0);if(hero)coverImage(ctx,hero,540,170,406,670,0);","if(hero)coverImage(ctx,hero,540,170,406,670,0);"); +classic=classic.replace("const money=v=>Math.round(Number(v||0)).toLocaleString('ru-RU')+' ₽';","const money=v=>Math.round(Number(v||0)).toLocaleString('ru-RU')+' ₽';\n const classicBoxCount=s=>(s?.items||[]).filter(i=>[0,5].includes(Number(i?.categoryId))).reduce((sum,i)=>sum+Math.max(0,Number(i?.qty||0)),0);"); +classic=classic.replaceAll("String(Math.round(Number(s.boxCount||0)))","String(Math.round(classicBoxCount(s)))"); +classic=classic.replaceAll("s.boxCount||0","classicBoxCount(s)||0"); +write('public/core/classic-offer-pdf-v1767.js',classic); + +// Runtime: restore the hidden templates and route every preview/download through the same page renderer. +let runtime=read('public/app-runtime.js'); +const oldIds="const OFFER_TEMPLATE_IDS=new Set(['light','editorial-grid','midnight-glass','emerald-gold']);"; +const newIds="const OFFER_TEMPLATE_IDS=new Set(['light','midnight-glass','editorial-grid','warm-sun','bento-cards','event-story','black-gold','personal-letter','event-ticket','solar-experience','midnight-compact','emerald-gold','neon-emerald']);"; +const idCount=runtime.split(oldIds).length-1;if(idCount<1)throw new Error(`OFFER_TEMPLATE_IDS: expected >=1, got ${idCount}`);runtime=runtime.split(oldIds).join(newIds); +const oldTemplates=` const TEMPLATES=[ + {id:'light',name:'Светлый фирменный',desc:'Классическая A4-композиция: обложка, карточки меню, итог и условия.',thumb:'offer-templates/thumb-light.jpg'}, + {id:'editorial-grid',name:'Солнце Editorial',desc:'Журнальная композиция: крупный первый экран, меню-сетка, фотоблоки и редакционная подача.',thumb:'offer-templates/thumb-editorial-grid.jpg'}, + {id:'midnight-glass',name:'Midnight Glass',desc:'Тёмная презентация: стеклянные панели, итог до меню и контрастная двухколоночная композиция.',thumb:'offer-templates/thumb-midnight-glass.jpg'}, + {id:'emerald-gold',name:'Emerald Gold',desc:'Премиальный буклет: золотой итог на первом экране, условия и меню как финальная часть.',thumb:'offer-templates/thumb-emerald-gold.jpg'} + ];`; +const newTemplates=` const TEMPLATES=[ + {id:'light',name:'Минимализм',desc:'Светлая премиальная классика: крупная обложка, фотография, факты события и чистая сетка.',thumb:'offer-templates/thumb-light.jpg'}, + {id:'midnight-glass',name:'Luxury Dark',desc:'Тёмная ресторанная презентация с золотыми акцентами и крупной food-фотографией.',thumb:'offer-templates/thumb-midnight-glass.jpg'}, + {id:'editorial-grid',name:'Editorial Magazine',desc:'Редакционный журнальный макет с нумерацией разделов, крупной типографикой и фотополосой.',thumb:'offer-templates/thumb-editorial-grid.jpg'}, + {id:'warm-sun',name:'Warm Sun',desc:'Тёплый фирменный дизайн в солнечной палитре с мягкими формами и сценарием подготовки.',thumb:'offer-templates/thumb-light.jpg'}, + {id:'bento-cards',name:'Bento Cards',desc:'Современная карточная композиция: метрики, фото, преимущества и итог собраны в bento-сетку.',thumb:'offer-templates/thumb-light.jpg'}, + {id:'event-story',name:'Event Story',desc:'Предложение как история события: таймлайн от заявки до готового стола и большая фотография.',thumb:'offer-templates/thumb-editorial-grid.jpg'}, + {id:'black-gold',name:'Food First',desc:'Максимальный акцент на еде: полноэкранное фото, чёрный фон и золотая ресторанная подача.',thumb:'offer-templates/thumb-midnight-glass.jpg'}, + {id:'personal-letter',name:'Personal Letter',desc:'Персональное предложение в форме красивого письма клиенту с мягкой типографикой.',thumb:'offer-templates/thumb-light.jpg'}, + {id:'event-ticket',name:'Event Ticket',desc:'Дизайн приглашения/билета: данные события, меню, итог и декоративный ticket-блок.',thumb:'offer-templates/thumb-editorial-grid.jpg'}, + {id:'solar-experience',name:'Solar Experience',desc:'Яркий фирменный солнечный макет с орбитальной композицией и сильным первым экраном.',thumb:'offer-templates/thumb-light.jpg'}, + {id:'midnight-compact',name:'Midnight Compact · архив',desc:'Возвращённый скрытый компактный тёмный шаблон.',thumb:'offer-templates/thumb-midnight-glass.jpg'}, + {id:'emerald-gold',name:'Emerald Gold · архив',desc:'Возвращённый премиальный изумрудно-золотой шаблон.',thumb:'offer-templates/thumb-emerald-gold.jpg'}, + {id:'neon-emerald',name:'Neon Emerald · архив',desc:'Возвращённый скрытый неоново-изумрудный шаблон.',thumb:'offer-templates/thumb-midnight-glass.jpg'} + ];`; +runtime=replaceOnce(runtime,oldTemplates,newTemplates,'template catalog'); +runtime=replaceOnce(runtime,' async function renderOfferPdfPages(s){',' async function renderOfferPdfPagesBase(s){','rename base pdf renderer'); +const insertion=` return canvases;\n }\n async function pdfBlobFromPages(canvases)`; +const wrapper=` return canvases;\n }\n async function renderOfferPdfPages(s){\n const classic=window.SunClassicOfferPDFV1767;\n if(classic?.renderPages)return classic.renderPages(s,renderOfferPdfPagesBase);\n return renderOfferPdfPagesBase(s);\n }\n async function pdfBlobFromPages(canvases)`; +runtime=replaceOnce(runtime,insertion,wrapper,'classic pdf wrapper'); +write('public/app-runtime.js',runtime); + +// Per-order picker must accept the restored IDs. +let ux=read('public/core/ux-fixes-v1764.js'); +ux=replaceOnce(ux," const VERSION='17.6.6';"," const VERSION='17.6.7';",'ux version'); +ux=replaceOnce(ux," const TEMPLATE_IDS=new Set(['light','editorial-grid','midnight-glass','emerald-gold']);"," const TEMPLATE_IDS=new Set(['light','midnight-glass','editorial-grid','warm-sun','bento-cards','event-story','black-gold','personal-letter','event-ticket','solar-experience','midnight-compact','emerald-gold','neon-emerald']);",'ux ids'); +ux=ux.replace('Шаблон закрепляется за этим предложением. Миниатюра повторяет структуру PDF.','Классические и архивные шаблоны. Предварительный просмотр совпадает со скачиваемым PDF.'); +write('public/core/ux-fixes-v1764.js',ux); + +// Performance version only; classic module loads synchronously from index for deterministic PDF preview. +let perf=read('public/core/performance.js'); +perf=replaceOnce(perf," const VERSION='17.6.6';\n const RELEASE='20260908-v17-6-6-offer-templates';",` const VERSION='${VERSION}';\n const RELEASE='${RELEASE}';`,'performance version'); +write('public/core/performance.js',perf); + +// Cache-busting + direct deterministic module load before the app runtime. +let index=read('public/index.html'); +index=index.split('20260908-v17-6-6-offer-templates').join(RELEASE); +const indexNeedle=``; +if(!index.includes(indexNeedle))throw new Error('pdf-engine index script not found'); +index=index.replace(indexNeedle,indexNeedle+``); +write('public/index.html',index); + +let sw=read('public/service-worker.js'); +sw=sw.replace(/const CACHE='sun-catering-pwa-v\d+-[^']+';\nconst VERSION='[^']+';/,`const CACHE='sun-catering-pwa-v72-${RELEASE}';\nconst VERSION='${RELEASE}';`); +sw=replaceOnce(sw,'`./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}`,','`./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}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,','service worker core'); +write('public/service-worker.js',sw); + +const pkg=JSON.parse(read('package.json'));pkg.version=VERSION;pkg.scripts['check:syntax']=pkg.scripts['check:syntax'].replace('node --check public/core/pdf-engine.js','node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js');write('package.json',JSON.stringify(pkg,null,2)+'\n'); +const lock=JSON.parse(read('package-lock.json'));lock.version=VERSION;if(lock.packages?.[''])lock.packages[''].version=VERSION;write('package-lock.json',JSON.stringify(lock,null,2)+'\n'); + +const manifestPath='docs/release-manifest.json',manifest=JSON.parse(read(manifestPath));manifest.version='v17.6.7';manifest.pwaCache=`v72-${RELEASE}`;manifest.release=RELEASE;manifest.clientOfferTemplates=['light','midnight-glass','editorial-grid','warm-sun','bento-cards','event-story','black-gold','personal-letter','event-ticket','solar-experience','midnight-compact','emerald-gold','neon-emerald'];manifest.offerTemplates=13;manifest.classicOfferTemplates=10;manifest.restoredHiddenOfferTemplates=['midnight-compact','emerald-gold','neon-emerald'];manifest.clientOfferPreviewUsesPdfPages=true;manifest.clientOfferAllTemplatesPreviewPdfParity=true;manifest.classicOfferPdfCoverPages=true;manifest.notes='Restored the earlier presentation-style client PDF experience: 10 classic visual concepts plus 3 hidden archive templates. Preview and download are built from the exact same canvas pages.';write(manifestPath,JSON.stringify(manifest,null,2)+'\n'); +write('docs/releases/V17.6.7-CHANGES.txt',`Caterium v17.6.7 — Classic client proposal PDFs\nDate: 2026-09-08\n\n- Restored presentation-style client proposal PDFs based on the earlier beautiful proposal references.\n- 10 classic designs: Минимализм, Luxury Dark, Editorial Magazine, Warm Sun, Bento Cards, Event Story, Food First, Personal Letter, Event Ticket, Solar Experience.\n- Restored hidden archive templates: Midnight Compact, Emerald Gold, Neon Emerald.\n- Every classic template gets a truly different A4 cover composition, not just another palette.\n- Preview and downloaded PDF use the exact same canvas page objects.\n- Existing per-order template selection remains authoritative.\n- PWA cache v72 / ${RELEASE}.\n`); + +// Keep release/security tests current and add v17.6.7 guards. +let staticTest=read('tests/static-security.mjs'); +staticTest=staticTest.replace("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'),ux=readPub('core/ux-fixes-v1764.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'),ux=readPub('core/ux-fixes-v1764.js'),classic=readPub('core/classic-offer-pdf-v1767.js');"); +staticTest=staticTest.replace("if(!sw.includes('v17-6-6-offer-templates')||!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.6');","if(!sw.includes('v17-6-7-classic-offer-pdf')||!sw.includes('classic-offer-pdf-v1767.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.6.7');"); +staticTest=staticTest.replace("if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260908-v17-6-6-offer-templates'))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('20260908-v17-6-7-classic-offer-pdf')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');"); +staticTest=staticTest.replace("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(!classic.includes(\"const VERSION='17.6.7'\")||!classic.includes('CLASSIC_IDS')||!classic.includes('ARCHIVE_IDS')||!classic.includes('renderPages'))fail('v17.6.7 classic PDF module missing');else ok('v17.6.7 classic PDF module present');\nif(/service_role\\s*[:=]\\s*['\"][A-Za-z0-9._-]{20,}/i.test(ops+hotfix+ux+classic)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops+hotfix+ux+classic))fail('possible secret in ops/hotfix/ux/classic module');else ok('ops/hotfix/ux/classic modules have no hard-coded secret');"); +write('tests/static-security.mjs',staticTest); + +let release=read('tests/release-check.mjs'); +release += `\ncheck(pkg.version==='17.6.7','package version is v17.6.7');\ncheck(index.includes('${RELEASE}')&&index.includes('classic-offer-pdf-v1767.js'),'index loads classic proposal PDF module');\ncheck(sw.includes('v72-${RELEASE}')&&sw.includes('classic-offer-pdf-v1767.js'),'service worker caches v17.6.7 classic PDF module');\ncheck(runtime.includes("'warm-sun'")&&runtime.includes("'bento-cards'")&&runtime.includes("'event-story'")&&runtime.includes("'personal-letter'")&&runtime.includes("'event-ticket'")&&runtime.includes("'solar-experience'"),'10 classic proposal designs are available');\ncheck(runtime.includes("'midnight-compact'")&&runtime.includes("'black-gold'")&&runtime.includes("'neon-emerald'"),'hidden archive proposal templates are restored');\ncheck(runtime.includes('SunClassicOfferPDFV1767')&&runtime.includes('renderOfferPdfPagesBase'),'classic renderer wraps the same PDF pages used by preview/download');\n`; +write('tests/release-check.mjs',release); + +let spec=read('tests/app.spec.mjs'); +spec += `\n\ntest('v17.6.7 classic client PDF renders distinctive cover and keeps preview/download page parity', async ({ page }) => {\n await page.goto('/index.html',{waitUntil:'domcontentloaded'});\n const source=fs.readFileSync(path.join(process.cwd(),'public','core','classic-offer-pdf-v1767.js'),'utf8');\n await page.addScriptTag({content:source});\n await page.waitForFunction(()=>Boolean(window.SunClassicOfferPDFV1767),null,{timeout:5000});\n const result=await page.evaluate(async()=>{\n const c=document.createElement('canvas');c.width=1600;c.height=1000;const x=c.getContext('2d');x.fillStyle='#d9a757';x.fillRect(0,0,c.width,c.height);x.fillStyle='#173a2d';x.fillRect(500,120,700,700);const photo=c.toDataURL('image/jpeg',.8);\n const snapshot={id:'1767',client:'Анна',event:'День рождения',date:'2026-09-20',time:'18:30',guests:24,offerTemplateId:'event-ticket',pricing:{base:23517,itemsTotal:23517,total:23517},items:[{id:'a',name:'Фуршетный бокс',qty:3,categoryId:0,photoData:photo,weight:'1500 г'}]};\n const fakeBase=async()=>{const base=document.createElement('canvas');base.width=1600;base.height=2262;return [base]};\n const pages=await window.SunClassicOfferPDFV1767.renderPages(snapshot,fakeBase);return {count:pages.length,id:pages[0].dataset.sunClassicTemplate,cover:pages[0].dataset.sunClassicCover,classic:window.SunClassicOfferPDFV1767.CLASSIC_IDS.length,archive:window.SunClassicOfferPDFV1767.ARCHIVE_IDS.length};\n });\n expect(result.count).toBe(2);expect(result.id).toBe('event-ticket');expect(result.cover).toBe('1');expect(result.classic).toBe(10);expect(result.archive).toBe(3);\n});\n`; +write('tests/app.spec.mjs',spec); + +console.log('Applied Caterium v17.6.7 classic proposal PDF patch');