94 lines
26 KiB
JavaScript
94 lines
26 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const root=process.cwd();
|
|
const read=p=>fs.readFileSync(path.join(root,p),'utf8');
|
|
const write=(p,v)=>{const full=path.join(root,p);fs.mkdirSync(path.dirname(full),{recursive:true});fs.writeFileSync(full,v)};
|
|
function replaceOne(text,from,to,label){if(!text.includes(from))throw new Error(`Missing ${label||from.slice(0,80)}`);return text.replace(from,to)}
|
|
function replaceRegex(text,re,to,label){if(!re.test(text))throw new Error(`Missing regex ${label||re}`);return text.replace(re,to)}
|
|
|
|
const VERSION='17.7.3';
|
|
const RELEASE='20260909-v17-7-3-clients-server-read';
|
|
const CACHE=`v78-${RELEASE}`;
|
|
|
|
// Data layer: derive v17.7.3 from the verified v17.7.2 implementation.
|
|
let dl=read('public/core/data-layer-v1772.js');
|
|
dl=replaceOne(dl,"if(window.CateriumDataV1772)return;","if(window.CateriumDataV1773)return;",'data layer guard');
|
|
dl=replaceOne(dl,"const VERSION='17.7.2';","const VERSION='17.7.3';",'data layer version');
|
|
dl=replaceOne(dl,"const RELEASE='20260909-v17-7-2-clients-foundation';",`const RELEASE='${RELEASE}';`,'data layer release');
|
|
dl=replaceOne(dl,"const CLIENT_CACHE_KEY='cateriumClientsV1772';","const CLIENT_CACHE_KEY='cateriumClientsV1772';\n const SERVER_CLIENT_CACHE_KEY='cateriumClientsServerV1773';",'server client cache constant');
|
|
dl=dl.replaceAll('window.CateriumDataV1772','window.CateriumDataV1773');
|
|
|
|
const listMarker=" function listClients(){return clone(buildClients())}\n";
|
|
const listReplacement=` function readServerClientCache(){const value=storage.read(SERVER_CLIENT_CACHE_KEY,null);return value&&typeof value==='object'&&!Array.isArray(value)?value:null}\n function currentServerRows(){const ws=String(workspace()?.id||''),cache=readServerClientCache();if(!ws||!cache||String(cache.workspaceId||'')!==ws)return [];return Array.isArray(cache.rows)?cache.rows:[]}\n function serverProfile(row){\n const data=row?.data&&typeof row.data==='object'&&!Array.isArray(row.data)?row.data:{},identity=data.identity&&typeof data.identity==='object'?data.identity:data;\n const key=String(row?.client_key||row?.canonical_key||'');if(!key)return null;\n const latestAddress=String(row?.latest_address||identity?.latestAddress||data?.latestAddress||'');\n return {key,name:String(row?.name||identity?.name||data?.name||''),phone:String(row?.phone||identity?.phone||data?.phone||''),latestAddress,addresses:latestAddress?[latestAddress]:[],orderIds:[],orderCount:0,totalSpent:0,loyalty:Object.prototype.hasOwnProperty.call(data,'loyalty')?clone(data.loyalty):null,communication:Object.prototype.hasOwnProperty.call(data,'communication')?clone(data.communication):null,serverVersion:Number(row?.version||0)||null,serverUpdatedAt:String(row?.updated_at||''),serverSourceKeys:Array.isArray(row?.source_keys)?row.source_keys.map(String):[],serverSourceCount:Number(row?.source_count||0)||1,canonicalRowPresent:Boolean(row?.canonical_row_present)};\n }\n function serverListClients(){return currentServerRows().map(serverProfile).filter(Boolean)}\n function mergeClientSources(){\n const local=buildClients(),byKey=new Map(local.map(item=>[item.key,{...clone(item),dataSource:'local'}]));\n for(const server of serverListClients()){\n if(server.key.startsWith('o:')&&!byKey.has(server.key))continue;\n const current=byKey.get(server.key)||{key:server.key,name:'',phone:'',latestAddress:'',addresses:[],orderIds:[],orderCount:0,totalSpent:0,loyalty:null,communication:null};\n const addresses=[...new Set([...(current.addresses||[]),...(server.addresses||[]),server.latestAddress].map(v=>String(v||'').trim()).filter(Boolean))];\n byKey.set(server.key,{...current,name:server.name||current.name,phone:server.phone||current.phone,latestAddress:server.latestAddress||current.latestAddress,addresses,loyalty:server.loyalty??current.loyalty,communication:server.communication??current.communication,serverVersion:server.serverVersion,serverUpdatedAt:server.serverUpdatedAt,serverSourceKeys:server.serverSourceKeys,serverSourceCount:server.serverSourceCount,canonicalRowPresent:server.canonicalRowPresent,dataSource:'server+local'});\n }\n return [...byKey.values()].sort((a,b)=>(b.orderCount-a.orderCount)||String(a.name||a.key).localeCompare(String(b.name||b.key),'ru'));\n }\n function listClients(options={}){const source=typeof options==='string'?options:String(options?.source||'auto');if(source==='local'||source==='legacy')return clone(buildClients());if(source==='server')return clone(serverListClients());return clone(mergeClientSources())}\n function compareClientSources(){\n const local=buildClients(),server=serverListClients(),merged=mergeClientSources(),serverKeys=new Set(server.map(x=>x.key)),localKeys=new Set(local.map(x=>x.key));\n const metricMismatches=local.map(item=>{const m=merged.find(x=>x.key===item.key);return !m||m.orderCount!==item.orderCount||Number(m.totalSpent)!==Number(item.totalSpent)?item.key:null}).filter(Boolean);\n return {localCount:local.length,serverCount:server.length,mergedCount:merged.length,serverPreferred:server.length>0,missingOnServer:[...localKeys].filter(k=>!serverKeys.has(k)),serverOnly:[...serverKeys].filter(k=>!localKeys.has(k)),legacyServerFallbackRows:server.filter(x=>x.key.startsWith('o:')).length,orderMetricsEqual:metricMismatches.length===0,metricMismatches};\n }\n let serverRefreshPromise=null;\n async function refreshServerClients({force=false,reason='manual'}={}){\n if(serverRefreshPromise)return serverRefreshPromise;if(!isSignedIn())return {status:'local',diagnostics:compareClientSources()};\n const c=cloud()?.getClient?.(),ws=workspace();if(!c?.rpc||!ws?.id)return {status:'offline',diagnostics:compareClientSources()};\n const cache=readServerClientCache(),age=cache?.fetchedAt?Date.now()-Date.parse(cache.fetchedAt):Infinity;if(!force&&String(cache?.workspaceId||'')===String(ws.id)&&age>=0&&age<15000)return {status:'cached',count:currentServerRows().length,diagnostics:compareClientSources()};\n serverRefreshPromise=(async()=>{\n try{\n const {data,error}=await c.rpc('sun_v17_clients_snapshot_v1773',{p_workspace:ws.id});if(error)throw error;const rows=Array.isArray(data)?data:[];\n storage.write(SERVER_CLIENT_CACHE_KEY,{workspaceId:String(ws.id),fetchedAt:new Date().toISOString(),rows:clone(rows)},{silent:true});\n const diagnostics=compareClientSources();emit('clients',{reason:\`server:\${reason}\`,changedKeys:rows.map(r=>String(r?.client_key||'')).filter(Boolean),clients:listClients(),diagnostics});\n try{window.dispatchEvent(new CustomEvent('caterium:clients-server-refresh',{detail:{reason,count:rows.length,diagnostics}}))}catch(_){}\n return {status:'loaded',count:rows.length,diagnostics};\n }finally{serverRefreshPromise=null}\n })();\n return serverRefreshPromise;\n }\n`;
|
|
dl=replaceOne(dl,listMarker,listReplacement,'client list server merge');
|
|
|
|
dl=replaceOne(dl,"const clientsRepo={list:listClients,get:getClient,upsert:upsertClient,setLoyalty:setClientLoyalty,setCommunication:setClientCommunication,adoptLegacy,push:pushClient,pushAll:pushAllClients,keyFromOrder:clientKeyFromOrder,keyFromValues:clientKeyFromValues,normalizePhone,normalizeName};","const clientsRepo={list:listClients,get:getClient,upsert:upsertClient,setLoyalty:setClientLoyalty,setCommunication:setClientCommunication,adoptLegacy,push:pushClient,pushAll:pushAllClients,refreshServer:refreshServerClients,serverList:serverListClients,compare:compareClientSources,keyFromOrder:clientKeyFromOrder,keyFromValues:clientKeyFromValues,normalizePhone,normalizeName};",'clients repo server methods');
|
|
dl=replaceOne(dl,"snapshot(){return {version:VERSION,workspaceId:workspace()?.id||null,signedIn:isSignedIn(),supportReadOnly:isSupportReadOnly(),orderCount:getOrders().length,catalogCount:getCatalog().length,clientCount:listClients().length,listenerTopics:listeners.size,pendingClientPushes:pendingServerKeys.size}},","snapshot(){const clientCompare=compareClientSources();return {version:VERSION,workspaceId:workspace()?.id||null,signedIn:isSignedIn(),supportReadOnly:isSupportReadOnly(),orderCount:getOrders().length,catalogCount:getCatalog().length,clientCount:listClients().length,clientServerCount:clientCompare.serverCount,clientServerPreferred:clientCompare.serverPreferred,clientOrderMetricsEqual:clientCompare.orderMetricsEqual,listenerTopics:listeners.size,pendingClientPushes:pendingServerKeys.size}},",'diagnostics snapshot');
|
|
dl=replaceOne(dl,"window.CateriumDataV1773=api;\n window.CateriumDataV1771=api;","window.CateriumDataV1773=api;\n window.CateriumDataV1772=api;\n window.CateriumDataV1771=api;",'compatibility aliases');
|
|
dl=replaceOne(dl,"window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>clientsRepo.pushAll().catch(()=>{}),800));\n window.addEventListener('sun:cloud-sync-complete',()=>setTimeout(()=>clientsRepo.adoptLegacy({pushServer:true,reason:'cloud-sync'}),300));\n setTimeout(()=>clientsRepo.pushAll().catch(()=>{}),3500);\n try{window.dispatchEvent(new CustomEvent('caterium:data-ready',{detail:{version:VERSION,phase:3,clients:true}}))}catch(_){}","window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>{clientsRepo.refreshServer({force:true,reason:'permissions'}).catch(()=>{});clientsRepo.pushAll().catch(()=>{})},800));\n window.addEventListener('sun:cloud-sync-complete',()=>setTimeout(()=>{clientsRepo.adoptLegacy({pushServer:true,reason:'cloud-sync'});clientsRepo.refreshServer({force:true,reason:'cloud-sync'}).catch(()=>{})},500));\n setTimeout(()=>clientsRepo.refreshServer({reason:'boot'}).catch(()=>{}),1400);\n setTimeout(()=>clientsRepo.pushAll().catch(()=>{}),3500);\n try{window.dispatchEvent(new CustomEvent('caterium:data-ready',{detail:{version:VERSION,phase:4,clients:true,clientServerRead:true}}))}catch(_){}",'client boot refresh');
|
|
write('public/core/data-layer-v1773.js',dl);
|
|
|
|
// Core loaders and compatibility routing.
|
|
let perf=read('public/core/performance.js');
|
|
perf=perf.replaceAll("const VERSION='17.7.2';","const VERSION='17.7.3';").replaceAll("const RELEASE='20260909-v17-7-2-clients-foundation';",`const RELEASE='${RELEASE}';`).replaceAll('CateriumDataV1772','CateriumDataV1773').replaceAll('cateriumDataV1772Script','cateriumDataV1773Script').replaceAll('data-layer-v1772.js','data-layer-v1773.js').replaceAll('v1772.js','v1773.js');
|
|
write('public/core/performance.js',perf);
|
|
|
|
let legacy=read('public/legacy/bootstrap.js');
|
|
legacy=replaceOne(legacy,'function dataLayer(){return window.CateriumDataV1772||window.CateriumDataV1771||window.CateriumData||window.CateriumDataV1770||null}','function dataLayer(){return window.CateriumDataV1773||window.CateriumDataV1772||window.CateriumDataV1771||window.CateriumData||window.CateriumDataV1770||null}','legacy dataLayer helper');
|
|
write('public/legacy/bootstrap.js',legacy);
|
|
|
|
let sw=read('public/service-worker.js');
|
|
sw=sw.replaceAll('sun-catering-pwa-v77-20260909-v17-7-2-clients-foundation',`sun-catering-pwa-${CACHE}`).replaceAll('20260909-v17-7-2-clients-foundation',RELEASE).replaceAll('data-layer-v1772.js','data-layer-v1773.js');
|
|
write('public/service-worker.js',sw);
|
|
|
|
let runtime=read('public/app-runtime.js');
|
|
runtime=runtime.replaceAll("const VERSION = '17.7.2'","const VERSION = '17.7.3'").replaceAll('v17.7.2 Clients Foundation','v17.7.3 Clients Server Read');
|
|
write('public/app-runtime.js',runtime);
|
|
|
|
// Existing client UI now consumes Data Layer profiles while preserving legacy calculations from orders.
|
|
let index=read('public/index.html');
|
|
index=index.replaceAll('20260909-v17-7-2-clients-foundation',RELEASE);
|
|
index=replaceOne(index,'const nameKey = String(order.contact || "").trim().toLowerCase();','const nameKey = String(order.contact || "").trim().replace(/\\s+/g, " ").toLowerCase();','UI canonical name key');
|
|
const collectRe=/ function collectClients\(\) \{[\s\S]*?\n \}\n\n function renderClientSummary/;
|
|
const collectReplacement=` function collectClients() {\n const repo=window.CateriumDataV1773?.clients||window.CateriumDataV1772?.clients||window.CateriumData?.clients;\n const profiles=repo?.list?.({source:'auto'});\n if(Array.isArray(profiles)&&profiles.length){\n const result=[];\n profiles.forEach(profile=>{\n const key=String(profile?.key||'');if(!key)return;\n const linked=orders.filter(order=>clientKey(order)===key);\n if(!linked.length&&key.startsWith('o:'))return;\n const valid=linked.filter(order=>order.status!=='Отменён');\n const addresses=new Set([...(Array.isArray(profile.addresses)?profile.addresses:[]),profile.latestAddress,...linked.map(order=>order.address)].map(v=>String(v||'').trim()).filter(Boolean));\n const lastDate=linked.reduce((max,order)=>{const stamp=\`${'${'}order.date||''}T${'${'}order.time||'00:00'}\`;return stamp>max?stamp:max},'');\n const latest=[...linked].sort((a,b)=>\`${'${'}b.date||''}T${'${'}b.time||''}\`.localeCompare(\`${'${'}a.date||''}T${'${'}a.time||''}\`))[0]||{};\n result.push({key,name:String(profile.name||latest.contact||'Без имени').trim()||'Без имени',phone:String(profile.phone||latest.phone||'').trim()||'—',addresses:[...addresses],orders:linked,total:valid.reduce((sum,order)=>sum+orderValue(order),0),paid:valid.reduce((sum,order)=>sum+Math.max(0,Number(order.prepayment||0)),0),activeOrders:valid.length,lastDate});\n });\n return result.sort((a,b)=>(b.lastDate||'').localeCompare(a.lastDate||''));\n }\n const people = {};\n orders.forEach(order => {\n const key = clientKey(order);\n if (!key) return;\n if (!people[key]) people[key] = {key,name:String(order.contact || "Без имени").trim() || "Без имени",phone:String(order.phone || "").trim() || "—",addresses:new Set(),orders:[],total:0,paid:0,activeOrders:0,lastDate:""};\n const person = people[key];\n if (order.contact && String(order.contact).trim()) person.name = String(order.contact).trim();\n if (order.phone && String(order.phone).trim()) person.phone = String(order.phone).trim();\n if (order.address && String(order.address).trim()) person.addresses.add(String(order.address).trim());\n person.orders.push(order);\n if (order.status !== "Отменён") { person.total += orderValue(order); person.paid += Math.max(0, Number(order.prepayment || 0)); person.activeOrders++; }\n const dateKey = \`${'${'}order.date || ""}T${'${'}order.time || "00:00"}\`; if (dateKey > person.lastDate) person.lastDate = dateKey;\n });\n return Object.values(people).map(person => ({ ...person, addresses: [...person.addresses] })).sort((a, b) => (b.lastDate || "").localeCompare(a.lastDate || ""));\n }\n\n function renderClientSummary`;
|
|
index=replaceRegex(index,collectRe,collectReplacement,'collectClients');
|
|
index=replaceOne(index,' window.openClientCard = encodedKey => {',` window.addEventListener('caterium:clients-server-refresh',()=>{if(clientView.classList.contains('on'))renderClients(document.getElementById("client-search")?.value||'')});\n\n window.openClientCard = encodedKey => {`,'server client refresh UI event');
|
|
index=replaceOne(index,' document.getElementById("client-search").value = "";\n renderClients();',' document.getElementById("client-search").value = "";\n renderClients();\n window.CateriumDataV1773?.clients?.refreshServer?.({reason:\'clients-open\'}).catch(()=>{});','clients open refresh');
|
|
write('public/index.html',index);
|
|
|
|
// Package version and syntax target.
|
|
const pkg=JSON.parse(read('package.json'));pkg.version=VERSION;pkg.scripts['check:syntax']=pkg.scripts['check:syntax'].replaceAll('data-layer-v1772.js','data-layer-v1773.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');
|
|
|
|
// Release manifest.
|
|
const manifest=JSON.parse(read('docs/release-manifest.json'));
|
|
Object.assign(manifest,{version:`v${VERSION}`,pwaCache:CACHE,release:RELEASE,stabilityLoggerVersion:VERSION,dataLayerPhase:4,clientServerRead:true,clientServerPreferred:true,clientServerSnapshotRpc:'sun_v17_clients_snapshot_v1773',clientLegacyFallback:true,clientLegacyRowsPreserved:true,clientOrderMetricsSource:'legacy orders verified locally',clientServerCache:'cateriumClientsServerV1773',notes:'Client server-read transition: canonical Supabase client snapshot is preferred for profile data while order counts and turnover remain verified from legacy orders; stale o: rows are preserved and deduplicated without destructive cleanup.'});
|
|
write('docs/release-manifest.json',JSON.stringify(manifest,null,2)+'\n');
|
|
write('docs/releases/V17.7.3-CHANGES.txt',`Caterium v17.7.3 — Clients Server Read\n\n- Client list and cards now consume Caterium Data Layer profiles instead of rebuilding identity only inside the UI.\n- Added canonical server snapshot RPC sun_v17_clients_snapshot_v1773.\n- Server profile data is preferred when available; local/legacy derivation remains the fallback.\n- Order count, turnover, paid amount and order history are still calculated from the current order set and compared in Data Layer diagnostics.\n- Legacy o: client rows are preserved; the snapshot canonicalizes them to p:/n: identities when possible and avoids duplicate display.\n- Added workspace-scoped server client cache and refresh events.\n- Existing v17.7.2 client write RPC and legacy loyalty/communication double-write remain compatible.\n`);
|
|
|
|
// Reproducible Supabase migration: read-only canonical snapshot, no destructive rekey/delete.
|
|
write('ops/sql/SUPABASE-V17.7.3-CLIENTS-SERVER-READ.sql',`-- Caterium v17.7.3 — canonical client server-read snapshot\n-- Read-only/additive migration. Existing p:/n:/o: rows are preserved.\n\ncreate or replace function public.sun_v17_clients_snapshot_v1773(p_workspace uuid)\nreturns table(\n client_key text,\n name text,\n phone text,\n latest_address text,\n data jsonb,\n version bigint,\n updated_at timestamptz,\n source_keys text[],\n source_count bigint,\n canonical_row_present boolean\n)\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $function$\nbegin\n if public.sun_member_role(p_workspace) is null then\n raise exception 'Access denied';\n end if;\n if not public.sun_workspace_has_feature(p_workspace,'clients') then\n raise exception 'Клиенты недоступны';\n end if;\n\n return query\n with prepared as (\n select c.*,\n regexp_replace(coalesce(nullif(trim(c.phone),''),nullif(trim(c.data#>>'{identity,phone}'),''),nullif(trim(c.data->>'phone'),''),''),'[^0-9]','','g') as phone_digits,\n lower(regexp_replace(coalesce(nullif(trim(c.name),''),nullif(trim(c.data#>>'{identity,name}'),''),nullif(trim(c.data->>'name'),''),''),'[[:space:]]+',' ','g')) as name_norm\n from public.sun_v17_clients c\n where c.workspace_id=p_workspace\n ), canonical as (\n select p.*,case when p.phone_digits<>'' then 'p:'||p.phone_digits when p.name_norm<>'' then 'n:'||p.name_norm else p.client_key end as canonical_key\n from prepared p\n ), ranked as (\n select c.*,row_number() over(partition by c.canonical_key order by (c.client_key=c.canonical_key) desc,c.updated_at desc,c.version desc,c.client_key) as rn\n from canonical c\n ), grouped as (\n select c.canonical_key,array_agg(c.client_key order by (c.client_key=c.canonical_key) desc,c.updated_at desc,c.client_key) as source_keys,count(*)::bigint as source_count,bool_or(c.client_key=c.canonical_key) as canonical_row_present\n from canonical c group by c.canonical_key\n )\n select r.canonical_key,\n coalesce(nullif(trim(r.name),''),nullif(trim(r.data#>>'{identity,name}'),''),nullif(trim(r.data->>'name'),'')),\n coalesce(nullif(trim(r.phone),''),nullif(trim(r.data#>>'{identity,phone}'),''),nullif(trim(r.data->>'phone'),'')),\n coalesce(nullif(trim(r.latest_address),''),nullif(trim(r.data#>>'{identity,latestAddress}'),''),nullif(trim(r.data->>'latestAddress'),'')),\n coalesce(r.data,'{}'::jsonb),r.version,r.updated_at,g.source_keys,g.source_count,g.canonical_row_present\n from ranked r join grouped g on g.canonical_key=r.canonical_key\n where r.rn=1\n order by r.updated_at desc,r.canonical_key;\nend;\n$function$;\n\nrevoke execute on function public.sun_v17_clients_snapshot_v1773(uuid) from public, anon;\ngrant execute on function public.sun_v17_clients_snapshot_v1773(uuid) to authenticated, service_role;\n`);
|
|
|
|
// Static/release checks follow the new current version and add server-read assertions.
|
|
let staticTest=read('tests/static-security.mjs');
|
|
staticTest=staticTest.replaceAll('data-layer-v1772.js','data-layer-v1773.js').replaceAll('dataLayer=readPub(\'core/data-layer-v1772.js\')','dataLayer=readPub(\'core/data-layer-v1773.js\')').replaceAll("const VERSION = '17.7.2'","const VERSION = '17.7.3'").replaceAll("const VERSION='17.7.2'","const VERSION='17.7.3'").replaceAll('20260909-v17-7-2-clients-foundation',RELEASE).replaceAll('v17-7-2-clients-foundation','v17-7-3-clients-server-read').replaceAll('CateriumDataV1771=api','CateriumDataV1772=api');
|
|
staticTest=replaceOne(staticTest,"if(!dataLayer.includes(\"const VERSION='17.7.3'\")||!dataLayer.includes('ordersRepo')||!dataLayer.includes('catalogRepo')||!dataLayer.includes('clientsRepo')||!dataLayer.includes('sun_v17_save_client_v1772')||!dataLayer.includes('CateriumDataV1772=api'))fail('v17.7.2 data layer missing');else ok('v17.7.2 data layer present');","if(!dataLayer.includes(\"const VERSION='17.7.3'\")||!dataLayer.includes('ordersRepo')||!dataLayer.includes('catalogRepo')||!dataLayer.includes('clientsRepo')||!dataLayer.includes('sun_v17_save_client_v1772')||!dataLayer.includes('sun_v17_clients_snapshot_v1773')||!dataLayer.includes('refreshServerClients')||!dataLayer.includes('CateriumDataV1772=api'))fail('v17.7.3 data layer missing');else ok('v17.7.3 data layer present');",'static v1773 data layer assertion');
|
|
write('tests/static-security.mjs',staticTest);
|
|
|
|
let releaseTest=read('tests/release-check.mjs');
|
|
releaseTest=releaseTest.replaceAll('data-layer-v1772.js','data-layer-v1773.js').replaceAll('dataLayer=read(\'core/data-layer-v1772.js\')','dataLayer=read(\'core/data-layer-v1773.js\')').replaceAll('17.7.2','17.7.3').replaceAll('20260909-v17-7-2-clients-foundation',RELEASE).replaceAll('v77-20260909-v17-7-3-clients-server-read',CACHE).replaceAll('CateriumDataV1771=api','CateriumDataV1772=api');
|
|
releaseTest=releaseTest.replaceAll('release notes exist through v17.7.3','release notes exist through v17.7.3');
|
|
releaseTest=replaceOne(releaseTest,"check(releaseManifest.dataLayerPhase===3&&releaseManifest.clientDataLayer===true&&releaseManifest.clientLegacyDoubleWrite===true,'release manifest records client data layer phase 3');","check(releaseManifest.dataLayerPhase===4&&releaseManifest.clientDataLayer===true&&releaseManifest.clientLegacyDoubleWrite===true&&releaseManifest.clientServerRead===true&&releaseManifest.clientLegacyFallback===true,'release manifest records client server-read data layer phase 4');",'release phase assertion');
|
|
releaseTest += "\ncheck(dataLayer.includes('sun_v17_clients_snapshot_v1773')&&dataLayer.includes('refreshServerClients')&&dataLayer.includes('compareClientSources'),'v17.7.3 client server snapshot and diagnostics are versioned');\ncheck(fs.existsSync(path.join(root,'ops/sql/SUPABASE-V17.7.3-CLIENTS-SERVER-READ.sql')),'v17.7.3 client server-read migration is versioned');\nif(bad)process.exit(1);\n";
|
|
// Remove the earlier terminal if(bad) so the appended checks execute.
|
|
releaseTest=releaseTest.replace(/if\(bad\)process\.exit\(1\);\n\ncheck\(dataLayer\.includes\('sun_v17_clients_snapshot_v1773'/,"check(dataLayer.includes('sun_v17_clients_snapshot_v1773'");
|
|
write('tests/release-check.mjs',releaseTest);
|
|
|
|
let appTest=read('tests/app.spec.mjs');
|
|
appTest=appTest.replaceAll('data-layer-v1772.js','data-layer-v1773.js').replaceAll('CateriumDataV1772','CateriumDataV1773').replaceAll("'17.7.2'","'17.7.3'").replaceAll('v17.7.2 client data layer','v17.7.3 client data layer');
|
|
appTest += `\n\ntest('v17.7.3 prefers server client profile while preserving legacy order metrics', async ({ page }) => {\n await page.addInitScript(()=>{\n localStorage.setItem('sunOrders',JSON.stringify([{id:11,contact:' Иван Петров ',phone:'',address:'Local A',date:'2026-09-01',time:'12:00',total:1000,prepayment:400,status:'Новый',lines:[]},{id:12,contact:'иван петров',phone:'',address:'Local B',date:'2026-09-02',time:'13:00',total:2000,prepayment:2000,status:'Новый',lines:[]}]))\n });\n await page.goto('/index.html',{waitUntil:'domcontentloaded'});\n await page.evaluate(()=>{\n window.SunCloudV2={getSession:()=>({user:{id:'u'}}),getWorkspace:()=>({id:'w'}),getSupportMode:()=>null,isSupportMode:()=>false,hasPermission:()=>true,getClientId:()=> 'e2e',getClient:()=>({rpc:async(name)=>{\n if(name==='sun_v17_clients_snapshot_v1773')return {data:[{client_key:'n:иван петров',name:'Иван Петров · сервер',phone:'',latest_address:'Server address',data:{identity:{name:'Иван Петров · сервер',phone:'',latestAddress:'Server address'},loyalty:{enabled:true,fixedDiscount:7}},version:4,updated_at:'2026-09-09T08:00:00Z',source_keys:['n:иван петров','o:11'],source_count:2,canonical_row_present:true}],error:null};\n if(name==='sun_v17_save_client_v1772')return {data:[],error:null};return {data:null,error:null};\n }})};\n });\n await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');\n const result=await page.evaluate(async()=>{const d=window.CateriumDataV1773;await d.clients.refreshServer({force:true,reason:'e2e'});const client=d.clients.get('n:иван петров'),compare=d.clients.compare();return {version:d.VERSION,name:client?.name,address:client?.latestAddress,orders:client?.orderCount,total:client?.totalSpent,discount:client?.loyalty?.fixedDiscount,serverCount:compare.serverCount,metrics:compare.orderMetricsEqual,alias:window.CateriumDataV1772===d};});\n expect(result.version).toBe('17.7.3');\n expect(result.name).toBe('Иван Петров · сервер');\n expect(result.address).toBe('Server address');\n expect(result.orders).toBe(2);\n expect(result.total).toBe(3000);\n expect(result.discount).toBe(7);\n expect(result.serverCount).toBe(1);\n expect(result.metrics).toBeTruthy();\n expect(result.alias).toBeTruthy();\n});\n`;
|
|
write('tests/app.spec.mjs',appTest);
|
|
|
|
console.log('v17.7.3 patch prepared');
|