From a98e636937552743f4b3c835f3a302ee2cb48c4a Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Sat, 12 Sep 2026 14:08:10 +0300 Subject: [PATCH] fix: gate production promotion on QA success; remove dead data-layer files promote-production.yml triggered on push to main independently of qa.yml, with no branch protection configured on the repo - a failing QA run (npm audit, static security tests, e2e) never blocked production. Switch it to the same workflow_run pattern deploy-timeweb.yml already uses: only promote the exact commit QA just passed. Also removes public/core/data-layer-v1770/1771/1772.js: only v1773 is ever loaded (index.html, performance.js's loadDataLayer, service-worker cache all reference v1773 only) - the older three were dead weight shipped to every visitor. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/promote-production.yml | 18 ++- public/core/data-layer-v1770.js | 68 --------- public/core/data-layer-v1771.js | 93 ------------ public/core/data-layer-v1772.js | 172 ----------------------- 4 files changed, 12 insertions(+), 339 deletions(-) delete mode 100644 public/core/data-layer-v1770.js delete mode 100644 public/core/data-layer-v1771.js delete mode 100644 public/core/data-layer-v1772.js diff --git a/.github/workflows/promote-production.yml b/.github/workflows/promote-production.yml index 8ec3684..1eea1ac 100644 --- a/.github/workflows/promote-production.yml +++ b/.github/workflows/promote-production.yml @@ -1,27 +1,33 @@ name: Caterium Direct Production on: - push: - branches: [main] + workflow_run: + workflows: ["Caterium QA"] + types: [completed] + workflow_dispatch: permissions: contents: write concurrency: group: caterium-production-promotion - cancel-in-progress: true + cancel-in-progress: false jobs: promote: + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main') runs-on: ubuntu-latest steps: - - name: Checkout current main + - name: Checkout tested main revision uses: actions/checkout@v4 with: - ref: main + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || 'main' }} fetch-depth: 0 - - name: Publish immediately to production branch + - name: Publish tested revision to production branch shell: bash run: | set -euo pipefail diff --git a/public/core/data-layer-v1770.js b/public/core/data-layer-v1770.js deleted file mode 100644 index c6854b7..0000000 --- a/public/core/data-layer-v1770.js +++ /dev/null @@ -1,68 +0,0 @@ -(()=>{ - 'use strict'; - if(window.CateriumDataV1770)return; - - const VERSION='17.7.0'; - const RELEASE='20260908-v17-7-0-architecture-foundation'; - 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 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 getOrders(){const live=runtimeOrders();return clone(live||storage.read('sunOrders',[])||[])} - function replaceOrders(next,{persistLocal=true,reason='replace'}={}){ - const list=Array.isArray(next)?clone(next):[]; - const live=runtimeOrders(); - if(live){live.length=0;live.push(...clone(list))} - if(persistLocal){const p=runtimePersist();if(p){try{p()}catch(error){console.warn('[Caterium Data] persist fallback',error);storage.write('sunOrders',list,{silent:true})}}else storage.write('sunOrders',list,{silent:true})} - else storage.write('sunOrders',list,{silent:true}); - 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();const index=list.findIndex(o=>String(o?.id)===String(id));if(index<0)return null; - const current=clone(list[index]);const 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} - } - - const ordersRepo={list:getOrders,get:getOrder,replace:replaceOrders,update:updateOrder,applyServerChanges:applyServerOrders}; - const diagnostics={ - snapshot(){return {version:VERSION,workspaceId:workspace()?.id||null,signedIn:isSignedIn(),supportReadOnly:isSupportReadOnly(),orderCount:getOrders().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()}))} - }; - - window.CateriumDataV1770={VERSION,RELEASE,storage,orders:ordersRepo,subscribe,emit,cloud,session,workspace,isSignedIn,isSupportReadOnly,canWrite,diagnostics}; - window.CateriumData=window.CateriumDataV1770; - try{window.dispatchEvent(new CustomEvent('caterium:data-ready',{detail:{version:VERSION}}))}catch(_){} -})(); diff --git a/public/core/data-layer-v1771.js b/public/core/data-layer-v1771.js deleted file mode 100644 index b0ce91a..0000000 --- a/public/core/data-layer-v1771.js +++ /dev/null @@ -1,93 +0,0 @@ -(()=>{ - '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(_){} -})(); diff --git a/public/core/data-layer-v1772.js b/public/core/data-layer-v1772.js deleted file mode 100644 index 8151fdf..0000000 --- a/public/core/data-layer-v1772.js +++ /dev/null @@ -1,172 +0,0 @@ -(()=>{ - 'use strict'; - if(window.CateriumDataV1772)return; - - const VERSION='17.7.2'; - const RELEASE='20260909-v17-7-2-clients-foundation'; - const CLIENT_CACHE_KEY='cateriumClientsV1772'; - const LEGACY_LOYALTY_KEY='sunClientLoyaltyV1'; - const LEGACY_COMM_KEY='sunClientCommunicationV1'; - 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)}); - if(persistLocal)queueMicrotask(()=>window.CateriumDataV1772?.clients?.adoptLegacy?.({pushServer:true,reason:`orders:${reason}`})); - 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 normalizePhone=value=>String(value||'').replace(/\D/g,''); - const normalizeName=value=>String(value||'').trim().replace(/\s+/g,' ').toLowerCase(); - const clientKeyFromValues=(phone,name,orderId='')=>{const p=normalizePhone(phone);if(p)return `p:${p}`;const n=normalizeName(name);if(n)return `n:${n}`;return orderId!==''&&orderId!=null?`o:${String(orderId)}`:''}; - const clientKeyFromOrder=order=>clientKeyFromValues(order?.phone,order?.contact,order?.id); - const orderStamp=order=>`${String(order?.date||'').padStart(10,'0')}T${String(order?.time||'').padStart(5,'0')}`; - function orderTotal(order){ - const explicit=Number(order?.total);if(Number.isFinite(explicit))return explicit; - const catalog=getCatalog();return (Array.isArray(order?.lines)?order.lines:[]).reduce((sum,line)=>{const item=catalog.find(x=>String(x?.id)===String(line?.id));return sum+Number(item?.price||0)*Number(line?.qty||0)},0); - } - function readObject(key){const value=storage.read(key,{});return value&&typeof value==='object'&&!Array.isArray(value)?value:{}} - function cacheCore(profile){return {key:profile.key,name:String(profile.name||''),phone:String(profile.phone||''),latestAddress:String(profile.latestAddress||''),loyalty:profile.loyalty?clone(profile.loyalty):null,communication:profile.communication?clone(profile.communication):null,serverVersion:Number(profile.serverVersion||0)||null,serverUpdatedAt:String(profile.serverUpdatedAt||'')}} - function readClientCache(){return readObject(CLIENT_CACHE_KEY)} - function writeClientCache(map){localStorage.setItem(CLIENT_CACHE_KEY,JSON.stringify(map||{}));return map} - function buildClients(){ - const cached=readClientCache(),loyalty=readObject(LEGACY_LOYALTY_KEY),communication=readObject(LEGACY_COMM_KEY),people=new Map(); - const ensure=key=>{if(!people.has(key)){const old=cached[key]||{};people.set(key,{key,name:String(old.name||''),phone:String(old.phone||''),latestAddress:String(old.latestAddress||''),addresses:new Set(),orderIds:[],orderCount:0,totalSpent:0,latestStamp:'',loyalty:null,communication:null,serverVersion:Number(old.serverVersion||0)||null,serverUpdatedAt:String(old.serverUpdatedAt||'')})}return people.get(key)}; - for(const order of getOrders()){ - const key=clientKeyFromOrder(order);if(!key)continue;const person=ensure(key),stamp=orderStamp(order),address=String(order?.address||'').trim(); - person.orderIds.push(String(order?.id??''));person.orderCount++;if(String(order?.status||'')!=='Отменён')person.totalSpent+=Math.max(0,Number(orderTotal(order)||0));if(address)person.addresses.add(address); - if(!person.latestStamp||stamp>=person.latestStamp){person.latestStamp=stamp;person.name=String(order?.contact||person.name||'');person.phone=String(order?.phone||person.phone||'');person.latestAddress=address||person.latestAddress;} - } - const allKeys=new Set([...people.keys(),...Object.keys(cached),...Object.keys(loyalty),...Object.keys(communication)]); - for(const key of allKeys){const person=ensure(key);person.loyalty=Object.prototype.hasOwnProperty.call(loyalty,key)?clone(loyalty[key]):null;person.communication=Object.prototype.hasOwnProperty.call(communication,key)?clone(communication[key]):null;if(person.latestAddress)person.addresses.add(person.latestAddress)} - return [...people.values()].map(person=>({...person,addresses:[...person.addresses],orderIds:person.orderIds.filter(Boolean),totalSpent:Math.round(person.totalSpent*100)/100,latestStamp:undefined})).sort((a,b)=>(b.orderCount-a.orderCount)||String(a.name||a.key).localeCompare(String(b.name||b.key),'ru')); - } - function listClients(){return clone(buildClients())} - function getClient(key){return listClients().find(x=>x.key===String(key))||null} - function writeLegacyMap(storageKey,key,value){ - const map=readObject(storageKey);if(value==null)delete map[key];else map[key]=clone(value); - bridgeSuppressed=true;try{localStorage.setItem(storageKey,JSON.stringify(map))}finally{bridgeSuppressed=false} - return map; - } - function persistClientCacheFromList(list){const map={};for(const item of list||[])map[item.key]=cacheCore(item);writeClientCache(map);return map} - function changedClientKeys(before,next){const keys=new Set([...Object.keys(before||{}),...Object.keys(next||{})]),changed=[];for(const key of keys){if(JSON.stringify(before?.[key]||null)!==JSON.stringify(next?.[key]||null))changed.push(key)}return changed} - function adoptLegacy({pushServer=false,reason='legacy-adopt'}={}){ - const before=readClientCache(),list=buildClients(),after={};for(const item of list)after[item.key]=cacheCore(item);const changed=changedClientKeys(before,after);writeClientCache(after);emit('clients',{reason,changedKeys:changed,clients:clone(list)});if(pushServer&&changed.length)scheduleServerPush(changed);return {changed:changed.length,keys:changed,clients:clone(list)}; - } - function upsertClient(profile,{pushServer=true,reason='client.upsert'}={}){ - const key=String(profile?.key||clientKeyFromValues(profile?.phone,profile?.name,profile?.orderId)||'');if(!key)return null; - const current=getClient(key)||{key,name:'',phone:'',latestAddress:'',loyalty:null,communication:null};const next={...current,...clone(profile),key}; - if(Object.prototype.hasOwnProperty.call(profile||{},'loyalty'))writeLegacyMap(LEGACY_LOYALTY_KEY,key,next.loyalty); - if(Object.prototype.hasOwnProperty.call(profile||{},'communication'))writeLegacyMap(LEGACY_COMM_KEY,key,next.communication); - const cache=readClientCache();cache[key]=cacheCore(next);writeClientCache(cache);emit('clients',{reason,changedKeys:[key],clients:listClients()});if(pushServer)scheduleServerPush([key]);return clone(getClient(key)||next); - } - function setClientLoyalty(key,value,{pushServer=true}={}){writeLegacyMap(LEGACY_LOYALTY_KEY,String(key),value);adoptLegacy({pushServer:false,reason:'client.loyalty'});if(pushServer)scheduleServerPush([String(key)]);return clone(getClient(key)?.loyalty||null)} - function setClientCommunication(key,value,{pushServer=true}={}){writeLegacyMap(LEGACY_COMM_KEY,String(key),value);adoptLegacy({pushServer:false,reason:'client.communication'});if(pushServer)scheduleServerPush([String(key)]);return clone(getClient(key)?.communication||null)} - function serverPayload(profile){return {schemaVersion:1,key:profile.key,identity:{name:String(profile.name||''),phone:String(profile.phone||''),latestAddress:String(profile.latestAddress||'')},loyalty:profile.loyalty?clone(profile.loyalty):null,communication:profile.communication?clone(profile.communication):null,source:'data-layer-v1772',updatedAt:new Date().toISOString()}} - async function pushClient(key){ - const profile=getClient(key);if(!profile)return {status:'missing',key};if(!isSignedIn())return {status:'local',key};if(isSupportReadOnly()||!canWrite('clients.edit'))return {status:'read-only',key}; - const c=cloud()?.getClient?.(),ws=workspace();if(!c?.rpc||!ws?.id)return {status:'offline',key}; - const cached=readClientCache()[key]||{},expected=Number(cached.serverVersion||0)||null; - const {data,error}=await c.rpc('sun_v17_save_client_v1772',{p_workspace:ws.id,p_client_key:key,p_profile:serverPayload(profile),p_expected_version:expected,p_client_id:cloud()?.getClientId?.()||'browser'});if(error)throw error; - const row=Array.isArray(data)?data[0]:data;if(row){const map=readClientCache();map[key]={...(map[key]||cacheCore(profile)),serverVersion:Number(row.version||0)||null,serverUpdatedAt:String(row.updated_at||'')};writeClientCache(map)} - return {status:'saved',key,version:Number(row?.version||0)||null}; - } - const pendingServerKeys=new Set();let serverPushTimer=0; - function scheduleServerPush(keys){for(const key of keys||[])if(key)pendingServerKeys.add(String(key));if(serverPushTimer)return;serverPushTimer=setTimeout(async()=>{serverPushTimer=0;const keys=[...pendingServerKeys];pendingServerKeys.clear();for(const key of keys){try{await pushClient(key)}catch(error){console.warn('[Caterium Clients] server save failed',key,error?.message||error)}}},250)} - async function pushAllClients(){const result=[];for(const profile of listClients()){try{result.push(await pushClient(profile.key))}catch(error){result.push({status:'error',key:profile.key,error:String(error?.message||error)})}}return result} - - let bridgeSuppressed=false; - function installLegacyClientBridge(){ - if(window.__cateriumClientStorageBridgeV1772||typeof Storage==='undefined')return;window.__cateriumClientStorageBridgeV1772=true; - const proto=Storage.prototype,rawSet=proto.setItem,rawRemove=proto.removeItem; - proto.setItem=function(key,value){const result=rawSet.call(this,key,value);if(!bridgeSuppressed&&this===localStorage&&(key===LEGACY_LOYALTY_KEY||key===LEGACY_COMM_KEY))queueMicrotask(()=>window.CateriumDataV1772?.clients?.adoptLegacy?.({pushServer:true,reason:`legacy-write:${key}`}));return result}; - proto.removeItem=function(key){const result=rawRemove.call(this,key);if(!bridgeSuppressed&&this===localStorage&&(key===LEGACY_LOYALTY_KEY||key===LEGACY_COMM_KEY))queueMicrotask(()=>window.CateriumDataV1772?.clients?.adoptLegacy?.({pushServer:true,reason:`legacy-remove:${key}`}));return result}; - } - - 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 clientsRepo={list:listClients,get:getClient,upsert:upsertClient,setLoyalty:setClientLoyalty,setCommunication:setClientCommunication,adoptLegacy,push:pushClient,pushAll:pushAllClients,keyFromOrder:clientKeyFromOrder,keyFromValues:clientKeyFromValues,normalizePhone,normalizeName}; - const diagnostics={ - 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}}, - 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,clients:clientsRepo,subscribe,emit,cloud,session,workspace,isSignedIn,isSupportReadOnly,canWrite,diagnostics}; - window.CateriumDataV1772=api; - window.CateriumDataV1771=api; - window.CateriumDataV1770=api; - window.CateriumData=api; - installLegacyClientBridge(); - adoptLegacy({pushServer:false,reason:'boot'}); - window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>clientsRepo.pushAll().catch(()=>{}),800)); - window.addEventListener('sun:cloud-sync-complete',()=>setTimeout(()=>clientsRepo.adoptLegacy({pushServer:true,reason:'cloud-sync'}),300)); - setTimeout(()=>clientsRepo.pushAll().catch(()=>{}),3500); - try{window.dispatchEvent(new CustomEvent('caterium:data-ready',{detail:{version:VERSION,phase:3,clients:true}}))}catch(_){} -})();