caterium-app/.github/scripts/patch-v1767-classic-offer.mjs
2026-09-08 07:21:43 +03:00

98 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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=`<script src="core/pdf-engine.js?v=${RELEASE}"></script>`;
if(!index.includes(indexNeedle))throw new Error('pdf-engine index script not found');
index=index.replace(indexNeedle,indexNeedle+`<script src="core/classic-offer-pdf-v1767.js?v=${RELEASE}"></script>`);
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');