Fix transient cloud sync timeouts and optimize trial photos
@ -11,7 +11,7 @@
|
|||||||
"serverReady": true,
|
"serverReady": true,
|
||||||
"workspaceAutoDiscovery": true,
|
"workspaceAutoDiscovery": true,
|
||||||
"invitesTemporarilyDisabled": false,
|
"invitesTemporarilyDisabled": false,
|
||||||
"pwaCache": "v92-20260918-proposal-quality",
|
"pwaCache": "v93-20260918-sync-demo-images",
|
||||||
"fullOfferDescriptions": true,
|
"fullOfferDescriptions": true,
|
||||||
"dynamicOfferRows": true,
|
"dynamicOfferRows": true,
|
||||||
"pdfOfferDescriptionFix": true,
|
"pdfOfferDescriptionFix": true,
|
||||||
@ -331,7 +331,7 @@
|
|||||||
"clientOfferTemplatePerOrder": true,
|
"clientOfferTemplatePerOrder": true,
|
||||||
"menuStyledSvgIcon": true,
|
"menuStyledSvgIcon": true,
|
||||||
"releaseIntegrityChecks": true,
|
"releaseIntegrityChecks": true,
|
||||||
"cloudRpcTimeoutMs": 12000,
|
"cloudRpcTimeoutMs": 45000,
|
||||||
"cloudConflictMaxRetries": 4,
|
"cloudConflictMaxRetries": 4,
|
||||||
"errorLogDedupMinutes": 5,
|
"errorLogDedupMinutes": 5,
|
||||||
"networkFailureBackoff": true,
|
"networkFailureBackoff": true,
|
||||||
@ -400,9 +400,17 @@
|
|||||||
"stockProducts": 43,
|
"stockProducts": 43,
|
||||||
"suppliers": 5,
|
"suppliers": 5,
|
||||||
"autoNewTrial": true,
|
"autoNewTrial": true,
|
||||||
"preservesSolnce": true
|
"preservesSolnce": true,
|
||||||
|
"imageFormat": "webp",
|
||||||
|
"imageMaxSidePx": 1024,
|
||||||
|
"imageBytes": 1343650,
|
||||||
|
"legacyImageLinksPreserved": true
|
||||||
},
|
},
|
||||||
"proposalLayout": "core/proposal-layout.js",
|
"proposalLayout": "core/proposal-layout.js",
|
||||||
"proposalLocalCyrillicFonts": true,
|
"proposalLocalCyrillicFonts": true,
|
||||||
"proposalRasterDpi": 290
|
"proposalRasterDpi": 290,
|
||||||
|
"cloudReadTimeoutMs": 12000,
|
||||||
|
"cloudFallbackTimeoutMs": 15000,
|
||||||
|
"cloudWriteTimeoutMs": 35000,
|
||||||
|
"cloudSafeNetworkRetries": 3
|
||||||
}
|
}
|
||||||
|
|||||||
30
docs/releases/2026-09-18-SYNC-DEMO-IMAGES.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Login, synchronization and trial image loading
|
||||||
|
|
||||||
|
Release cache: `v93-20260918-sync-demo-images`.
|
||||||
|
|
||||||
|
The previous transport aborted every request, including saves, after seven
|
||||||
|
seconds. A slow response could therefore surface as `AbortError: signal is
|
||||||
|
aborted without reason`. Reads had only four seconds on the fallback route.
|
||||||
|
|
||||||
|
- Reads and password login now allow 12 seconds through the proxy and 15 seconds
|
||||||
|
through the same Supabase project's direct route. A successful fallback is
|
||||||
|
preferred for 60 seconds, avoiding another wait on the failing route.
|
||||||
|
- Writes have a separate 35-second limit, longer than the proxy's 30-second
|
||||||
|
upstream limit. A write is never replayed by the transport. The outer cloud
|
||||||
|
operation deadline is 45 seconds so it cannot overtake a normal save.
|
||||||
|
- A transient sync failure retries after 2, 5 and 15 seconds, at most three
|
||||||
|
times. Each retry fetches the server revision and merges again, including when
|
||||||
|
a save committed but its response was lost. After exhaustion the UI shows a
|
||||||
|
readable connection error and keeps local changes.
|
||||||
|
- A late sync/pull response is ignored after account/workspace changes or local
|
||||||
|
sign-out. Caller cancellation is preserved and does not start a fallback.
|
||||||
|
- Ten trial photos now use 1024px WebP assets: **1,343,650 bytes instead of
|
||||||
|
22,930,358 bytes** (17.1 times smaller). The original images remain available
|
||||||
|
for rollback. Existing catalog/offer PNG references resolve to the optimized
|
||||||
|
files; customer photos and database records are not rewritten.
|
||||||
|
- The service worker caches same-origin app assets only, excluding API routes.
|
||||||
|
|
||||||
|
Validation covers delayed reads/saves, route fallback, cancellation, an uncertain
|
||||||
|
commit without a duplicate save, stale account responses, the actual SDK login,
|
||||||
|
all ten image dimensions and the 1.5 MB combined budget. Full application checks
|
||||||
|
also cover trial TTK/stock/procurement and all 21 proposal designs.
|
||||||
15
ops/demo/optimize-demo-images.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""Build lightweight display assets while retaining original demo PNGs for rollback."""
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
root = Path(__file__).resolve().parents[2] / "public" / "demo" / "images"
|
||||||
|
original_bytes = optimized_bytes = 0
|
||||||
|
for source in sorted(root.glob("*.png")):
|
||||||
|
with Image.open(source) as image:
|
||||||
|
image = image.convert("RGB")
|
||||||
|
image.thumbnail((1024, 1024), Image.Resampling.LANCZOS)
|
||||||
|
target = source.with_suffix(".webp")
|
||||||
|
image.save(target, "WEBP", quality=83, method=6)
|
||||||
|
original_bytes += source.stat().st_size
|
||||||
|
optimized_bytes += target.stat().st_size
|
||||||
|
print(f"Demo images: {original_bytes:,} -> {optimized_bytes:,} bytes")
|
||||||
@ -17,3 +17,8 @@
|
|||||||
ExpiresByType text/html "access plus 0 seconds"
|
ExpiresByType text/html "access plus 0 seconds"
|
||||||
ExpiresByType application/javascript "access plus 0 seconds"
|
ExpiresByType application/javascript "access plus 0 seconds"
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
# Compatibility for trial catalogs and saved documents with the original URLs.
|
||||||
|
RewriteRule ^demo/images/(berry-dessert|bruschetta-tomato|caprese|cheese-fruit|chicken-sandwich|meat-assortment|mushroom-tartlet|salmon-cream|turkey-wrap|vegetables-hummus)\.png$ demo/images/$1.webp [L]
|
||||||
|
</IfModule>
|
||||||
|
|||||||
@ -67,7 +67,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
}
|
}
|
||||||
function loadImage(src){
|
function loadImage(src){
|
||||||
return new Promise(resolve=>{
|
return new Promise(resolve=>{
|
||||||
const raw=imageSource(src),img=new Image();let done=false;
|
const raw=window.SunSafe.imageAssetSrc(imageSource(src)),img=new Image();let done=false;
|
||||||
const finish=value=>{if(done)return;done=true;clearTimeout(timer);resolve(value)};
|
const finish=value=>{if(done)return;done=true;clearTimeout(timer);resolve(value)};
|
||||||
try{const url=new URL(raw,document.baseURI||location.href);if(/^https?:$/i.test(url.protocol)&&url.origin!==location.origin)img.crossOrigin='anonymous'}catch(_){}
|
try{const url=new URL(raw,document.baseURI||location.href);if(/^https?:$/i.test(url.protocol)&&url.origin!==location.origin)img.crossOrigin='anonymous'}catch(_){}
|
||||||
img.onload=()=>finish(img);
|
img.onload=()=>finish(img);
|
||||||
@ -445,7 +445,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
renderCategoryTabs();ensureCatalogTools();const catalogSearch=$('sunCatalogSearch');if(catalogSearch){const boxMode=[0,5].includes(Number(activeCat));catalogSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';catalogSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#new .catalog h1');if(title)title.textContent=catName(activeCat);
|
renderCategoryTabs();ensureCatalogTools();const catalogSearch=$('sunCatalogSearch');if(catalogSearch){const boxMode=[0,5].includes(Number(activeCat));catalogSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';catalogSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#new .catalog h1');if(title)title.textContent=catName(activeCat);
|
||||||
const all=boxes.filter(item=>Number(item.category||0)===Number(activeCat)),items=all.filter(item=>itemMatches(item,catalogQuery));const cat=catById(activeCat)||{name:'Каталог',prep:true};
|
const all=boxes.filter(item=>Number(item.category||0)===Number(activeCat)),items=all.filter(item=>itemMatches(item,catalogQuery));const cat=catById(activeCat)||{name:'Каталог',prep:true};
|
||||||
if(Number(activeCat)===BANQUET_CATEGORY){$('tiles').innerHTML=renderBanquetView(items);window.CateriumBanquet.bind($('tiles'),{catalog:banquetCatalog(),draft,rerender:renderCatalogV5});renderOrderLines();updateOrderSummary();return;}
|
if(Number(activeCat)===BANQUET_CATEGORY){$('tiles').innerHTML=renderBanquetView(items);window.CateriumBanquet.bind($('tiles'),{catalog:banquetCatalog(),draft,rerender:renderCatalogV5});renderOrderLines();updateOrderSummary();return;}
|
||||||
const cards=items.map(item=>`<button class="tile" type="button" onclick="add('${esc(item.id)}')" title="Добавить в заказ">${item.photo?`<img src="${esc(item.photo)}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='${window.CateriumBranding.logoHTML()}'">`:("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}<span>${esc(item.name)}</span>${item.catalogSection?`<small class="tile-section">${esc(item.catalogSection)}</small>`:''}${(item.weight||inferBoxPieces(item))?`<small class="tile-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}<span class="tile-price-wrap"><small class="tile-price">${money(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${Number(item.oldPrice||0)>Number(item.price||0)?`<span class="sale-badge">АКЦИЯ</span>`:''}</button>`).join('');
|
const cards=items.map(item=>`<button class="tile" type="button" onclick="add('${esc(item.id)}')" title="Добавить в заказ">${item.photo?`<img src="${esc(window.SunSafe.imageAssetSrc(item.photo))}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='${window.CateriumBranding.logoHTML()}'">`:("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}<span>${esc(item.name)}</span>${item.catalogSection?`<small class="tile-section">${esc(item.catalogSection)}</small>`:''}${(item.weight||inferBoxPieces(item))?`<small class="tile-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}<span class="tile-price-wrap"><small class="tile-price">${money(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${Number(item.oldPrice||0)>Number(item.price||0)?`<span class="sale-badge">АКЦИЯ</span>`:''}</button>`).join('');
|
||||||
const addText=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');
|
const addText=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');
|
||||||
const empty=items.length?'':`<div class="catalog-empty">${catalogQuery?'По вашему запросу ничего не найдено.':`В разделе «${esc(cat.name)}» пока нет позиций.`}</div>`;
|
const empty=items.length?'':`<div class="catalog-empty">${catalogQuery?'По вашему запросу ничего не найдено.':`В разделе «${esc(cat.name)}» пока нет позиций.`}</div>`;
|
||||||
$('tiles').innerHTML=`<button class="tile add" type="button" onclick="editBox(null)">+<br>${addText}</button>${cards}${empty}`;
|
$('tiles').innerHTML=`<button class="tile add" type="button" onclick="editBox(null)">+<br>${addText}</button>${cards}${empty}`;
|
||||||
@ -462,7 +462,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
function renderManagerV5(){
|
function renderManagerV5(){
|
||||||
ensureManagerToolbar();const managerSearch=$('sunManagerSearch');if(managerSearch){const boxMode=[0,5].includes(Number(activeCat));managerSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';managerSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#manager .dialog-head h2'),add=document.querySelector('#manager .sun-manager-toolbar .primary')||document.querySelector('#manager .primary');if(title)title.textContent=catName(activeCat);if(add){add.textContent=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');add.onclick=()=>window.editBox(null)}
|
ensureManagerToolbar();const managerSearch=$('sunManagerSearch');if(managerSearch){const boxMode=[0,5].includes(Number(activeCat));managerSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';managerSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#manager .dialog-head h2'),add=document.querySelector('#manager .sun-manager-toolbar .primary')||document.querySelector('#manager .primary');if(title)title.textContent=catName(activeCat);if(add){add.textContent=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');add.onclick=()=>window.editBox(null)}
|
||||||
const items=boxes.filter(i=>Number(i.category||0)===Number(activeCat)&&itemMatches(i,managerQuery)),cat=catById(activeCat)||{prep:true};
|
const items=boxes.filter(i=>Number(i.category||0)===Number(activeCat)&&itemMatches(i,managerQuery)),cat=catById(activeCat)||{prep:true};
|
||||||
$('managerList').innerHTML=items.length?items.map(item=>`<button type="button" onclick="editBox('${esc(item.id)}')">${item.photo?`<img src="${esc(item.photo)}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='${window.CateriumBranding.logoHTML()}'">`:("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}<b>${esc(item.name)}</b><span class="manager-price-wrap"><small class="manager-card-price">${money(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${(item.weight||inferBoxPieces(item))?`<small class="manager-card-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}${cat.prep?`<small>${(item.ingredients||[]).length} позиций в составе</small>`:''}</button>`).join(''):'<p class="empty">Ничего не найдено.</p>';
|
$('managerList').innerHTML=items.length?items.map(item=>`<button type="button" onclick="editBox('${esc(item.id)}')">${item.photo?`<img src="${esc(window.SunSafe.imageAssetSrc(item.photo))}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='${window.CateriumBranding.logoHTML()}'">`:("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}<b>${esc(item.name)}</b><span class="manager-price-wrap"><small class="manager-card-price">${money(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${(item.weight||inferBoxPieces(item))?`<small class="manager-card-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}${cat.prep?`<small>${(item.ingredients||[]).length} позиций в составе</small>`:''}</button>`).join(''):'<p class="empty">Ничего не найдено.</p>';
|
||||||
}
|
}
|
||||||
window.openManager=()=>{managerQuery='';ensureManagerToolbar();if($('sunManagerSearch'))$('sunManagerSearch').value='';renderManagerV5();window.modal?.('manager');};
|
window.openManager=()=>{managerQuery='';ensureManagerToolbar();if($('sunManagerSearch'))$('sunManagerSearch').value='';renderManagerV5();window.modal?.('manager');};
|
||||||
window.editBox=id=>{
|
window.editBox=id=>{
|
||||||
@ -1088,7 +1088,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const isSafeRasterDataUrl=src=>/^data:image\/(?!svg\+xml)[a-z0-9.+-]+;/i.test(String(src||''));
|
const isSafeRasterDataUrl=src=>/^data:image\/(?!svg\+xml)[a-z0-9.+-]+;/i.test(String(src||''));
|
||||||
async function asDataUrlOne(src){
|
async function asDataUrlOne(src){
|
||||||
if(!src)return'';
|
if(!src)return'';
|
||||||
const raw=String(src).trim();
|
const raw=window.SunSafe.imageAssetSrc(src);
|
||||||
if(isSafeRasterDataUrl(raw))return raw;
|
if(isSafeRasterDataUrl(raw))return raw;
|
||||||
if(/^data:/i.test(raw))return'';
|
if(/^data:/i.test(raw))return'';
|
||||||
try{
|
try{
|
||||||
@ -2116,7 +2116,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const DEFAULT_SUPABASE_URL = 'https://usfjwhztqoopzzfmfbis.supabase.co';
|
const DEFAULT_SUPABASE_URL = 'https://usfjwhztqoopzzfmfbis.supabase.co';
|
||||||
const DEFAULT_SUPABASE_KEY = 'sb_publishable_CAxfhMKrduJjuk_5ybCQLg_TqSGWGoy';
|
const DEFAULT_SUPABASE_KEY = 'sb_publishable_CAxfhMKrduJjuk_5ybCQLg_TqSGWGoy';
|
||||||
const SUPABASE_API_PROXY = 'https://api.caterium.ru';
|
const SUPABASE_API_PROXY = 'https://api.caterium.ru';
|
||||||
const PROXY_FETCH_TIMEOUT_MS = 7000;
|
const PROXY_FETCH_TIMEOUT_MS = 12000;
|
||||||
const supabaseProxyFetch=window.CateriumCloudTransport.create({upstream:DEFAULT_SUPABASE_URL,proxy:SUPABASE_API_PROXY,timeout:PROXY_FETCH_TIMEOUT_MS});
|
const supabaseProxyFetch=window.CateriumCloudTransport.create({upstream:DEFAULT_SUPABASE_URL,proxy:SUPABASE_API_PROXY,timeout:PROXY_FETCH_TIMEOUT_MS});
|
||||||
|
|
||||||
let config = loadConfig();
|
let config = loadConfig();
|
||||||
@ -2132,9 +2132,10 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
let membershipLoad = null;
|
let membershipLoad = null;
|
||||||
let realtimeChannel = null;
|
let realtimeChannel = null;
|
||||||
let syncTimer = null;
|
let syncTimer = null;
|
||||||
|
let networkRetryTimer = null;
|
||||||
let isSyncing = false;
|
let isSyncing = false;
|
||||||
let dirty = false;
|
let dirty = false;
|
||||||
const CLOUD_RPC_TIMEOUT_MS=12000;
|
const CLOUD_RPC_TIMEOUT_MS=45000;
|
||||||
const CLOUD_CONFLICT_MAX_RETRIES=4;
|
const CLOUD_CONFLICT_MAX_RETRIES=4;
|
||||||
function errText(e,fallback='Неизвестная ошибка'){
|
function errText(e,fallback='Неизвестная ошибка'){
|
||||||
if(e==null)return fallback;
|
if(e==null)return fallback;
|
||||||
@ -2456,7 +2457,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
if(signOutInProgress)return;
|
if(signOutInProgress)return;
|
||||||
signOutInProgress=true;
|
signOutInProgress=true;
|
||||||
setSignOutUiBusy(true);
|
setSignOutUiBusy(true);
|
||||||
clearTimeout(syncTimer);
|
clearTimeout(syncTimer);clearTimeout(networkRetryTimer);
|
||||||
setStatus('syncing','Выходим из аккаунта…');
|
setStatus('syncing','Выходим из аккаунта…');
|
||||||
try{
|
try{
|
||||||
// Preserve the company working copy first. Do not block logout on a cloud
|
// Preserve the company working copy first. Do not block logout on a cloud
|
||||||
@ -2757,7 +2758,8 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyPayload(payload, replace=false) {
|
async function applyPayload(payload, replace=false, current=()=>true) {
|
||||||
|
if(!current())throw new DOMException('Компания сменилась.','AbortError');
|
||||||
if (!payload?.storage || typeof payload.storage !== 'object') throw new Error('Некорректный формат облачной базы.');
|
if (!payload?.storage || typeof payload.storage !== 'object') throw new Error('Некорректный формат облачной базы.');
|
||||||
suppressStorageTracking = true;
|
suppressStorageTracking = true;
|
||||||
try {
|
try {
|
||||||
@ -2769,6 +2771,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
if (!shouldSyncKey(key) || !entry) continue;
|
if (!shouldSyncKey(key) || !entry) continue;
|
||||||
let decoded = await decodeMedia(entry.v);
|
let decoded = await decodeMedia(entry.v);
|
||||||
if (key === 'sunBoxes' && entry.t === 'j') decoded = await compactSunBoxesForStorage(decoded,{maxSide:900,maxChars:130000,quality:.78});
|
if (key === 'sunBoxes' && entry.t === 'j') decoded = await compactSunBoxesForStorage(decoded,{maxSide:900,maxChars:130000,quality:.78});
|
||||||
|
if(!current())throw new DOMException('Компания сменилась.','AbortError');
|
||||||
let raw = entry.t === 'j' ? JSON.stringify(decoded) : String(decoded ?? '');
|
let raw = entry.t === 'j' ? JSON.stringify(decoded) : String(decoded ?? '');
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, raw);
|
localStorage.setItem(key, raw);
|
||||||
@ -2849,16 +2852,21 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pullRemote({replace=false,quiet=false}={}) {
|
async function pullRemote({replace=false,quiet=false}={}) {
|
||||||
if (!client || !workspace?.id) return;
|
if (signOutInProgress || !client || !session || !workspace?.id) return;
|
||||||
|
const startedUser=session.user.id,startedWorkspace=workspace.id;
|
||||||
|
const current=()=>!signOutInProgress&&session?.user?.id===startedUser&&workspace?.id===startedWorkspace;
|
||||||
setStatus('syncing','Загружаю облачные данные…');
|
setStatus('syncing','Загружаю облачные данные…');
|
||||||
try {
|
try {
|
||||||
const row = await fetchRemoteRow();
|
const row = await fetchRemoteRow();
|
||||||
|
if(!current())return;
|
||||||
if (!row || payloadEmpty(row.payload)) {
|
if (!row || payloadEmpty(row.payload)) {
|
||||||
setStatus('ready','В облаке пока нет данных.'); return;
|
setStatus('ready','В облаке пока нет данных.'); return;
|
||||||
}
|
}
|
||||||
if (!quiet) await backupBeforeCloud('Перед загрузкой данных из Supabase');
|
if (!quiet) await backupBeforeCloud('Перед загрузкой данных из Supabase');
|
||||||
await applyPayload(row.payload, replace);
|
await applyPayload(row.payload, replace, current);
|
||||||
|
if(!current())return;
|
||||||
await setBaseline(row);
|
await setBaseline(row);
|
||||||
|
if(!current())return;
|
||||||
config.migrated[workspace.id] = true;
|
config.migrated[workspace.id] = true;
|
||||||
config.lastSync = row.updated_at || new Date().toISOString();
|
config.lastSync = row.updated_at || new Date().toISOString();
|
||||||
saveConfig(); dirty=false;
|
saveConfig(); dirty=false;
|
||||||
@ -2866,26 +2874,32 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
if (!quiet) toast('Данные загружены из облака.','success');
|
if (!quiet) toast('Данные загружены из облака.','success');
|
||||||
try{window.dispatchEvent(new CustomEvent('sun:cloud-sync-complete',{detail:{row:clone(row),payload:clone(row.payload),kind:'pull'}}));}catch(_){}
|
try{window.dispatchEvent(new CustomEvent('sun:cloud-sync-complete',{detail:{row:clone(row),payload:clone(row.payload),kind:'pull'}}));}catch(_){}
|
||||||
renderCloudUI();
|
renderCloudUI();
|
||||||
} catch (error) { handleError(error,'Не удалось загрузить облачные данные.'); }
|
} catch (error) { if(current())handleError(error,'Не удалось загрузить облачные данные.'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncNow({quiet=false,retryCount=0}={}) {
|
async function syncNow({quiet=false,retryCount=0,networkRetry=0}={}) {
|
||||||
if (isSyncing || !client || !session || !workspace?.id || !workspaceMigrated()) return;
|
if (signOutInProgress || isSyncing || !client || !session || !workspace?.id || !workspaceMigrated()) return;
|
||||||
if (!navigator.onLine) { setStatus('offline','Офлайн — изменения ждут подключения.'); return; }
|
if (!navigator.onLine) { setStatus('offline','Офлайн — изменения ждут подключения.'); return; }
|
||||||
|
clearTimeout(networkRetryTimer);
|
||||||
|
const startedUser=session.user.id,startedWorkspace=workspace.id;
|
||||||
|
const current=()=>!signOutInProgress&&session?.user?.id===startedUser&&workspace?.id===startedWorkspace;
|
||||||
isSyncing = true;
|
isSyncing = true;
|
||||||
setStatus('syncing','Синхронизация…');
|
setStatus('syncing','Синхронизация…');
|
||||||
try {
|
try {
|
||||||
const [baseline, remote, localPayload] = await Promise.all([getBaseline(), fetchRemoteRow(), collectLocalPayload()]);
|
const [baseline, remote, localPayload] = await Promise.all([getBaseline(), fetchRemoteRow(), collectLocalPayload()]);
|
||||||
|
if(!current())return;
|
||||||
if (!remote || payloadEmpty(remote.payload)) {
|
if (!remote || payloadEmpty(remote.payload)) {
|
||||||
if (!canWrite()) throw new Error('В облаке нет базы, а у пользователя нет прав на её создание.');
|
if (!canWrite()) throw new Error('В облаке нет базы, а у пользователя нет прав на её создание.');
|
||||||
const row = await upsertPayload(localPayload, remote?.revision ?? null);
|
const row = await upsertPayload(localPayload, remote?.revision ?? null);
|
||||||
await setBaseline(row); dirty=false; config.lastSync=row.updated_at||new Date().toISOString();saveConfig();
|
if(!current())return;
|
||||||
|
await setBaseline(row); if(!current())return; dirty=false; config.lastSync=row.updated_at||new Date().toISOString();saveConfig();
|
||||||
setStatus('ready','Синхронизировано.'); if(!quiet)toast('Синхронизация завершена.','success'); return;
|
setStatus('ready','Синхронизировано.'); if(!quiet)toast('Синхронизация завершена.','success'); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!baseline) {
|
if (!baseline) {
|
||||||
await applyPayload(remote.payload, true);
|
await applyPayload(remote.payload, true, current);
|
||||||
await setBaseline(remote); dirty=false; config.lastSync=remote.updated_at||new Date().toISOString();saveConfig();
|
if(!current())return;
|
||||||
|
await setBaseline(remote); if(!current())return; dirty=false; config.lastSync=remote.updated_at||new Date().toISOString();saveConfig();
|
||||||
setStatus('ready','Подключена существующая облачная база.'); if(!quiet)toast('Загружена актуальная облачная база.','success'); return;
|
setStatus('ready','Подключена существующая облачная база.'); if(!quiet)toast('Загружена актуальная облачная база.','success'); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2896,17 +2910,19 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
|
|
||||||
if (changedVsRemote) {
|
if (changedVsRemote) {
|
||||||
if (!canWrite()) {
|
if (!canWrite()) {
|
||||||
await applyPayload(remote.payload, true);
|
await applyPayload(remote.payload, true, current);
|
||||||
finalRow = remote;
|
finalRow = remote;
|
||||||
} else {
|
} else {
|
||||||
finalRow = await upsertPayload(merged.payload, remote.revision);
|
finalRow = await upsertPayload(merged.payload, remote.revision);
|
||||||
if (changedVsLocal) await applyPayload(merged.payload, true);
|
if (changedVsLocal) await applyPayload(merged.payload, true, current);
|
||||||
}
|
}
|
||||||
} else if (changedVsLocal) {
|
} else if (changedVsLocal) {
|
||||||
await applyPayload(remote.payload, true);
|
await applyPayload(remote.payload, true, current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!current())return;
|
||||||
await setBaseline(finalRow);
|
await setBaseline(finalRow);
|
||||||
|
if(!current())return;
|
||||||
dirty=false;
|
dirty=false;
|
||||||
config.lastSync=finalRow.updated_at||new Date().toISOString();saveConfig();
|
config.lastSync=finalRow.updated_at||new Date().toISOString();saveConfig();
|
||||||
setStatus('ready', merged.conflicts.length ? `Синхронизировано, объединено конфликтов: ${merged.conflicts.length}.` : 'Синхронизировано.');
|
setStatus('ready', merged.conflicts.length ? `Синхронизировано, объединено конфликтов: ${merged.conflicts.length}.` : 'Синхронизировано.');
|
||||||
@ -2916,12 +2932,19 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
window.SunStabilityV17?.markSynced?.(finalRow?.revision);
|
window.SunStabilityV17?.markSynced?.(finalRow?.revision);
|
||||||
renderCloudUI();
|
renderCloudUI();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if(!current())return;
|
||||||
const msg=String(error?.message||'');
|
const msg=String(error?.message||'');
|
||||||
if(msg.includes('SUN_CONFLICT')){
|
if(msg.includes('SUN_CONFLICT')){
|
||||||
const nextRetry=Number(retryCount||0)+1;
|
const nextRetry=Number(retryCount||0)+1;
|
||||||
try{window.SunStabilityV17?.recordConflict?.(msg);}catch(_){}
|
try{window.SunStabilityV17?.recordConflict?.(msg);}catch(_){}
|
||||||
if(nextRetry>CLOUD_CONFLICT_MAX_RETRIES){handleError(error,'Не удалось синхронизировать после нескольких безопасных повторов.');}
|
if(nextRetry>CLOUD_CONFLICT_MAX_RETRIES){handleError(error,'Не удалось синхронизировать после нескольких безопасных повторов.');}
|
||||||
else{const delay=Math.min(5000,400*Math.pow(2,nextRetry-1));setStatus('pending','Облачная версия изменилась. Повтор '+nextRetry+'/'+CLOUD_CONFLICT_MAX_RETRIES+' через '+(Math.round(delay/100)/10)+' сек.…');setTimeout(()=>syncNow({quiet:true,retryCount:nextRetry}),delay);}
|
else{const delay=Math.min(5000,400*Math.pow(2,nextRetry-1));setStatus('pending','Облачная версия изменилась. Повтор '+nextRetry+'/'+CLOUD_CONFLICT_MAX_RETRIES+' через '+(Math.round(delay/100)/10)+' сек.…');setTimeout(()=>syncNow({quiet:true,retryCount:nextRetry}),delay);}
|
||||||
|
}else if(window.CateriumCloudTransport.isTransient(error)&&networkRetry<3){
|
||||||
|
const delay=[2000,5000,15000][networkRetry];
|
||||||
|
setStatus('pending','Соединение прервалось. Изменения сохранены на устройстве; повторяю синхронизацию…');
|
||||||
|
// Re-read the remote revision and merge before any write. A timed-out
|
||||||
|
// save may already have committed; never blindly resend its request.
|
||||||
|
networkRetryTimer=setTimeout(()=>{if(current())syncNow({quiet:true,networkRetry:networkRetry+1})},delay);
|
||||||
}else handleError(error,'Ошибка синхронизации.');
|
}else handleError(error,'Ошибка синхронизации.');
|
||||||
}
|
}
|
||||||
finally { isSyncing=false; }
|
finally { isSyncing=false; }
|
||||||
@ -3189,7 +3212,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
return false;
|
return false;
|
||||||
}catch(error){
|
}catch(error){
|
||||||
if(!current())return false;
|
if(!current())return false;
|
||||||
membershipError=error?.message||'Не удалось загрузить рабочую базу.';
|
membershipError=window.CateriumCloudTransport.errorMessage(error);
|
||||||
setStatus('error',membershipError);
|
setStatus('error',membershipError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -3275,7 +3298,8 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleError(error, message) {
|
function handleError(error, message) {
|
||||||
console.error('[SunCloudV2]',error);const detail=String(error?.message||error||'Неизвестная ошибка');setStatus('error',`${message} ${detail}`);toast(`${message} ${detail}`,'error',7500);renderCloudUI();
|
if(signOutInProgress)return;
|
||||||
|
console.error('[SunCloudV2]',error);const detail=window.CateriumCloudTransport.errorMessage(error);setStatus('error',`${message} ${detail}`);toast(`${message} ${detail}`,'error',7500);renderCloudUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
function observeSettings() {
|
function observeSettings() {
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
const persisted=await c.auth.getSession();if(persisted.error)throw persisted.error;if(!persisted.data?.session?.user)throw new Error('Сессия входа не сохранилась. Повторите вход.');
|
const persisted=await c.auth.getSession();if(persisted.error)throw persisted.error;if(!persisted.data?.session?.user)throw new Error('Сессия входа не сохранилась. Повторите вход.');
|
||||||
setError(gate,'Вход выполнен. Открываю Caterium…');
|
setError(gate,'Вход выполнен. Открываю Caterium…');
|
||||||
setTimeout(()=>location.reload(),120);
|
setTimeout(()=>location.reload(),120);
|
||||||
}catch(error){setError(gate,String(error?.message||error||'Не удалось войти.'));if(button)button.disabled=false;busy=false;}
|
}catch(error){setError(gate,window.CateriumCloudTransport?.errorMessage(error)||String(error?.message||error||'Не удалось войти.'));if(button)button.disabled=false;busy=false;}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publicSignup(gate){
|
async function publicSignup(gate){
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
(()=>{
|
(()=>{
|
||||||
'use strict';
|
'use strict';
|
||||||
const READ_RPCS=new Set(['sun_my_workspaces','sun_fetch_app_state','sun_is_platform_admin','caterium_trial_demo_status']);
|
const READ_RPCS=new Set(['sun_my_workspaces','sun_fetch_app_state','sun_is_platform_admin','caterium_trial_demo_status']);
|
||||||
function create({upstream,proxy,timeout=7000,fallbackTimeout=4000}){
|
const isTransient=error=>/TimeoutError|AbortError|CATERIUM_TIMEOUT|Failed to fetch|fetch failed|NetworkError|Load failed|network request failed|превышено время ожидания/i.test(String(error?.message||error||''));
|
||||||
|
const errorMessage=error=>isTransient(error)?'Сервер временно не отвечает. Изменения остаются на этом устройстве. Проверьте соединение и повторите загрузку.':String(error?.message||error||'Неизвестная ошибка');
|
||||||
|
function create({upstream,proxy,timeout=12000,fallbackTimeout=15000,writeTimeout=35000,cooldown=60000}){
|
||||||
const origin=new URL(upstream).origin;
|
const origin=new URL(upstream).origin;
|
||||||
|
let directUntil=0;
|
||||||
return async function(input,init={}){
|
return async function(input,init={}){
|
||||||
const original=new Request(input,init),url=new URL(original.url),isBackend=url.origin===origin;
|
const original=new Request(input,init),url=new URL(original.url),isBackend=url.origin===origin;
|
||||||
const read=original.method==='GET'||original.method==='HEAD'||(original.method==='POST'&&url.pathname.startsWith('/rest/v1/rpc/')&&READ_RPCS.has(url.pathname.slice('/rest/v1/rpc/'.length)));
|
const read=original.method==='GET'||original.method==='HEAD'||(original.method==='POST'&&url.pathname.startsWith('/rest/v1/rpc/')&&READ_RPCS.has(url.pathname.slice('/rest/v1/rpc/'.length)));
|
||||||
@ -12,8 +15,9 @@
|
|||||||
async function attempt(target,limit){
|
async function attempt(target,limit){
|
||||||
const controller=new AbortController(),abort=()=>controller.abort(original.signal.reason);
|
const controller=new AbortController(),abort=()=>controller.abort(original.signal.reason);
|
||||||
if(original.signal.aborted)abort();else original.signal.addEventListener('abort',abort,{once:true});
|
if(original.signal.aborted)abort();else original.signal.addEventListener('abort',abort,{once:true});
|
||||||
const timer=setTimeout(()=>controller.abort(),limit);
|
const timer=setTimeout(()=>controller.abort(new DOMException('Сервер не ответил вовремя. Проверьте соединение и повторите загрузку.','TimeoutError')),limit);
|
||||||
try{
|
try{
|
||||||
|
if(controller.signal.aborted)throw controller.signal.reason;
|
||||||
const response=await fetch(new Request(target,original.clone()),{signal:controller.signal});
|
const response=await fetch(new Request(target,original.clone()),{signal:controller.signal});
|
||||||
if(expectJson&&response.ok&&response.status!==204){
|
if(expectJson&&response.ok&&response.status!==204){
|
||||||
const text=await response.clone().text();
|
const text=await response.clone().text();
|
||||||
@ -23,13 +27,18 @@
|
|||||||
return response;
|
return response;
|
||||||
}finally{clearTimeout(timer);original.signal.removeEventListener('abort',abort)}
|
}finally{clearTimeout(timer);original.signal.removeEventListener('abort',abort)}
|
||||||
}
|
}
|
||||||
|
const directFirst=isBackend&&Date.now()<directUntil;
|
||||||
|
const first=directFirst?original.url:isBackend?proxy+url.pathname+url.search:original.url;
|
||||||
|
const second=directFirst?proxy+url.pathname+url.search:original.url;
|
||||||
try{
|
try{
|
||||||
const response=await attempt(isBackend?proxy+url.pathname+url.search:original.url,timeout);
|
const response=await attempt(first,safeFallback?(directFirst?fallbackTimeout:timeout):writeTimeout);
|
||||||
if(!safeFallback||response.status<500)return response;
|
if(!safeFallback||response.status<500)return response;
|
||||||
}catch(error){if(!safeFallback||original.signal.aborted)throw error}
|
}catch(error){if(!safeFallback||original.signal.aborted)throw error}
|
||||||
// Same backend, same authorization, one SDK session. Never replay writes.
|
// Same backend, same authorization, one SDK session. Never replay writes.
|
||||||
return attempt(original.url,fallbackTimeout);
|
const response=await attempt(second,directFirst?timeout:fallbackTimeout);
|
||||||
|
if(response.ok)directUntil=directFirst?0:Date.now()+cooldown;
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
window.CateriumCloudTransport=Object.freeze({create});
|
window.CateriumCloudTransport=Object.freeze({create,isTransient,errorMessage});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
(()=>{
|
(()=>{
|
||||||
'use strict';
|
'use strict';
|
||||||
const VERSION='17.7.3';
|
const VERSION='17.7.3';
|
||||||
const RELEASE='20260918-proposal-quality';
|
const RELEASE='20260918-sync-demo-images';
|
||||||
|
|
||||||
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
||||||
function installAuthBoot(){
|
function installAuthBoot(){
|
||||||
|
|||||||
@ -5,8 +5,17 @@
|
|||||||
})[ch]);
|
})[ch]);
|
||||||
const escapeAttr=escapeHTML;
|
const escapeAttr=escapeHTML;
|
||||||
const idToken=value=>String(value??'').replace(/[^a-zA-Z0-9_-]/g,'');
|
const idToken=value=>String(value??'').replace(/[^a-zA-Z0-9_-]/g,'');
|
||||||
|
// Existing trial databases and saved offers still reference the PNG originals.
|
||||||
|
// Resolve only our ten bundled demo assets; never rewrite customer photos.
|
||||||
|
const demoNames='berry-dessert|bruschetta-tomato|caprese|cheese-fruit|chicken-sandwich|meat-assortment|mushroom-tartlet|salmon-cream|turkey-wrap|vegetables-hummus';
|
||||||
|
const demoPath=new RegExp('^/(demo/images/(?:'+demoNames+'))\\.png$');
|
||||||
|
const imageAssetSrc=value=>{
|
||||||
|
const src=String(value??'').trim();
|
||||||
|
try{const url=new URL(src,document.baseURI);const match=url.pathname.match(demoPath);if(url.origin===location.origin&&match)return match[1]+'.webp'+url.search+url.hash;}catch(_){}
|
||||||
|
return src;
|
||||||
|
};
|
||||||
const safeImageSrc=value=>{
|
const safeImageSrc=value=>{
|
||||||
const s=String(value??'').trim();
|
const s=imageAssetSrc(value);
|
||||||
if(!s)return '';
|
if(!s)return '';
|
||||||
if(/^data:image\/(?:png|jpe?g|webp|gif);base64,[a-z0-9+/=\s]+$/i.test(s))return s;
|
if(/^data:image\/(?:png|jpe?g|webp|gif);base64,[a-z0-9+/=\s]+$/i.test(s))return s;
|
||||||
if(/^(?:\.\/|\.\.\/|\/)?[a-z0-9_./-]+\.(?:png|jpe?g|webp|gif)(?:[?#][^\s]*)?$/i.test(s))return s;
|
if(/^(?:\.\/|\.\.\/|\/)?[a-z0-9_./-]+\.(?:png|jpe?g|webp|gif)(?:[?#][^\s]*)?$/i.test(s))return s;
|
||||||
@ -19,7 +28,7 @@
|
|||||||
if(reference&&reference.parentNode===parent)parent.insertBefore(node,reference);else parent.appendChild(node);
|
if(reference&&reference.parentNode===parent)parent.insertBefore(node,reference);else parent.appendChild(node);
|
||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
window.SunSafe=Object.freeze({escapeHTML,escapeAttr,idToken,safeImageSrc,setText,insertBefore});
|
window.SunSafe=Object.freeze({escapeHTML,escapeAttr,idToken,safeImageSrc,imageAssetSrc,setText,insertBefore});
|
||||||
|
|
||||||
// Small bootstrap for account/profile UI. Keeping it here makes the account
|
// Small bootstrap for account/profile UI. Keeping it here makes the account
|
||||||
// center available on every Caterium screen without touching the legacy monolith.
|
// center available on every Caterium screen without touching the legacy monolith.
|
||||||
@ -30,4 +39,4 @@
|
|||||||
script.async=true;
|
script.async=true;
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
BIN
public/demo/images/berry-dessert.webp
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
public/demo/images/bruschetta-tomato.webp
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
public/demo/images/caprese.webp
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/demo/images/cheese-fruit.webp
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
public/demo/images/chicken-sandwich.webp
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
public/demo/images/meat-assortment.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
public/demo/images/mushroom-tartlet.webp
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
public/demo/images/salmon-cream.webp
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
public/demo/images/turkey-wrap.webp
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
public/demo/images/vegetables-hummus.webp
Normal file
|
After Width: | Height: | Size: 97 KiB |
@ -1,5 +1,5 @@
|
|||||||
const CACHE='sun-catering-pwa-v92-20260918-proposal-quality';
|
const CACHE='sun-catering-pwa-v93-20260918-sync-demo-images';
|
||||||
const VERSION='20260918-proposal-quality';
|
const VERSION='20260918-sync-demo-images';
|
||||||
const CORE=[
|
const CORE=[
|
||||||
'./','./index.html',`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
'./','./index.html',`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
||||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.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}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.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/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.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}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||||
@ -30,7 +30,7 @@ function forceFresh(req){
|
|||||||
}
|
}
|
||||||
self.addEventListener('fetch',event=>{
|
self.addEventListener('fetch',event=>{
|
||||||
const req=event.request;if(req.method!=='GET')return;
|
const req=event.request;if(req.method!=='GET')return;
|
||||||
const url=new URL(req.url);if(url.pathname.startsWith('/api/'))return;
|
const url=new URL(req.url);if(url.origin!==self.location.origin||url.pathname.startsWith('/api/'))return;
|
||||||
if(req.mode==='navigate'){
|
if(req.mode==='navigate'){
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
fetch(req,{cache:'no-store'})
|
fetch(req,{cache:'no-store'})
|
||||||
|
|||||||
@ -1,13 +1,74 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import {test,expect} from '@playwright/test';
|
import {test,expect} from '@playwright/test';
|
||||||
|
|
||||||
|
test('slow connections use a healthy route, allow longer saves and preserve caller cancellation',async({page})=>{
|
||||||
|
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<html><body></body></html>'}));
|
||||||
|
await page.goto('/index.html');await page.addScriptTag({url:'/core/cloud-transport.js'});
|
||||||
|
const result=await page.evaluate(async()=>{
|
||||||
|
const upstream='https://backend.example.invalid',proxy='https://proxy.example.invalid',calls=[];
|
||||||
|
const ok=()=>new Response('{}',{headers:{'content-type':'application/json'}});
|
||||||
|
window.fetch=(request,{signal})=>{calls.push(request.url);return request.url.startsWith(proxy)?new Promise((_,reject)=>signal.addEventListener('abort',()=>reject(signal.reason),{once:true})):Promise.resolve(ok())};
|
||||||
|
const send=CateriumCloudTransport.create({upstream,proxy,timeout:10,fallbackTimeout:100,writeTimeout:100});
|
||||||
|
await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',body:'{}'});
|
||||||
|
await send(upstream+'/rest/v1/rpc/sun_fetch_app_state',{method:'POST',body:'{}'});
|
||||||
|
const routes=calls.splice(0);
|
||||||
|
window.fetch=(request,{signal})=>{calls.push(request.url);return new Promise((resolve,reject)=>{const timer=setTimeout(()=>resolve(ok()),35);signal.addEventListener('abort',()=>{clearTimeout(timer);reject(signal.reason)},{once:true})})};
|
||||||
|
const slowSave=CateriumCloudTransport.create({upstream,proxy,timeout:10,writeTimeout:100});
|
||||||
|
const saved=(await slowSave(upstream+'/rest/v1/rpc/sun_save_app_state_v17',{method:'POST',body:'{}'})).status;const saveCalls=calls.splice(0);
|
||||||
|
const timeoutSend=CateriumCloudTransport.create({upstream,proxy,writeTimeout:5});let timeoutName='';
|
||||||
|
try{await timeoutSend(upstream+'/rest/v1/rpc/sun_save_app_state_v17',{method:'POST',body:'{}'})}catch(e){timeoutName=e.name}const timeoutCalls=calls.splice(0);
|
||||||
|
const controller=new AbortController();controller.abort(new DOMException('Account changed','AbortError'));let cancel='';
|
||||||
|
try{await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',body:'{}',signal:controller.signal})}catch(e){cancel=e.message}
|
||||||
|
return {routes,saved,saveCalls,timeoutName,timeoutCalls,cancel,cancelCalls:calls};
|
||||||
|
});
|
||||||
|
expect(result.routes.map(u=>new URL(u).host)).toEqual(['proxy.example.invalid','backend.example.invalid','backend.example.invalid']);
|
||||||
|
expect(result.saved).toBe(200);expect(result.saveCalls).toHaveLength(1);expect(result.timeoutName).toBe('TimeoutError');expect(result.timeoutCalls).toHaveLength(1);
|
||||||
|
expect(result.cancel).toBe('Account changed');expect(result.cancelCalls).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function syncHarness(page){
|
||||||
|
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<html><body></body></html>'}));await page.goto('/index.html');
|
||||||
|
await page.addScriptTag({url:'/core/sun-safe.js'});await page.addScriptTag({url:'/core/cloud-transport.js'});
|
||||||
|
const runtime=fs.readFileSync('public/app-runtime.js','utf8');let source=runtime.slice(runtime.indexOf('/* ===== MODULE: cloud-sync-v2.js'),runtime.indexOf('/* ===== MODULE: admin-rbac-v3.js'));
|
||||||
|
source=source.replace('async function boot(){','async function boot(){return;').replace('window.SunCloudV2={',`window.SunCloudV2={testInit:c=>{client=c;session={user:{id:'test-user'}};workspace={id:'company',role:'admin'};config.migrated.company=true;config.tenantStorageReady=true;config.localWorkspaceId='company';config.workspaceId='company'},testBaseline:setBaseline,testLeave:()=>{session=null;workspace=null},`);
|
||||||
|
await page.addScriptTag({content:'var orders=[],boxes=[];'+source});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('an uncertain save is recovered by reading its committed revision without a duplicate write',async({page})=>{
|
||||||
|
await syncHarness(page);await page.clock.install();
|
||||||
|
await page.evaluate(async()=>{
|
||||||
|
window.calls=[];window.toasts=[];window.SunEnterprise={toast:message=>window.toasts.push(message)};
|
||||||
|
const payload={format:'sun-cloud-v2',version:2,storage:{sunOrders:{t:'j',v:[]}}};let row={payload,revision:1};
|
||||||
|
localStorage.setItem('sunOrders',JSON.stringify([{id:'test-order',total:2200}]));
|
||||||
|
SunCloudV2.testInit({rpc:async(name,args)=>{calls.push(name);if(name==='sun_fetch_app_state')return {data:[structuredClone(row)]};if(name==='sun_save_app_state_v17'){row={payload:structuredClone(args.p_payload),revision:2};return {error:{message:'TimeoutError: server response lost'}}}throw new Error(name)}});
|
||||||
|
await SunCloudV2.testBaseline({payload,revision:1});await SunCloudV2.syncNow();
|
||||||
|
});
|
||||||
|
expect(await page.evaluate(()=>SunCloudV2.status().lastStatus)).toBe('pending');
|
||||||
|
await page.clock.fastForward(2100);
|
||||||
|
await expect.poll(()=>page.evaluate(()=>SunCloudV2.status().lastStatus)).toBe('ready');
|
||||||
|
const result=await page.evaluate(()=>({calls,toasts,orders:JSON.parse(localStorage.sunOrders)}));
|
||||||
|
expect(result.calls.filter(x=>x==='sun_fetch_app_state')).toHaveLength(2);expect(result.calls.filter(x=>x==='sun_save_app_state_v17')).toHaveLength(1);
|
||||||
|
expect(result.toasts).toEqual([]);expect(result.orders).toEqual([{id:'test-order',total:2200}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a late sync response cannot apply the previous account data after sign-out',async({page})=>{
|
||||||
|
await syncHarness(page);
|
||||||
|
const result=await page.evaluate(async()=>{
|
||||||
|
window.toasts=[];window.SunEnterprise={toast:message=>toasts.push(message)};let resolve;
|
||||||
|
SunCloudV2.testInit({rpc:()=>new Promise(r=>resolve=r)});
|
||||||
|
const pending=SunCloudV2.syncNow();SunCloudV2.testLeave();resolve({data:[{revision:2,payload:{storage:{sunOrders:{t:'j',v:[{id:'previous-user'}]}}}}]});await pending;
|
||||||
|
return {orders:localStorage.getItem('sunOrders'),toasts,workspace:SunCloudV2.getWorkspace()};
|
||||||
|
});
|
||||||
|
expect(result.orders).toBeNull();expect(result.toasts).toEqual([]);expect(result.workspace).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
test('cloud reads and password login recover from empty proxy responses without replaying writes',async({page})=>{
|
test('cloud reads and password login recover from empty proxy responses without replaying writes',async({page})=>{
|
||||||
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><body></body></html>'}));
|
await page.route('**/index.html',r=>r.fulfill({contentType:'text/html',body:'<!doctype html><html><body></body></html>'}));
|
||||||
await page.goto('/index.html');await page.addScriptTag({url:'/core/cloud-transport.js'});
|
await page.goto('/index.html');await page.addScriptTag({url:'/core/cloud-transport.js'});
|
||||||
const result=await page.evaluate(async()=>{
|
const result=await page.evaluate(async()=>{
|
||||||
const calls=[],upstream='https://backend.example.invalid',proxy='https://proxy.example.invalid';let scenario='read';
|
const calls=[],upstream='https://backend.example.invalid',proxy='https://proxy.example.invalid';let scenario='read';
|
||||||
window.fetch=async request=>{calls.push({url:request.url,body:await request.text(),auth:request.headers.get('authorization')});if(scenario==='denied')return new Response('{"error":"denied"}',{status:401});if(scenario==='write')return new Response('',{status:503});return request.url.startsWith(proxy)?new Response('',{headers:{'content-type':'text/html'}}):new Response('[{"id":"company"}]',{headers:{'content-type':'application/json'}})};
|
window.fetch=async request=>{calls.push({url:request.url,body:await request.text(),auth:request.headers.get('authorization')});if(scenario==='denied')return new Response('{"error":"denied"}',{status:401});if(scenario==='write')return new Response('',{status:503});return request.url.startsWith(proxy)?new Response('',{headers:{'content-type':'text/html'}}):new Response('[{"id":"company"}]',{headers:{'content-type':'application/json'}})};
|
||||||
const send=window.CateriumCloudTransport.create({upstream,proxy});
|
const send=window.CateriumCloudTransport.create({upstream,proxy,cooldown:0});
|
||||||
const read=await (await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',headers:{Authorization:'Bearer test-token'},body:'{}'})).json();
|
const read=await (await send(upstream+'/rest/v1/rpc/sun_my_workspaces',{method:'POST',headers:{Authorization:'Bearer test-token'},body:'{}'})).json();
|
||||||
const readCalls=calls.splice(0);
|
const readCalls=calls.splice(0);
|
||||||
await send(upstream+'/auth/v1/token?grant_type=password',{method:'POST',body:'{"email":"test@example.invalid","password":"test"}'});const authCalls=calls.splice(0);
|
await send(upstream+'/auth/v1/token?grant_type=password',{method:'POST',body:'{"email":"test@example.invalid","password":"test"}'});const authCalls=calls.splice(0);
|
||||||
|
|||||||
@ -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((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(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([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
||||||
check(sw.includes('v92-20260918-proposal-quality')&&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(sw.includes('v93-20260918-sync-demo-images')&&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('20260918-proposal-quality')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
check(index.includes('20260918-sync-demo-images')&&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('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("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');
|
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
||||||
@ -39,9 +39,9 @@ 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(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.version===`v${pkg.version}`,'release manifest version matches package.json');
|
||||||
check(releaseManifest.channel==='production','release manifest channel is production');
|
check(releaseManifest.channel==='production','release manifest channel is production');
|
||||||
check(String(releaseManifest.pwaCache||'').includes('v92-20260918-proposal-quality'),'release manifest points to current PWA cache');
|
check(String(releaseManifest.pwaCache||'').includes('v93-20260918-sync-demo-images'),'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(['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('CLOUD_RPC_TIMEOUT_MS=45000')&&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.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("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('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(runtime.includes('DEV_ADMIN_TTL_MS=30000')&&hotfix.includes('checkPlatformAdmin?.(false)')&&hotfix.includes('},10000);'),'developer access checks are throttled');
|
||||||
@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace
|
|||||||
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
|
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(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
|
||||||
check(pkg.version==='17.7.3','package version is v17.7.3');
|
check(pkg.version==='17.7.3','package version is v17.7.3');
|
||||||
check(index.includes('20260918-proposal-quality'),'index cache bust is v17.7.3');
|
check(index.includes('20260918-sync-demo-images'),'index cache bust is v17.7.3');
|
||||||
check(sw.includes('v92-20260918-proposal-quality')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
check(sw.includes('v93-20260918-sync-demo-images')&&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(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(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
||||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
||||||
|
|||||||
@ -28,8 +28,8 @@ 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');
|
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));
|
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(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||||
if(!sw.includes('20260918-proposal-quality')||!sw.includes('login-signature-v1776.js')||!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 for login refresh');
|
if(!sw.includes('20260918-sync-demo-images')||!sw.includes('login-signature-v1776.js')||!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 for login refresh');
|
||||||
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-proposal-quality')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-sync-demo-images')||!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('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("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('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');
|
||||||
@ -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 ['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 ['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 ["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.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}`);
|
for(const marker of ['CLOUD_RPC_TIMEOUT_MS=45000','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(!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(!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');
|
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');
|
||||||
|
|||||||
@ -40,7 +40,11 @@ test('demo TTK, order, shortages, receipt and one-time write-off work in the ful
|
|||||||
});
|
});
|
||||||
test('demo photo assets load and company changes remove the trial guide',async({page})=>{
|
test('demo photo assets load and company changes remove the trial guide',async({page})=>{
|
||||||
await ready(page);
|
await ready(page);
|
||||||
expect(await page.evaluate(async d=>{const images=await Promise.all(d.boxes.map(b=>new Promise(resolve=>{const i=new Image();i.onload=()=>resolve(i.naturalWidth>500);i.onerror=()=>resolve(false);i.src=b.photo})));return images.every(Boolean)},demo)).toBe(true);
|
expect(await page.evaluate(async d=>{const images=await Promise.all(d.boxes.map(b=>new Promise(resolve=>{const i=new Image();i.onload=()=>resolve(i.naturalWidth===1024);i.onerror=()=>resolve(false);i.src=SunSafe.imageAssetSrc(b.photo)})));return images.every(Boolean)},demo)).toBe(true);
|
||||||
|
const sources=await page.locator('#tiles img[src*="demo/images/"]').evaluateAll(images=>images.map(i=>i.getAttribute('src')));
|
||||||
|
expect(sources).toHaveLength(10);expect(sources.every(s=>s.endsWith('.webp'))).toBe(true);
|
||||||
|
const bytes=demo.boxes.reduce((sum,b)=>sum+fs.statSync('public/'+b.photo.replace('.png','.webp')).size,0);expect(bytes).toBeLessThan(1500000);
|
||||||
|
expect(await page.evaluate(()=>['catalog/001.jpg','data:image/png;base64,AAAA','https://other.example/demo/images/caprese.png','demo/images/custom.png'].map(SunSafe.imageAssetSrc))).toEqual(['catalog/001.jpg','data:image/png;base64,AAAA','https://other.example/demo/images/caprese.png','demo/images/custom.png']);
|
||||||
await page.evaluate(()=>{window.dispatchEvent(new Event('sun:cloud-tenant-changing'));window.SunCloudV2.getSession=()=>null;localStorage.removeItem('sunTrialDemoV1');window.CateriumTrialDemo.render()});
|
await page.evaluate(()=>{window.dispatchEvent(new Event('sun:cloud-tenant-changing'));window.SunCloudV2.getSession=()=>null;localStorage.removeItem('sunTrialDemoV1');window.CateriumTrialDemo.render()});
|
||||||
await expect(page.locator('#ctTrialDemo')).toHaveCount(0);
|
await expect(page.locator('#ctTrialDemo')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|||||||