caterium-app/public/core/data-layer-v1773.js
pavlov346346-source 424bf8ce87
Compact clients and explicit timed menu promotions (#31)
Add compact accessible client summaries and menu discounts in percent or rubles with explicit durations. Preserve ordinary catalog prices and order line snapshots; derive current prices and expire promotions automatically without background writes. Keep fractional prices, legacy sale compatibility, and regression coverage. Full pull-request QA passed. Publication remains gated by full main QA and byte-for-byte production asset and UI verification.
2026-09-19 09:34:50 +03:00

228 lines
23 KiB
JavaScript

(()=>{
'use strict';
if(window.CateriumDataV1773)return;
const VERSION='17.7.3';
const RELEASE='20260909-v17-7-3-clients-server-read';
const CLIENT_CACHE_KEY='cateriumClientsV1772';
const SERVER_CLIENT_CACHE_KEY='cateriumClientsServerV1773';
const LEGACY_LOYALTY_KEY='sunClientLoyaltyV1';
const LEGACY_COMM_KEY='sunClientCommunicationV1';
const listeners=new Map();
let tenantChanging=false,tenantEpoch=0;
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.CateriumDataV1773?.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+(window.CateriumPricing?.linePrice(line,item)??Number(line?.price??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(){
if(tenantChanging)return [];
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 readServerClientCache(){const value=storage.read(SERVER_CLIENT_CACHE_KEY,null);return value&&typeof value==='object'&&!Array.isArray(value)?value:null}
function currentServerRows(){const ws=String(workspace()?.id||''),cache=readServerClientCache();if(!ws||!cache||String(cache.workspaceId||'')!==ws)return [];return Array.isArray(cache.rows)?cache.rows:[]}
function serverProfile(row){
const data=row?.data&&typeof row.data==='object'&&!Array.isArray(row.data)?row.data:{},identity=data.identity&&typeof data.identity==='object'?data.identity:data;
const key=String(row?.client_key||row?.canonical_key||'');if(!key)return null;
const latestAddress=String(row?.latest_address||identity?.latestAddress||data?.latestAddress||'');
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)};
}
function serverListClients(){return currentServerRows().map(serverProfile).filter(Boolean)}
function mergeClientSources(local,server){
local=local||buildClients();server=server||serverListClients();
const byKey=new Map(local.map(item=>[item.key,{...clone(item),dataSource:'local'}]));
for(const row of server){
if(row.key.startsWith('o:')&&!byKey.has(row.key))continue;
const current=byKey.get(row.key)||{key:row.key,name:'',phone:'',latestAddress:'',addresses:[],orderIds:[],orderCount:0,totalSpent:0,loyalty:null,communication:null};
const addresses=[...new Set([...(current.addresses||[]),...(row.addresses||[]),row.latestAddress].map(v=>String(v||'').trim()).filter(Boolean))];
byKey.set(row.key,{...current,name:row.name||current.name,phone:row.phone||current.phone,latestAddress:row.latestAddress||current.latestAddress,addresses,loyalty:row.loyalty??current.loyalty,communication:row.communication??current.communication,serverVersion:row.serverVersion,serverUpdatedAt:row.serverUpdatedAt,serverSourceKeys:row.serverSourceKeys,serverSourceCount:row.serverSourceCount,canonicalRowPresent:row.canonicalRowPresent,dataSource:'server+local'});
}
return [...byKey.values()].sort((a,b)=>(b.orderCount-a.orderCount)||String(a.name||a.key).localeCompare(String(b.name||b.key),'ru'));
}
function listClients(options={}){if(tenantChanging)return [];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())}
function compareClientSources(){
const local=buildClients(),server=serverListClients(),merged=mergeClientSources(local,server),serverKeys=new Set(server.map(x=>x.key)),localKeys=new Set(local.map(x=>x.key));
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);
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};
}
let serverRefreshPromise=null;
async function refreshServerClients({force=false,reason='manual'}={}){
if(tenantChanging)return {status:'switching'};
if(serverRefreshPromise)return serverRefreshPromise;if(!isSignedIn())return {status:'local',diagnostics:compareClientSources()};
const c=cloud()?.getClient?.(),ws=workspace();if(!c?.rpc||!ws?.id)return {status:'offline',diagnostics:compareClientSources()};
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()};
const epoch=tenantEpoch,userId=session()?.user?.id;
serverRefreshPromise=(async()=>{
try{
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:[];
if(epoch!==tenantEpoch||tenantChanging||workspace()?.id!==ws.id||session()?.user?.id!==userId)return {status:'stale'};
storage.write(SERVER_CLIENT_CACHE_KEY,{workspaceId:String(ws.id),fetchedAt:new Date().toISOString(),rows:clone(rows)},{silent:true});
const diagnostics=compareClientSources();emit('clients',{reason:`server:${reason}`,changedKeys:rows.map(r=>String(r?.client_key||'')).filter(Boolean),clients:listClients(),diagnostics});
try{window.dispatchEvent(new CustomEvent('caterium:clients-server-refresh',{detail:{reason,count:rows.length,diagnostics}}))}catch(_){}
return {status:'loaded',count:rows.length,diagnostics};
}finally{serverRefreshPromise=null}
})();
return serverRefreshPromise;
}
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'}={}){
if(tenantChanging)return {changed:0,keys:[],clients:[]};
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,knownProfile){
if(tenantChanging)return {status:'switching',key};
const profile=knownProfile||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 epoch=tenantEpoch,userId=session()?.user?.id;
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;
if(epoch!==tenantEpoch||tenantChanging||workspace()?.id!==ws.id||session()?.user?.id!==userId)return {status:'stale',key};
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=[],epoch=tenantEpoch,workspaceId=workspace()?.id;for(const profile of listClients()){if(tenantChanging||epoch!==tenantEpoch||workspace()?.id!==workspaceId)break;try{result.push(await pushClient(profile.key,profile))}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.CateriumDataV1773?.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.CateriumDataV1773?.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,refreshServer:refreshServerClients,serverList:serverListClients,compare:compareClientSources,keyFromOrder:clientKeyFromOrder,keyFromValues:clientKeyFromValues,normalizePhone,normalizeName};
const diagnostics={
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}},
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.CateriumDataV1773=api;
window.CateriumDataV1772=api;
window.CateriumDataV1771=api;
window.CateriumDataV1770=api;
window.CateriumData=api;
installLegacyClientBridge();
window.addEventListener('sun:cloud-tenant-changing',()=>{tenantChanging=true;tenantEpoch++;clearTimeout(serverPushTimer);serverPushTimer=0;pendingServerKeys.clear()});
window.addEventListener('sun:cloud-state-applied',()=>{tenantChanging=false});
adoptLegacy({pushServer:false,reason:'boot'});
window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>{clientsRepo.refreshServer({force:true,reason:'permissions'}).catch(()=>{});clientsRepo.pushAll().catch(()=>{})},800));
window.addEventListener('sun:cloud-sync-complete',()=>setTimeout(()=>{clientsRepo.adoptLegacy({pushServer:true,reason:'cloud-sync'});clientsRepo.refreshServer({force:true,reason:'cloud-sync'}).catch(()=>{})},500));
setTimeout(()=>clientsRepo.refreshServer({reason:'boot'}).catch(()=>{}),1400);
setTimeout(()=>clientsRepo.pushAll().catch(()=>{}),3500);
try{window.dispatchEvent(new CustomEvent('caterium:data-ready',{detail:{version:VERSION,phase:4,clients:true,clientServerRead:true}}))}catch(_){}
})();