Caterium v17.7.3 — clients server read
Server-preferred normalized client profiles with legacy fallback, canonical snapshot RPC, preserved order metrics, release metadata and tests.
This commit is contained in:
parent
815cc602c5
commit
4aa66d0a56
@ -1,6 +1,6 @@
|
||||
{
|
||||
"app": "Caterium",
|
||||
"version": "v17.7.2",
|
||||
"version": "v17.7.3",
|
||||
"channel": "production",
|
||||
"schema": 17,
|
||||
"legacyStateKept": true,
|
||||
@ -11,7 +11,7 @@
|
||||
"serverReady": true,
|
||||
"workspaceAutoDiscovery": true,
|
||||
"invitesTemporarilyDisabled": false,
|
||||
"pwaCache": "v77-20260909-v17-7-2-clients-foundation",
|
||||
"pwaCache": "v78-20260909-v17-7-3-clients-server-read",
|
||||
"fullOfferDescriptions": true,
|
||||
"dynamicOfferRows": true,
|
||||
"pdfOfferDescriptionFix": true,
|
||||
@ -108,7 +108,7 @@
|
||||
"catalogCompositionTildaEndpoint": "getproduct",
|
||||
"catalogCompositionPremiumForceRefresh": true,
|
||||
"catalogCompositionCacheRequiresPremium": true,
|
||||
"notes": "Client foundation: canonical client identity, loyalty and communication data routed through Caterium Data Layer with legacy localStorage compatibility and additive normalized server writes.",
|
||||
"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.",
|
||||
"catalogPhotoSources": 113,
|
||||
"catalogPhotosStoredInCatalog": true,
|
||||
"catalogLegacyPhotosInCatalog": 60,
|
||||
@ -246,7 +246,7 @@
|
||||
"signupTrial": "14-day Full",
|
||||
"developerMfaInputSelectorFixed": true,
|
||||
"developerMfaRepeatedValidationToastsFixed": true,
|
||||
"release": "20260909-v17-7-2-clients-foundation",
|
||||
"release": "20260909-v17-7-3-clients-server-read",
|
||||
"registrationFlow": "email-password-confirm-company-auto-login",
|
||||
"emailConfirmationRequired": false,
|
||||
"employeeInviteLinks": false,
|
||||
@ -327,7 +327,7 @@
|
||||
"cloudConflictMaxRetries": 4,
|
||||
"errorLogDedupMinutes": 5,
|
||||
"networkFailureBackoff": true,
|
||||
"stabilityLoggerVersion": "17.7.2",
|
||||
"stabilityLoggerVersion": "17.7.3",
|
||||
"developerAdminCheckTtlMs": 30000,
|
||||
"supportRefreshLightweight": true,
|
||||
"supportRefreshIntervalMs": 20000,
|
||||
@ -368,12 +368,18 @@
|
||||
"errorTelemetryRejectsLocalFile": true,
|
||||
"errorTelemetryServerDedupMinutes": 5,
|
||||
"errorTelemetryRetentionDays": 30,
|
||||
"dataLayerPhase": 3,
|
||||
"dataLayerPhase": 4,
|
||||
"dataLayerLegacyOrderWrites": true,
|
||||
"dataLayerLegacyCatalogWrites": true,
|
||||
"clientDataLayer": true,
|
||||
"clientLegacyDoubleWrite": true,
|
||||
"clientCanonicalIdentity": "phone > normalized name > order id fallback",
|
||||
"clientNormalizedRpc": "sun_v17_save_client_v1772",
|
||||
"clientLegacyRowsPreserved": true
|
||||
"clientLegacyRowsPreserved": true,
|
||||
"clientServerRead": true,
|
||||
"clientServerPreferred": true,
|
||||
"clientServerSnapshotRpc": "sun_v17_clients_snapshot_v1773",
|
||||
"clientLegacyFallback": true,
|
||||
"clientOrderMetricsSource": "legacy orders verified locally",
|
||||
"clientServerCache": "cateriumClientsServerV1773"
|
||||
}
|
||||
|
||||
9
docs/releases/V17.7.3-CHANGES.txt
Normal file
9
docs/releases/V17.7.3-CHANGES.txt
Normal file
@ -0,0 +1,9 @@
|
||||
Caterium v17.7.3 — Clients Server Read
|
||||
|
||||
- Client list and cards now consume Caterium Data Layer profiles instead of rebuilding identity only inside the UI.
|
||||
- Added canonical server snapshot RPC sun_v17_clients_snapshot_v1773.
|
||||
- Server profile data is preferred when available; local/legacy derivation remains the fallback.
|
||||
- Order count, turnover, paid amount and order history are still calculated from the current order set and compared in Data Layer diagnostics.
|
||||
- Legacy o: client rows are preserved; the snapshot canonicalizes them to p:/n: identities when possible and avoids duplicate display.
|
||||
- Added workspace-scoped server client cache and refresh events.
|
||||
- Existing v17.7.2 client write RPC and legacy loyalty/communication double-write remain compatible.
|
||||
58
ops/sql/SUPABASE-V17.7.3-CLIENTS-SERVER-READ.sql
Normal file
58
ops/sql/SUPABASE-V17.7.3-CLIENTS-SERVER-READ.sql
Normal file
@ -0,0 +1,58 @@
|
||||
-- Caterium v17.7.3 — canonical client server-read snapshot
|
||||
-- Read-only/additive migration. Existing p:/n:/o: rows are preserved.
|
||||
|
||||
create or replace function public.sun_v17_clients_snapshot_v1773(p_workspace uuid)
|
||||
returns table(
|
||||
client_key text,
|
||||
name text,
|
||||
phone text,
|
||||
latest_address text,
|
||||
data jsonb,
|
||||
version bigint,
|
||||
updated_at timestamptz,
|
||||
source_keys text[],
|
||||
source_count bigint,
|
||||
canonical_row_present boolean
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $function$
|
||||
begin
|
||||
if public.sun_member_role(p_workspace) is null then
|
||||
raise exception 'Access denied';
|
||||
end if;
|
||||
if not public.sun_workspace_has_feature(p_workspace,'clients') then
|
||||
raise exception 'Клиенты недоступны';
|
||||
end if;
|
||||
|
||||
return query
|
||||
with prepared as (
|
||||
select c.*,
|
||||
regexp_replace(coalesce(nullif(trim(c.phone),''),nullif(trim(c.data#>>'{identity,phone}'),''),nullif(trim(c.data->>'phone'),''),''),'[^0-9]','','g') as phone_digits,
|
||||
lower(regexp_replace(coalesce(nullif(trim(c.name),''),nullif(trim(c.data#>>'{identity,name}'),''),nullif(trim(c.data->>'name'),''),''),'[[:space:]]+',' ','g')) as name_norm
|
||||
from public.sun_v17_clients c
|
||||
where c.workspace_id=p_workspace
|
||||
), canonical as (
|
||||
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
|
||||
from prepared p
|
||||
), ranked as (
|
||||
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
|
||||
from canonical c
|
||||
), grouped as (
|
||||
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
|
||||
from canonical c group by c.canonical_key
|
||||
)
|
||||
select r.canonical_key,
|
||||
coalesce(nullif(trim(r.name),''),nullif(trim(r.data#>>'{identity,name}'),''),nullif(trim(r.data->>'name'),'')),
|
||||
coalesce(nullif(trim(r.phone),''),nullif(trim(r.data#>>'{identity,phone}'),''),nullif(trim(r.data->>'phone'),'')),
|
||||
coalesce(nullif(trim(r.latest_address),''),nullif(trim(r.data#>>'{identity,latestAddress}'),''),nullif(trim(r.data->>'latestAddress'),'')),
|
||||
coalesce(r.data,'{}'::jsonb),r.version,r.updated_at,g.source_keys,g.source_count,g.canonical_row_present
|
||||
from ranked r join grouped g on g.canonical_key=r.canonical_key
|
||||
where r.rn=1
|
||||
order by r.updated_at desc,r.canonical_key;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
revoke execute on function public.sun_v17_clients_snapshot_v1773(uuid) from public, anon;
|
||||
grant execute on function public.sun_v17_clients_snapshot_v1773(uuid) to authenticated, service_role;
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "caterium-app",
|
||||
"version": "17.7.2",
|
||||
"version": "17.7.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "caterium-app",
|
||||
"version": "17.7.2",
|
||||
"version": "17.7.3",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.51.0",
|
||||
"http-server": "^14.1.1",
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "caterium-app",
|
||||
"private": true,
|
||||
"version": "17.7.2",
|
||||
"version": "17.7.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/data-layer-v1772.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js",
|
||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js",
|
||||
"test:static": "node tests/static-security.mjs",
|
||||
"check:release": "node tests/release-check.mjs",
|
||||
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release",
|
||||
|
||||
@ -4221,8 +4221,8 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
|
||||
'use strict';
|
||||
if (window.SunStabilityV17) return;
|
||||
|
||||
const VERSION = '17.7.2';
|
||||
const RELEASE = 'v17.7.2 Clients Foundation';
|
||||
const VERSION = '17.7.3';
|
||||
const RELEASE = 'v17.7.3 Clients Server Read';
|
||||
const DB_NAME = 'SunStabilityV17';
|
||||
const DB_VERSION = 1;
|
||||
const DAILY_KEY = 'sunV17DailyBackup';
|
||||
|
||||
215
public/core/data-layer-v1773.js
Normal file
215
public/core/data-layer-v1773.js
Normal file
@ -0,0 +1,215 @@
|
||||
(()=>{
|
||||
'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();
|
||||
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+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 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(){
|
||||
const local=buildClients(),byKey=new Map(local.map(item=>[item.key,{...clone(item),dataSource:'local'}]));
|
||||
for(const server of serverListClients()){
|
||||
if(server.key.startsWith('o:')&&!byKey.has(server.key))continue;
|
||||
const current=byKey.get(server.key)||{key:server.key,name:'',phone:'',latestAddress:'',addresses:[],orderIds:[],orderCount:0,totalSpent:0,loyalty:null,communication:null};
|
||||
const addresses=[...new Set([...(current.addresses||[]),...(server.addresses||[]),server.latestAddress].map(v=>String(v||'').trim()).filter(Boolean))];
|
||||
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'});
|
||||
}
|
||||
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={}){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(),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(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()};
|
||||
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:[];
|
||||
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'}={}){
|
||||
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.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();
|
||||
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(_){}
|
||||
})();
|
||||
@ -1,7 +1,7 @@
|
||||
(()=>{
|
||||
'use strict';
|
||||
const VERSION='17.7.2';
|
||||
const RELEASE='20260909-v17-7-2-clients-foundation';
|
||||
const VERSION='17.7.3';
|
||||
const RELEASE='20260909-v17-7-3-clients-server-read';
|
||||
const critical=img=>img.closest('header,.brand,#sunCloudAuthGate,.sun-auth-gate')||img.id==='sunLoginLogo'||img.classList.contains('sun-live-catalog-logo');
|
||||
const tune=img=>{
|
||||
if(!(img instanceof HTMLImageElement)||critical(img))return;
|
||||
@ -92,8 +92,8 @@
|
||||
function startMemoryTimer(){if(memoryTimer)return;memoryTimer=setInterval(()=>{if(!document.hidden&&developerVisible())enhanceDeveloperMemory({force:true}).catch(()=>{})},MEMORY_REFRESH_MS)}
|
||||
|
||||
function loadDataLayer(){
|
||||
if(window.CateriumDataV1772||document.getElementById('cateriumDataV1772Script'))return;
|
||||
const script=document.createElement('script');script.id='cateriumDataV1772Script';script.src=`core/data-layer-v1772.js?v=${RELEASE}`;script.async=false;script.onerror=()=>console.error('[Caterium] Не загрузился data-layer-v1772.js');document.head.appendChild(script);
|
||||
if(window.CateriumDataV1773||document.getElementById('cateriumDataV1773Script'))return;
|
||||
const script=document.createElement('script');script.id='cateriumDataV1773Script';script.src=`core/data-layer-v1773.js?v=${RELEASE}`;script.async=false;script.onerror=()=>console.error('[Caterium] Не загрузился data-layer-v1773.js');document.head.appendChild(script);
|
||||
}
|
||||
function loadServerAutomation(){
|
||||
if(window.CateriumServerAutomationV1770||document.getElementById('cateriumServerAutomationV1770Script'))return;
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
public/legacy/bootstrap.js
vendored
2
public/legacy/bootstrap.js
vendored
File diff suppressed because one or more lines are too long
@ -1,8 +1,8 @@
|
||||
const CACHE='sun-catering-pwa-v77-20260909-v17-7-2-clients-foundation';
|
||||
const VERSION='20260909-v17-7-2-clients-foundation';
|
||||
const CACHE='sun-catering-pwa-v78-20260909-v17-7-3-clients-server-read';
|
||||
const VERSION='20260909-v17-7-3-clients-server-read';
|
||||
const CORE=[
|
||||
'./','./index.html',
|
||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/data-layer-v1772.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
|
||||
'./catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg',
|
||||
'./sun-logo.png','./caterium-login-logo.png','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest',
|
||||
|
||||
@ -156,7 +156,7 @@ test('Developer Console memory refresh is bounded and does not react to its own
|
||||
const result=await page.evaluate(()=>({count:window.__rpcCount,box:document.querySelectorAll('#sunDevMemoryV1761').length,version:document.getElementById('sunDevReleaseVersion')?.textContent}));
|
||||
expect(result.count).toBeLessThanOrEqual(2);
|
||||
expect(result.box).toBe(1);
|
||||
expect(result.version).toBe('17.7.2');
|
||||
expect(result.version).toBe('17.7.3');
|
||||
});
|
||||
test('mobile body does not overflow viewport', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name!=='mobile-390'); await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await page.waitForTimeout(500);
|
||||
@ -172,7 +172,7 @@ test('v17.6.5 stays free of timer page errors during idle', async ({ page }, tes
|
||||
await page.waitForTimeout(5500);
|
||||
expect(errors).toEqual([]);
|
||||
const runtimeSource=fs.readFileSync(path.join(process.cwd(),'public','app-runtime.js'),'utf8');
|
||||
expect(runtimeSource).toContain("const VERSION = '17.7.2'");
|
||||
expect(runtimeSource).toContain("const VERSION = '17.7.3'");
|
||||
});
|
||||
|
||||
test('v17.6.5 support refresh uses lightweight cloud API', async ({ page }) => {
|
||||
@ -261,9 +261,9 @@ test('v17.6.9 offer workspace module boots', async ({ page }) => {
|
||||
|
||||
test('v17.7.1 data layer can update orders without direct UI globals', async ({ page }) => {
|
||||
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||
await injectCore(page,'data-layer-v1772.js','CateriumDataV1772');
|
||||
const result=await page.evaluate(()=>{const d=window.CateriumDataV1772;d.orders.replace([{id:77,status:'Новый',total:1000}],{persistLocal:false,reason:'e2e-seed'});const before=d.orders.get(77);d.orders.update(77,o=>({...o,status:'Тест'}),{persistLocal:false});return {before:before.status,after:d.orders.get(77).status,version:d.VERSION,alias:window.CateriumDataV1770===d}});
|
||||
expect(result).toEqual({before:'Новый',after:'Тест',version:'17.7.2',alias:true});
|
||||
await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');
|
||||
const result=await page.evaluate(()=>{const d=window.CateriumDataV1773;d.orders.replace([{id:77,status:'Новый',total:1000}],{persistLocal:false,reason:'e2e-seed'});const before=d.orders.get(77);d.orders.update(77,o=>({...o,status:'Тест'}),{persistLocal:false});return {before:before.status,after:d.orders.get(77).status,version:d.VERSION,alias:window.CateriumDataV1770===d}});
|
||||
expect(result).toEqual({before:'Новый',after:'Тест',version:'17.7.3',alias:true});
|
||||
});
|
||||
|
||||
test('v17.7.0 server automation applies returned normalized orders through data layer', async ({ page }) => {
|
||||
@ -277,32 +277,32 @@ test('v17.7.0 server automation applies returned normalized orders through data
|
||||
|
||||
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 injectCore(page,'data-layer-v1772.js','CateriumDataV1772');
|
||||
await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');
|
||||
const result=await page.evaluate(()=>{
|
||||
const d=window.CateriumDataV1772,beforeOrders=d.orders.list(),beforeCatalog=d.catalog.list();
|
||||
const d=window.CateriumDataV1773,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.2',alias:true,hasOrder:true,hasCatalog:true});
|
||||
expect(result).toEqual({version:'17.7.3',alias:true,hasOrder:true,hasCatalog:true});
|
||||
});
|
||||
|
||||
|
||||
test('v17.7.2 client data layer uses phone then normalized name identity', async ({ page }) => {
|
||||
test('v17.7.3 client data layer uses phone then normalized name identity', async ({ page }) => {
|
||||
await page.addInitScript(()=>{
|
||||
localStorage.setItem('sunOrders',JSON.stringify([{id:1,contact:' Иван Петров ',phone:'',address:'A',total:1000,lines:[]},{id:2,contact:'иван петров',phone:'',address:'B',total:2000,lines:[]},{id:3,contact:'Анна',phone:'+7 (999) 123-45-67',total:3000,lines:[]}]))
|
||||
localStorage.setItem('sunClientLoyaltyV1',JSON.stringify({'n:иван петров':{enabled:true,fixedDiscount:5}}));
|
||||
localStorage.setItem('sunClientCommunicationV1',JSON.stringify({'p:79991234567':{preferred:'telegram'}}));
|
||||
});
|
||||
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||
await injectCore(page,'data-layer-v1772.js','CateriumDataV1772');
|
||||
await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');
|
||||
const result=await page.evaluate(()=>{
|
||||
const d=window.CateriumDataV1772,clients=d.clients.list();
|
||||
const d=window.CateriumDataV1773,clients=d.clients.list();
|
||||
return {version:d.VERSION,nameKey:d.clients.keyFromValues('', ' Иван Петров '),phoneKey:d.clients.keyFromValues('+7 (999) 123-45-67','Анна'),ivan:clients.find(x=>x.key==='n:иван петров'),anna:clients.find(x=>x.key==='p:79991234567'),alias:window.CateriumDataV1771===d};
|
||||
});
|
||||
expect(result.version).toBe('17.7.2');
|
||||
expect(result.version).toBe('17.7.3');
|
||||
expect(result.nameKey).toBe('n:иван петров');
|
||||
expect(result.phoneKey).toBe('p:79991234567');
|
||||
expect(result.ivan.orderCount).toBe(2);
|
||||
@ -311,3 +311,28 @@ test('v17.7.2 client data layer uses phone then normalized name identity', async
|
||||
expect(result.anna.communication.preferred).toBe('telegram');
|
||||
expect(result.alias).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
test('v17.7.3 prefers server client profile while preserving legacy order metrics', async ({ page }) => {
|
||||
await page.addInitScript(()=>{
|
||||
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:[]}]))
|
||||
});
|
||||
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||
await page.evaluate(()=>{
|
||||
window.SunCloudV2={getSession:()=>({user:{id:'u'}}),getWorkspace:()=>({id:'w'}),getSupportMode:()=>null,isSupportMode:()=>false,hasPermission:()=>true,getClientId:()=> 'e2e',getClient:()=>({rpc:async(name)=>{
|
||||
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};
|
||||
if(name==='sun_v17_save_client_v1772')return {data:[],error:null};return {data:null,error:null};
|
||||
}})};
|
||||
});
|
||||
await injectCore(page,'data-layer-v1773.js','CateriumDataV1773');
|
||||
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};});
|
||||
expect(result.version).toBe('17.7.3');
|
||||
expect(result.name).toBe('Иван Петров · сервер');
|
||||
expect(result.address).toBe('Server address');
|
||||
expect(result.orders).toBe(2);
|
||||
expect(result.total).toBe(3000);
|
||||
expect(result.discount).toBe(7);
|
||||
expect(result.serverCount).toBe(1);
|
||||
expect(result.metrics).toBeTruthy();
|
||||
expect(result.alias).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@ const root=process.cwd(), pub=path.join(root,'public');
|
||||
const read=p=>fs.readFileSync(path.join(pub,p),'utf8');
|
||||
const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
|
||||
let bad=0;const check=(v,m)=>{console.log(`${v?'OK':'FAIL'}: ${m}`);if(!v)bad++};
|
||||
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-v1772.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');
|
||||
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-v1773.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');
|
||||
const pkg=JSON.parse(readRoot('package.json'));
|
||||
const lock=JSON.parse(readRoot('package-lock.json'));
|
||||
const releaseManifest=JSON.parse(readRoot('docs/release-manifest.json'));
|
||||
@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent')
|
||||
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
|
||||
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
||||
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
||||
check(sw.includes('v77-20260909-v17-7-2-clients-foundation')&&sw.includes('data-layer-v1772.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.2');
|
||||
check(index.includes('20260909-v17-7-2-clients-foundation')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.2');
|
||||
check(sw.includes('v78-20260909-v17-7-3-clients-server-read')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
||||
check(index.includes('20260909-v17-7-3-clients-server-read')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
||||
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
|
||||
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
|
||||
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
||||
@ -39,17 +39,17 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30
|
||||
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
|
||||
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
|
||||
check(releaseManifest.channel==='production','release manifest channel is production');
|
||||
check(String(releaseManifest.pwaCache||'').includes('v77-20260909-v17-7-2-clients-foundation'),'release manifest points to current PWA cache');
|
||||
check(['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','17.7.2'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.2');
|
||||
check(String(releaseManifest.pwaCache||'').includes('v78-20260909-v17-7-3-clients-server-read'),'release manifest points to current PWA cache');
|
||||
check(['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','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
|
||||
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=12000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
|
||||
check(runtime.includes("const VERSION = '17.7.2'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
|
||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
|
||||
check(runtime.includes('refreshSupportWorkspace')&&runtime.includes('sun_dev_support_snapshot'),'cloud exposes lightweight read-only support refresh');
|
||||
check(runtime.includes('DEV_ADMIN_TTL_MS=30000')&&hotfix.includes('checkPlatformAdmin?.(false)')&&hotfix.includes('},10000);'),'developer access checks are throttled');
|
||||
check(performance.includes('pendingImageRoots')&&performance.includes('queueImageScan')&&ux.includes('},5000);'),'background DOM maintenance is batched/throttled');
|
||||
check(runtime.includes('explicitTemplate')&&runtime.includes("OFFER_TEMPLATE_IDS.has(explicitTemplate)"),'per-order proposal template survives render and PDF');
|
||||
check(runtime.includes("else if(id==='midnight-glass')")&&runtime.includes("else if(id==='emerald-gold')")&&runtime.includes("if(id==='editorial-grid')"),'existing proposal layout sequences remain available');
|
||||
check(runtime.includes('data-template-mini')&&ux.includes('data-template-mini'),'global and per-offer selectors show structural PDF previews');
|
||||
check(pkg.version==='17.7.2','package version is v17.7.2');
|
||||
check(pkg.version==='17.7.3','package version is v17.7.3');
|
||||
check(classic.includes("const VERSION='17.6.7'")&&classic.includes('CLASSIC_IDS')&&classic.includes('ARCHIVE_IDS'),'classic proposal PDF module is v17.6.7');
|
||||
check(runtime.includes("'warm-sun'")&&runtime.includes("'bento-cards'")&&runtime.includes("'event-story'")&&runtime.includes("'personal-letter'")&&runtime.includes("'event-ticket'")&&runtime.includes("'solar-experience'"),'10 classic proposal designs are available');
|
||||
check(runtime.includes("'midnight-compact'")&&runtime.includes("'black-gold'")&&runtime.includes("'neon-emerald'"),'hidden archive proposal templates are restored');
|
||||
@ -64,15 +64,17 @@ check(offerWorkspace.includes("const VERSION='17.6.9'")&&offerWorkspace.includes
|
||||
check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace.includes('Оформление PDF')&&offerWorkspace.includes('data-offer-gallery-slot'),'offer workspace separates preview/editor/templates and exposes two gallery uploads');
|
||||
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
|
||||
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
|
||||
check(pkg.version==='17.7.2','package version is v17.7.2');
|
||||
check(index.includes('20260909-v17-7-2-clients-foundation'),'index cache bust is v17.7.2');
|
||||
check(sw.includes('v77-20260909-v17-7-2-clients-foundation')&&sw.includes('data-layer-v1772.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.2 client foundation modules');
|
||||
check(fs.existsSync(path.join(root,'public/core/data-layer-v1772.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
||||
check(pkg.version==='17.7.3','package version is v17.7.3');
|
||||
check(index.includes('20260909-v17-7-3-clients-server-read'),'index cache bust is v17.7.3');
|
||||
check(sw.includes('v78-20260909-v17-7-3-clients-server-read')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
||||
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
||||
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
||||
check(runtime.includes("const VERSION = '17.7.2'")&&runtime.includes("v17.7.2 Clients Foundation"),'stability logger reports v17.7.2');
|
||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
||||
check(fs.existsSync(path.join(root,'ops/sql/SUPABASE-V17.7.0-SERVER-ORDER-AUTOMATION.sql'))&&fs.existsSync(path.join(root,'ops/sql/SUPABASE-V17.7.0-ERROR-TELEMETRY-HYGIENE.sql')),'v17.7.0 server migrations are versioned');
|
||||
|
||||
check(dataLayer.includes("const VERSION='17.7.2'")&&dataLayer.includes('catalogRepo')&&dataLayer.includes('clientsRepo')&&dataLayer.includes('CateriumDataV1771=api'),'v17.7.2 data layer exposes order/catalog/client repositories with compatibility aliases');
|
||||
check(dataLayer.includes("const VERSION='17.7.3'")&&dataLayer.includes('catalogRepo')&&dataLayer.includes('clientsRepo')&&dataLayer.includes('CateriumDataV1772=api'),'v17.7.3 data layer exposes order/catalog/client repositories with compatibility aliases');
|
||||
check(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');
|
||||
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');
|
||||
check(dataLayer.includes('sun_v17_clients_snapshot_v1773')&&dataLayer.includes('refreshServerClients')&&dataLayer.includes('compareClientSources'),'v17.7.3 client server snapshot and diagnostics are versioned');
|
||||
check(fs.existsSync(path.join(root,'ops/sql/SUPABASE-V17.7.3-CLIENTS-SERVER-READ.sql')),'v17.7.3 client server-read migration is versioned');
|
||||
if(bad)process.exit(1);
|
||||
|
||||
@ -6,7 +6,7 @@ const readPub=p=>fs.readFileSync(path.join(pub,p),'utf8');
|
||||
const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
|
||||
const fail=m=>{console.error('FAIL:',m);process.exitCode=1};
|
||||
const ok=m=>console.log('OK:',m);
|
||||
const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),dataLayer=readPub('core/data-layer-v1772.js'),serverAutomation=readPub('core/server-automation-v1770.js'),ops=readPub('core/ops-ux-v1762.js'),hotfix=readPub('core/hotfix-v1763.js'),ux=readPub('core/ux-fixes-v1764.js'),classic=readPub('core/classic-offer-pdf-v1767.js'),developerUX=readPub('core/developer-console-v1768.js'),offerWorkspace=readPub('core/offer-workspace-v1769.js');
|
||||
const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),dataLayer=readPub('core/data-layer-v1773.js'),serverAutomation=readPub('core/server-automation-v1770.js'),ops=readPub('core/ops-ux-v1762.js'),hotfix=readPub('core/hotfix-v1763.js'),ux=readPub('core/ux-fixes-v1764.js'),classic=readPub('core/classic-offer-pdf-v1767.js'),developerUX=readPub('core/developer-console-v1768.js'),offerWorkspace=readPub('core/offer-workspace-v1769.js');
|
||||
if(!html.includes('core/sun-safe.js'))fail('SunSafe must load before legacy modules');else ok('shared SunSafe loaded');
|
||||
if(html.includes('offer-gallery-data.js')||fs.existsSync(path.join(pub,'offer-gallery-data.js')))fail('blocking offer-gallery-data.js still present');else ok('base64 gallery removed');
|
||||
for(const raw of ['<span>${b.name}</span>','<span>${o.event}</span>','<span>${o.address||','value="${x[0]}"','value="${x[2]}"']) if(legacy.includes(raw)) fail(`legacy bootstrap contains raw HTML interpolation ${raw}`);
|
||||
@ -28,15 +28,15 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
|
||||
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||
if(!sw.includes('v17-7-2-clients-foundation')||!sw.includes('data-layer-v1772.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.7.0');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260909-v17-7-2-clients-foundation')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||
if(!sw.includes('v17-7-3-clients-server-read')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.7.0');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
||||
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
||||
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
||||
if(!performance.includes('MEMORY_REFRESH_MS=30000')||!performance.includes('MEMORY_TIMEOUT_MS=8000')||!performance.includes('memoryPromise'))fail('Developer Console bounded refresh controls missing');else ok('Developer Console bounded refresh controls present');
|
||||
if(!performance.includes('pendingImageRoots')||!performance.includes('queueImageScan'))fail('batched image mutation scanning missing');else ok('image mutation scanning is batched');
|
||||
if(!performance.includes('loadDataLayer')||!performance.includes('data-layer-v1772.js')||!performance.includes('loadServerAutomation'))fail('v17.7.1 data layer loaders missing');else ok('v17.7.1 data layer loaders present');
|
||||
if(!dataLayer.includes("const VERSION='17.7.2'")||!dataLayer.includes('ordersRepo')||!dataLayer.includes('catalogRepo')||!dataLayer.includes('clientsRepo')||!dataLayer.includes('sun_v17_save_client_v1772')||!dataLayer.includes('CateriumDataV1771=api'))fail('v17.7.2 data layer missing');else ok('v17.7.2 data layer present');
|
||||
if(!performance.includes('loadDataLayer')||!performance.includes('data-layer-v1773.js')||!performance.includes('loadServerAutomation'))fail('v17.7.1 data layer loaders missing');else ok('v17.7.1 data layer loaders 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');
|
||||
if(!serverAutomation.includes("const VERSION='17.7.0'")||!serverAutomation.includes("rpc('sun_run_order_automation'")||!serverAutomation.includes('applyServerChanges'))fail('v17.7.0 server automation client missing');else ok('v17.7.0 server automation client present');
|
||||
if(!performance.includes('ux-fixes-v1764.js')||!performance.includes('SunUXFixV1764'))fail('UX fix loader missing');else ok('UX fix loader present');
|
||||
if(!performance.includes('hotfix-v1763.js')||!performance.includes('SunHotfixV1763'))fail('v17.6.3 hotfix loader missing');else ok('v17.6.3 hotfix loader present');
|
||||
@ -44,7 +44,7 @@ if(!performance.includes('ops-ux-v1762.js')||!performance.includes('SunOpsUXV176
|
||||
for(const marker of ['patchDeveloperOpen','enhanceDeveloperGate','data-saas-admin','stopImmediatePropagation','instanceof HTMLElement']) if(!hotfix.includes(marker))fail(`developer/SaaS hotfix marker missing: ${marker}`);else ok(`developer/SaaS hotfix marker: ${marker}`);
|
||||
for(const marker of ['SUPPORT_POLL_MS=20000','supportReadPermission','refreshSupportWorkspace','sun-menu-editor-v1762','showCalendarDay',"ROUTE_BASE_KEY='sunRouteBaseV1'",'showRouteOrder','routeOpenYandex']) if(!ops.includes(marker)) fail(`ops UX marker missing: ${marker}`); else ok(`ops UX marker: ${marker}`);
|
||||
for(const marker of ["AUTO_DELAY_MS=60*1000","order.prepayment=total","order.status='Отдан заказчику'",'sunAutoCompletedAt','classificationDate','persistOfferTemplate','clientOfferTemplateId','offerTemplateId','sun-v1764-menu-icon','CateriumServerAutomationV1770?.enabled']) if(!ux.includes(marker))fail(`UX compatibility marker missing: ${marker}`);else ok(`UX compatibility marker: ${marker}`);
|
||||
for(const marker of ['CLOUD_RPC_TIMEOUT_MS=12000','CLOUD_CONFLICT_MAX_RETRIES=4','refreshSupportWorkspace',"const VERSION = '17.7.2'",'ERROR_DEDUPE_MS=5*60*1000','DEV_ADMIN_TTL_MS=30000']) if(!runtime.includes(marker))fail(`stability marker missing: ${marker}`);else ok(`stability marker: ${marker}`);
|
||||
for(const marker of ['CLOUD_RPC_TIMEOUT_MS=12000','CLOUD_CONFLICT_MAX_RETRIES=4','refreshSupportWorkspace',"const VERSION = '17.7.3'",'ERROR_DEDUPE_MS=5*60*1000','DEV_ADMIN_TTL_MS=30000']) if(!runtime.includes(marker))fail(`stability marker missing: ${marker}`);else ok(`stability marker: ${marker}`);
|
||||
if(!hotfix.includes('checkPlatformAdmin?.(false)')||!hotfix.includes('},10000);'))fail('Developer fallback polling is still aggressive');else ok('Developer fallback polling is throttled');
|
||||
if(!ux.includes('sunMenuIconV1766')||!ux.includes('sun-offer-template-mini-editorial-grid')||!runtime.includes('explicitTemplate'))fail('v17.6.6 proposal/menu markers missing');else ok('v17.6.6 proposal/menu markers present');
|
||||
if(!classic.includes("const VERSION='17.6.7'")||!classic.includes('CLASSIC_IDS')||!classic.includes('ARCHIVE_IDS')||!classic.includes('renderPages'))fail('v17.6.7 classic PDF module missing');else ok('v17.6.7 classic PDF module present');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user