caterium-app/ops/release_v1771.py
2026-09-09 02:34:12 +03:00

233 lines
18 KiB
Python
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.

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