From 317931ea209e618056aba9863816d69d88c5dc7c Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Wed, 9 Sep 2026 02:34:12 +0300 Subject: [PATCH] Add v17.7.1 release patch script --- ops/release_v1771.py | 232 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 ops/release_v1771.py diff --git a/ops/release_v1771.py b/ops/release_v1771.py new file mode 100644 index 0000000..98d709d --- /dev/null +++ b/ops/release_v1771.py @@ -0,0 +1,232 @@ +from pathlib import Path +import json + +ROOT = Path(__file__).resolve().parents[1] +OLD_RELEASE = '20260908-v17-7-0-architecture-foundation' +NEW_RELEASE = '20260909-v17-7-1-data-layer-adoption' +OLD_CACHE = 'v75-20260908-v17-7-0-architecture-foundation' +NEW_CACHE = 'v76-20260909-v17-7-1-data-layer-adoption' + + +def read(path): + return (ROOT / path).read_text(encoding='utf-8') + + +def write(path, text): + p = ROOT / path + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(text, encoding='utf-8') + + +def replace_once(path, old, new): + text = read(path) + if old not in text: + raise SystemExit(f'marker not found in {path}: {old[:100]}') + write(path, text.replace(old, new, 1)) + + +if json.loads(read('package.json')).get('version') == '17.7.1': + print('v17.7.1 already applied') + raise SystemExit(0) + +DATA_LAYER = r'''(()=>{ + 'use strict'; + if(window.CateriumDataV1771)return; + + const VERSION='17.7.1'; + const RELEASE='20260909-v17-7-1-data-layer-adoption'; + const listeners=new Map(); + const clone=value=>{try{return structuredClone(value)}catch(_){return JSON.parse(JSON.stringify(value))}}; + const safeJson=(raw,fallback)=>{try{const value=JSON.parse(raw);return value==null?fallback:value}catch(_){return fallback}}; + const emit=(topic,payload)=>{for(const fn of listeners.get(topic)||[]){try{fn(payload)}catch(error){console.error('[Caterium Data]',error)}}}; + const subscribe=(topic,fn)=>{if(typeof fn!=='function')return()=>{};if(!listeners.has(topic))listeners.set(topic,new Set());listeners.get(topic).add(fn);return()=>listeners.get(topic)?.delete(fn)}; + + function runtimeOrders(){try{if(typeof orders!=='undefined'&&Array.isArray(orders))return orders}catch(_){}return null} + function runtimeCatalog(){try{if(typeof boxes!=='undefined'&&Array.isArray(boxes))return boxes}catch(_){}return null} + function runtimePersist(){try{return typeof persist==='function'?persist:null}catch(_){return null}} + function cloud(){return window.SunCloudV2||null} + function session(){try{return cloud()?.getSession?.()||null}catch(_){return null}} + function workspace(){try{return cloud()?.getWorkspace?.()||null}catch(_){return null}} + function supportMode(){try{return cloud()?.getSupportMode?.()||null}catch(_){return null}} + function isSignedIn(){return Boolean(session()?.user)} + function isSupportReadOnly(){return Boolean(supportMode()||cloud()?.isSupportMode?.())} + function canWrite(permission='app.write'){ + if(isSupportReadOnly())return false; + if(!isSignedIn())return true; + try{const c=cloud();if(typeof c?.hasPermission==='function')return c.hasPermission(permission)||c.hasPermission('app.write')}catch(_){} + return true; + } + + const storage={ + read(key,fallback=null){const raw=localStorage.getItem(String(key));if(raw==null)return fallback;return safeJson(raw,fallback)}, + write(key,value,{silent=false}={}){localStorage.setItem(String(key),JSON.stringify(value));if(!silent)emit(`storage:${key}`,clone(value));return value}, + remove(key){localStorage.removeItem(String(key));emit(`storage:${key}`,null)}, + raw(key){return localStorage.getItem(String(key))} + }; + + function persistDomain(key,list,persistLocal){ + if(!persistLocal){storage.write(key,list,{silent:true});return} + const p=runtimePersist(); + if(p){try{p();return}catch(error){console.warn('[Caterium Data] persist fallback',error)}} + storage.write(key,list,{silent:true}); + } + + function getOrders(){const live=runtimeOrders();return clone(live||storage.read('sunOrders',[])||[])} + function replaceOrders(next,{persistLocal=true,reason='replace'}={}){ + const list=Array.isArray(next)?clone(next):[],live=runtimeOrders(); + if(live){live.length=0;live.push(...clone(list))} + persistDomain('sunOrders',list,persistLocal);emit('orders',{reason,orders:clone(list)});return list; + } + function getOrder(id){return getOrders().find(o=>String(o?.id)===String(id))||null} + function updateOrder(id,updater,{persistLocal=true,reason='update'}={}){ + const list=getOrders(),index=list.findIndex(o=>String(o?.id)===String(id));if(index<0)return null; + const current=clone(list[index]),next=typeof updater==='function'?updater(current):{...current,...clone(updater||{})};if(!next)return null; + list[index]=next;replaceOrders(list,{persistLocal,reason});return clone(next); + } + function applyServerOrders(changed,{reason='server'}={}){ + const patches=Array.isArray(changed)?changed:[];if(!patches.length)return {changed:0,ids:[]}; + const list=getOrders(),map=new Map(list.map((o,i)=>[String(o?.id),i])),ids=[]; + for(const patch of patches){const id=String(patch?.id??'');if(!id)continue;const index=map.get(id);if(index==null){list.push(clone(patch));map.set(id,list.length-1)}else list[index]=clone(patch);ids.push(id)} + replaceOrders(list,{persistLocal:false,reason}); + try{window.render?.();window.renderOrders?.();window.renderStats?.();window.renderClients?.()}catch(_){} + try{window.dispatchEvent(new CustomEvent('caterium:data-orders-applied',{detail:{ids,reason}}))}catch(_){} + return {changed:ids.length,ids}; + } + + function getCatalog(){const live=runtimeCatalog();return clone(live||storage.read('sunBoxes',[])||[])} + function replaceCatalog(next,{persistLocal=true,reason='replace'}={}){ + const list=Array.isArray(next)?clone(next):[],live=runtimeCatalog(); + if(live){live.length=0;live.push(...clone(list))} + persistDomain('sunBoxes',list,persistLocal);emit('catalog',{reason,items:clone(list)});return list; + } + function getCatalogItem(id){return getCatalog().find(x=>String(x?.id)===String(id))||null} + function upsertCatalogItem(item,{persistLocal=true,reason='upsert'}={}){ + if(!item?.id)return null;const list=getCatalog(),index=list.findIndex(x=>String(x?.id)===String(item.id)),next=clone(item); + if(index<0)list.push(next);else list[index]=next;replaceCatalog(list,{persistLocal,reason});return clone(next); + } + function removeCatalogItem(id,{persistLocal=true,reason='remove'}={}){ + const list=getCatalog(),next=list.filter(x=>String(x?.id)!==String(id));if(next.length===list.length)return false; + replaceCatalog(next,{persistLocal,reason});return true; + } + + const ordersRepo={list:getOrders,get:getOrder,replace:replaceOrders,update:updateOrder,applyServerChanges:applyServerOrders}; + const catalogRepo={list:getCatalog,get:getCatalogItem,replace:replaceCatalog,upsert:upsertCatalogItem,remove:removeCatalogItem}; + const diagnostics={ + snapshot(){return {version:VERSION,workspaceId:workspace()?.id||null,signedIn:isSignedIn(),supportReadOnly:isSupportReadOnly(),orderCount:getOrders().length,catalogCount:getCatalog().length,listenerTopics:listeners.size}}, + measure(name,fn){const started=performance.now();return Promise.resolve().then(fn).finally(()=>emit('metric',{name,durationMs:Math.round((performance.now()-started)*10)/10,at:new Date().toISOString()}))} + }; + + const api={VERSION,RELEASE,storage,orders:ordersRepo,catalog:catalogRepo,subscribe,emit,cloud,session,workspace,isSignedIn,isSupportReadOnly,canWrite,diagnostics}; + window.CateriumDataV1771=api; + window.CateriumDataV1770=api; + window.CateriumData=api; + try{window.dispatchEvent(new CustomEvent('caterium:data-ready',{detail:{version:VERSION,phase:2}}))}catch(_){} +})(); +''' +write('public/core/data-layer-v1771.js', DATA_LAYER) + +legacy = read('public/legacy/bootstrap.js') +old = "function persist(){localStorage.sunBoxes=JSON.stringify(boxes);localStorage.sunOrders=JSON.stringify(orders)}function show(id,b)" +new = "function persist(){localStorage.sunBoxes=JSON.stringify(boxes);localStorage.sunOrders=JSON.stringify(orders)}function dataLayer(){return window.CateriumDataV1771||window.CateriumData||window.CateriumDataV1770||null}function persistOrders(reason){let d=dataLayer();if(d?.orders?.replace){d.orders.replace(orders,{persistLocal:true,reason:reason||'legacy.orders'});return}persist()}function persistCatalog(reason){let d=dataLayer();if(d?.catalog?.replace){d.catalog.replace(boxes,{persistLocal:true,reason:reason||'legacy.catalog'});return}persist()}function show(id,b)" +if old not in legacy: + raise SystemExit('legacy persist marker missing') +legacy = legacy.replace(old, new, 1) +for a, b in [ + ("persist();deleteOrderBtn.style.display='inline-block';renderOrders();updateOrderSummary();alert('Заказ сохранён.');", "persistOrders('order.save');deleteOrderBtn.style.display='inline-block';renderOrders();updateOrderSummary();alert('Заказ сохранён.');"), + ("function status(id,s){orders.find(x=>x.id==id).status=s;persist()}", "function status(id,s){orders.find(x=>x.id==id).status=s;persistOrders('order.status')}"), + ("orders=orders.filter(x=>x.id!=id);persist();renderOrders();", "orders=orders.filter(x=>x.id!=id);persistOrders('order.delete');renderOrders();"), + ("boxes[boxes.findIndex(x=>x.id==edited.id)]=edited;persist();closeModal('editor');", "boxes[boxes.findIndex(x=>x.id==edited.id)]=edited;persistCatalog('catalog.save');closeModal('editor');"), + ("boxes=boxes.filter(x=>x.id!=edited.id);persist();closeModal('editor');", "boxes=boxes.filter(x=>x.id!=edited.id);persistCatalog('catalog.delete');closeModal('editor');") +]: + if a not in legacy: + raise SystemExit('legacy CRUD marker missing: ' + a[:80]) + legacy = legacy.replace(a, b, 1) +write('public/legacy/bootstrap.js', legacy) + +perf = read('public/core/performance.js') +perf = perf.replace("const VERSION='17.7.0';\n const RELEASE='20260908-v17-7-0-architecture-foundation';", "const VERSION='17.7.1';\n const RELEASE='20260909-v17-7-1-data-layer-adoption';", 1) +for a, b in [ + ("window.CateriumDataV1770||document.getElementById('cateriumDataV1770Script')", "window.CateriumDataV1771||document.getElementById('cateriumDataV1771Script')"), + ("script.id='cateriumDataV1770Script'", "script.id='cateriumDataV1771Script'"), + ("core/data-layer-v1770.js?v=${RELEASE}", "core/data-layer-v1771.js?v=${RELEASE}"), + ("Не загрузился data-layer-v1770.js", "Не загрузился data-layer-v1771.js") +]: + if a not in perf: + raise SystemExit('performance marker missing: ' + a) + perf = perf.replace(a, b, 1) +write('public/core/performance.js', perf) + +replace_once('public/app-runtime.js', "const VERSION = '17.7.0';\n const RELEASE = 'v17.7.0 Architecture Foundation';", "const VERSION = '17.7.1';\n const RELEASE = 'v17.7.1 Data Layer Adoption';") + +sw = read('public/service-worker.js').replace(OLD_CACHE, NEW_CACHE).replace(OLD_RELEASE, NEW_RELEASE).replace('data-layer-v1770.js', 'data-layer-v1771.js') +write('public/service-worker.js', sw) +write('public/index.html', read('public/index.html').replace(OLD_RELEASE, NEW_RELEASE)) + +pkg = json.loads(read('package.json')) +pkg['version'] = '17.7.1' +pkg['scripts']['check:syntax'] = pkg['scripts']['check:syntax'].replace('core/data-layer-v1770.js', 'core/data-layer-v1771.js') +write('package.json', json.dumps(pkg, ensure_ascii=False, indent=2) + '\n') +lock = json.loads(read('package-lock.json')) +lock['version'] = '17.7.1' +lock['packages']['']['version'] = '17.7.1' +write('package-lock.json', json.dumps(lock, ensure_ascii=False, indent=2) + '\n') + +manifest = json.loads(read('docs/release-manifest.json')) +manifest.update({ + 'version': 'v17.7.1', 'pwaCache': NEW_CACHE, 'release': NEW_RELEASE, + 'notes': 'Data layer adoption: base order save/status/delete and catalog save/delete paths now write through Caterium Data Layer with legacy local persistence retained as a fallback.', + 'stabilityLoggerVersion': '17.7.1', 'dataLayer': 'core/data-layer-v1771.js', 'dataLayerPhase': 2, + 'dataLayerLegacyOrderWrites': True, 'dataLayerLegacyCatalogWrites': True, 'legacyStateKept': True +}) +write('docs/release-manifest.json', json.dumps(manifest, ensure_ascii=False, indent=2) + '\n') +write('docs/releases/V17.7.1-CHANGES.txt', '''Caterium v17.7.1 — Data Layer Adoption\nDate: 2026-09-09\n\n1. Base order create/update, status and delete writes now go through Caterium Data Layer.\n2. Base catalog save/delete writes now go through the same data layer.\n3. Added catalog repository list/get/replace/upsert/remove operations.\n4. Legacy persist() remains as a compatibility fallback.\n5. CateriumDataV1770 remains as a compatibility alias; CateriumDataV1771 is current.\n6. Server order automation remains compatible through CateriumData.\n7. PWA cache/versioning and stability telemetry advance to v17.7.1.\n''') + +static = read('tests/static-security.mjs') +static = static.replace("dataLayer=readPub('core/data-layer-v1770.js')", "dataLayer=readPub('core/data-layer-v1771.js')") +static = static.replace("v17-7-0-architecture-foundation')||!sw.includes('data-layer-v1770.js')", "v17-7-1-data-layer-adoption')||!sw.includes('data-layer-v1771.js')") +static = static.replace("20260908-v17-7-0-architecture-foundation')", "20260909-v17-7-1-data-layer-adoption')", 1) +static = static.replace("if(!performance.includes('loadDataLayer')||!performance.includes('loadServerAutomation'))fail('v17.7.0 foundation loaders missing');else ok('v17.7.0 foundation loaders present');", "if(!performance.includes('loadDataLayer')||!performance.includes('data-layer-v1771.js')||!performance.includes('loadServerAutomation'))fail('v17.7.1 data layer loaders missing');else ok('v17.7.1 data layer loaders present');") +static = static.replace("if(!dataLayer.includes(\"const VERSION='17.7.0'\")||!dataLayer.includes('ordersRepo')||!dataLayer.includes('applyServerOrders'))fail('v17.7.0 data layer missing');else ok('v17.7.0 data layer present');", "if(!dataLayer.includes(\"const VERSION='17.7.1'\")||!dataLayer.includes('ordersRepo')||!dataLayer.includes('catalogRepo')||!dataLayer.includes('applyServerOrders')||!dataLayer.includes('CateriumDataV1770=api'))fail('v17.7.1 data layer missing');else ok('v17.7.1 data layer present');") +static = static.replace("\"const VERSION = '17.7.0'\"", "\"const VERSION = '17.7.1'\"", 1) +anchor = "if(!legacy.includes('sunEsc(')||!legacy.includes('sunAttr('))fail('legacy bootstrap is not using shared escaping');else ok('legacy bootstrap escapes text and attributes');" +static = static.replace(anchor, anchor + "\nif(!legacy.includes(\"persistOrders('order.save')\")||!legacy.includes(\"persistOrders('order.status')\")||!legacy.includes(\"persistOrders('order.delete')\")||!legacy.includes(\"persistCatalog('catalog.save')\")||!legacy.includes(\"persistCatalog('catalog.delete')\"))fail('legacy CRUD is not routed through data layer');else ok('legacy CRUD routes through data layer');") +write('tests/static-security.mjs', static) + +release = read('tests/release-check.mjs') +release = release.replace("const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),ops=read('core/ops-ux-v1762.js'),hotfix=read('core/hotfix-v1763.js'),ux=read('core/ux-fixes-v1764.js'),classic=read('core/classic-offer-pdf-v1767.js'),developerUX=read('core/developer-console-v1768.js'),offerWorkspace=read('core/offer-workspace-v1769.js');", "const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),dataLayer=read('core/data-layer-v1771.js'),legacy=read('legacy/bootstrap.js'),ops=read('core/ops-ux-v1762.js'),hotfix=read('core/hotfix-v1763.js'),ux=read('core/ux-fixes-v1764.js'),classic=read('core/classic-offer-pdf-v1767.js'),developerUX=read('core/developer-console-v1768.js'),offerWorkspace=read('core/offer-workspace-v1769.js');") +release = release.replace("v75-20260908-v17-7-0-architecture-foundation", "v76-20260909-v17-7-1-data-layer-adoption") +release = release.replace("data-layer-v1770.js", "data-layer-v1771.js") +release = release.replace("20260908-v17-7-0-architecture-foundation", "20260909-v17-7-1-data-layer-adoption") +release = release.replace("['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.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']") +release = release.replace("release notes exist through v17.7.0", "release notes exist through v17.7.1") +release = release.replace("const VERSION = '17.7.0'", "const VERSION = '17.7.1'") +release = release.replace("pkg.version==='17.7.0'", "pkg.version==='17.7.1'") +release = release.replace("package version is v17.7.0", "package version is v17.7.1") +release = release.replace("v17.7.0 Architecture Foundation", "v17.7.1 Data Layer Adoption") +release = release.replace("stability logger reports v17.7.0", "stability logger reports v17.7.1") +extra = "\ncheck(dataLayer.includes(\"const VERSION='17.7.1'\")&&dataLayer.includes('catalogRepo')&&dataLayer.includes('CateriumDataV1770=api'),'v17.7.1 data layer exposes order/catalog repositories with compatibility alias');\ncheck(legacy.includes(\"persistOrders('order.save')\")&&legacy.includes(\"persistOrders('order.status')\")&&legacy.includes(\"persistOrders('order.delete')\")&&legacy.includes(\"persistCatalog('catalog.save')\")&&legacy.includes(\"persistCatalog('catalog.delete')\"),'base legacy CRUD routes through data layer');\ncheck(releaseManifest.dataLayerPhase===2&&releaseManifest.dataLayerLegacyOrderWrites===true&&releaseManifest.dataLayerLegacyCatalogWrites===true,'release manifest records data layer phase 2');\n" +release = release.replace("if(bad)process.exit(1);", extra + "if(bad)process.exit(1);") +write('tests/release-check.mjs', release) + +app = read('tests/app.spec.mjs').replace("expect(result.version).toBe('17.7.0');", "expect(result.version).toBe('17.7.1');", 1).replace("const VERSION = '17.7.0'", "const VERSION = '17.7.1'", 1) +smoke = r''' + +test('v17.7.1 data layer serves orders and catalog with v17.7.0 compatibility alias', async ({ page }) => { + await page.goto('/index.html',{waitUntil:'domcontentloaded'}); + await page.waitForFunction(()=>Boolean(window.CateriumDataV1771),null,{timeout:5000}); + const result=await page.evaluate(()=>{ + const d=window.CateriumDataV1771,beforeOrders=d.orders.list(),beforeCatalog=d.catalog.list(); + d.orders.replace([...beforeOrders,{id:'qa-v1771',event:'QA',lines:[]}],{persistLocal:false,reason:'qa-order'}); + d.catalog.replace([...beforeCatalog,{id:'qa-v1771',name:'QA item',price:0,ingredients:[]}],{persistLocal:false,reason:'qa-catalog'}); + const hasOrder=Boolean(d.orders.get('qa-v1771')),hasCatalog=Boolean(d.catalog.get('qa-v1771')); + d.orders.replace(beforeOrders,{persistLocal:false,reason:'qa-restore'});d.catalog.replace(beforeCatalog,{persistLocal:false,reason:'qa-restore'}); + return {version:d.VERSION,alias:window.CateriumDataV1770===d,hasOrder,hasCatalog}; + }); + expect(result).toEqual({version:'17.7.1',alias:true,hasOrder:true,hasCatalog:true}); +}); +''' +if "v17.7.1 data layer serves orders and catalog" not in app: + app += smoke +write('tests/app.spec.mjs', app) + +print('Applied v17.7.1 data layer adoption patch')