Caterium v17.6.1 — chat photo compression and server memory

Release v17.6.1 after green GitHub QA and Cloudflare preview checks.
This commit is contained in:
pavlov346346-source 2026-09-07 18:11:05 +03:00 committed by GitHub
parent fd5b4bf413
commit 672a8e2237
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 171 additions and 13 deletions

View File

@ -2,6 +2,9 @@ name: Caterium QA
on: on:
push: push:
pull_request: pull_request:
concurrency:
group: caterium-qa-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
qa: qa:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -0,0 +1,26 @@
Caterium v17.6.1 — Chat photo compression + server memory counters
Date: 2026-09-07
1. Photos selected in chat are optimized in the browser before upload.
- Maximum side: 2048 px.
- Target size: about 2 MB.
- Absolute attachment limit remains 15 MB.
- Non-image files are unchanged; GIF/SVG are not recompressed.
- Browsers without DataTransfer fall back to the original chat upload path.
2. Developer Console now receives server-side memory metrics:
- PostgreSQL database size.
- Supabase Storage size.
- Storage object count.
- Total server footprint (database + Storage).
- Per-bucket Storage breakdown is also returned by the RPC.
3. Supabase migration: SUPABASE-V17.6.1-CHAT-STORAGE-GUARD.sql.
4. GitHub QA initialization race fixed:
- Playwright waits for SunSafe and SunPdfEngine before invoking them.
- Added an automated large-photo compression test.
5. PWA cache bumped to v66 / v17.6.1-chat-storage-guard.
Release flow: feature/chat-storage-guard-v1761 -> GitHub QA + Cloudflare preview -> main -> Cloudflare production.

View File

@ -0,0 +1,45 @@
-- Caterium v17.6.1: server memory counters for Developer Console
create or replace function public.sun_platform_dashboard()
returns jsonb
language plpgsql
stable
security definer
set search_path='public','auth'
as $$
declare
v_companies bigint; v_accounts bigint; v_admins bigint; v_active bigint; v_trial bigint;
v_locked bigint; v_errors bigint; v_backups bigint; v_members bigint;
v_last_state timestamptz; v_last_backup timestamptz;
v_database_bytes bigint := 0; v_storage_bytes bigint := 0; v_storage_objects bigint := 0;
v_storage_buckets jsonb := '{}'::jsonb;
begin
perform public.sun_require_platform_admin_aal2();
select count(*) into v_companies from public.sun_workspaces;
select count(*) into v_accounts from auth.users;
select count(*) into v_admins from public.sun_platform_admins;
select count(*) into v_members from public.sun_workspace_members where is_active=true;
select count(*) into v_active from public.sun_workspace_subscriptions where status='active' and coalesce(current_period_end,'infinity'::timestamptz)>now();
select count(*) into v_trial from public.sun_workspace_subscriptions where status='trialing' and coalesce(trial_ends_at,'infinity'::timestamptz)>now();
select count(*) into v_locked from public.sun_workspaces w where public.sun_subscription_access_mode(w.id) in ('read_only','blocked');
select count(*) into v_errors from public.sun_v17_error_events where created_at>now()-interval '24 hours';
select count(*) into v_backups from public.sun_v17_backups where created_at>now()-interval '24 hours';
select max(updated_at) into v_last_state from public.sun_app_state;
select max(created_at) into v_last_backup from public.sun_v17_backups;
v_database_bytes := pg_database_size(current_database());
select count(*), coalesce(sum(case when coalesce(metadata->>'size','') ~ '^[0-9]+$' then (metadata->>'size')::bigint else 0 end),0)
into v_storage_objects, v_storage_bytes from storage.objects;
select coalesce(jsonb_object_agg(bucket_id,jsonb_build_object('objects',object_count,'bytes',bucket_bytes,'size',pg_size_pretty(bucket_bytes))),'{}'::jsonb)
into v_storage_buckets
from (select bucket_id,count(*)::bigint as object_count,coalesce(sum(case when coalesce(metadata->>'size','') ~ '^[0-9]+$' then (metadata->>'size')::bigint else 0 end),0)::bigint as bucket_bytes from storage.objects group by bucket_id) s;
return jsonb_build_object(
'companies',v_companies,'accounts',v_accounts,'platform_admins',v_admins,'memberships',v_members,
'active_subscriptions',v_active,'trials',v_trial,'restricted_companies',v_locked,
'errors_24h',v_errors,'backups_24h',v_backups,'last_state_at',v_last_state,'last_backup_at',v_last_backup,
'database_bytes',v_database_bytes,'database_size',pg_size_pretty(v_database_bytes),
'storage_bytes',v_storage_bytes,'storage_size',pg_size_pretty(v_storage_bytes),'storage_objects',v_storage_objects,'storage_buckets',v_storage_buckets,
'server_bytes',v_database_bytes+v_storage_bytes,'server_size',pg_size_pretty(v_database_bytes+v_storage_bytes),
'postgres_version',current_setting('server_version'));
end;
$$;
revoke all on function public.sun_platform_dashboard() from public, anon;
grant execute on function public.sun_platform_dashboard() to authenticated;

View File

@ -1,7 +1,7 @@
{ {
"name": "caterium-app", "name": "caterium-app",
"private": true, "private": true,
"version": "17.6.0", "version": "17.6.1",
"type": "module", "type": "module",
"scripts": { "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/pdf-engine.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/pdf-engine.js",

View File

@ -11,11 +11,73 @@
if(root instanceof HTMLImageElement)tune(root); if(root instanceof HTMLImageElement)tune(root);
root?.querySelectorAll?.('img').forEach(tune); root?.querySelectorAll?.('img').forEach(tune);
}; };
const MAX_FILE=15*1024*1024,TARGET=2*1024*1024,MAX_SIDE=2048;
const guardedInputs=new WeakSet();
const toast=(text,type='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(text,type,ms)}catch(_){}};
const jpegName=name=>String(name||'photo').replace(/\.[^.]+$/,'')+'.jpg';
const canvasBlob=(canvas,type,quality)=>new Promise((resolve,reject)=>canvas.toBlob(b=>b?resolve(b):reject(new Error('Не удалось сжать изображение.')),type,quality));
async function decodeImage(file){
if('createImageBitmap' in window){const bitmap=await createImageBitmap(file);return {source:bitmap,width:bitmap.width,height:bitmap.height,close:()=>bitmap.close?.()};}
const url=URL.createObjectURL(file);try{const img=new Image();await new Promise((resolve,reject)=>{img.onload=resolve;img.onerror=reject;img.src=url});return {source:img,width:img.naturalWidth,height:img.naturalHeight,close:()=>URL.revokeObjectURL(url)};}catch(e){URL.revokeObjectURL(url);throw e;}
}
async function compressImage(file){
if(!(file instanceof File)||!String(file.type||'').startsWith('image/')||/gif|svg/i.test(file.type||''))return file;
let decoded;try{decoded=await decodeImage(file);}catch(_){return file;}
try{
const {width,height,source}=decoded;if(!width||!height)return file;
if(file.size<=TARGET&&Math.max(width,height)<=MAX_SIDE)return file;
let scale=Math.min(1,MAX_SIDE/Math.max(width,height)),quality=.84,best=null;
for(let pass=0;pass<5;pass++){
const w=Math.max(1,Math.round(width*scale)),h=Math.max(1,Math.round(height*scale));
const canvas=document.createElement('canvas');canvas.width=w;canvas.height=h;
const ctx=canvas.getContext('2d',{alpha:false});if(!ctx)return file;ctx.fillStyle='#fff';ctx.fillRect(0,0,w,h);ctx.drawImage(source,0,0,w,h);
const blob=await canvasBlob(canvas,'image/jpeg',quality);if(!best||blob.size<best.size)best=blob;if(blob.size<=TARGET)break;
quality=Math.max(.60,quality-.08);scale*=.86;
}
if(!best||best.size>=file.size)return file;
return new File([best],jpegName(file.name),{type:'image/jpeg',lastModified:file.lastModified||Date.now()});
}finally{decoded.close?.();}
}
async function prepareAttachment(file){const prepared=await compressImage(file);if(prepared.size>MAX_FILE)throw new Error(`Файл «${file.name}» больше 15 МБ даже после оптимизации.`);return {file:prepared,originalSize:file.size,savedBytes:Math.max(0,file.size-prepared.size)};}
async function guardChatFiles(input){
const raw=[...(input.files||[])];if(!raw.length)return;
const prepared=[];let saved=0;
for(const f of raw){try{const p=await prepareAttachment(f);prepared.push(p.file);saved+=p.savedBytes;}catch(e){toast(e.message||String(e),'warn',6000);}}
if(!prepared.length){input.value='';return;}
const dt=new DataTransfer();prepared.forEach(f=>dt.items.add(f));input.files=dt.files;
guardedInputs.add(input);input.dispatchEvent(new Event('change',{bubbles:true}));guardedInputs.delete(input);
if(saved>256*1024)toast(`Фото оптимизированы: сэкономлено ${(saved/1024/1024).toFixed(1)} МБ.`,'success',4200);
}
document.addEventListener('change',e=>{
const input=e.target;if(!(input instanceof HTMLInputElement)||input.id!=='sunChatFilesV29'||guardedInputs.has(input)||typeof DataTransfer==='undefined')return;
e.preventDefault();e.stopImmediatePropagation();guardChatFiles(input).catch(err=>{console.error('[Caterium photo compression]',err);guardedInputs.add(input);input.dispatchEvent(new Event('change',{bubbles:true}));guardedInputs.delete(input);});
},true);
window.SunAttachmentGuard={VERSION:'17.6.1',MAX_FILE,TARGET,MAX_SIDE,compressImage,prepareAttachment};
let memoryData=null,memoryAt=0,memoryLoading=false;
const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'');
async function loadMemory(){
const now=Date.now();if(memoryLoading)return memoryData;if(memoryData&&now-memoryAt<5000)return memoryData;
const c=window.SunCloudV2?.getClient?.();if(!c)return null;memoryLoading=true;
try{const r=await c.rpc('sun_dev_dashboard');if(r.error)throw r.error;memoryData=r.data||null;memoryAt=Date.now();return memoryData;}catch(_){return null}finally{memoryLoading=false;}
}
async function enhanceDeveloperMemory(){
const view=document.getElementById('sun-developer-console-v22');if(!view?.classList.contains('on'))return;
const body=document.getElementById('sunDevBody');if(!body)return;
const version=document.getElementById('sunDevReleaseVersion');if(version)version.textContent='17.6.1';
const d=await loadMemory();if(!d||!view.classList.contains('on'))return;
let box=document.getElementById('sunDevMemoryV1761');if(!box){box=document.createElement('div');box.id='sunDevMemoryV1761';box.className='sun-dev-grid';box.style.marginBottom='12px';body.prepend(box);}
box.innerHTML=`<div class="sun-dev-kpi"><small>Память сервера</small><b>${esc(d.server_size||d.database_size||'—')}</b></div><div class="sun-dev-kpi"><small>База PostgreSQL</small><b>${esc(d.database_size||'—')}</b></div><div class="sun-dev-kpi"><small>Файлы Storage</small><b>${esc(d.storage_size||'—')}</b></div><div class="sun-dev-kpi"><small>Объектов Storage</small><b>${Number(d.storage_objects||0)}</b></div>`;
}
const start=()=>{ const start=()=>{
scan(document); scan(document);
const mo=new MutationObserver(records=>records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}))); const mo=new MutationObserver(records=>{records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory();});
mo.observe(document.documentElement,{childList:true,subtree:true}); mo.observe(document.documentElement,{childList:true,subtree:true});
window.SunPerformance={scanImages:()=>scan(document),disconnect:()=>mo.disconnect()}; document.addEventListener('click',e=>{if(e.target.closest('#sunDeveloperNavV22,[data-dev-tab],#sunDevRefresh'))setTimeout(enhanceDeveloperMemory,100)},true);
window.addEventListener('sun:cloud-state-applied',()=>setTimeout(enhanceDeveloperMemory,150));
setInterval(()=>{if(document.getElementById('sun-developer-console-v22')?.classList.contains('on'))enhanceDeveloperMemory();},5000);
window.SunPerformance={scanImages:()=>scan(document),refreshDeveloperMemory:enhanceDeveloperMemory,disconnect:()=>mo.disconnect()};
}; };
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start(); if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start();
})(); })();

View File

@ -1,5 +1,5 @@
const CACHE='sun-catering-pwa-v65-20260907-v17-6-0-stability-security'; const CACHE='sun-catering-pwa-v66-20260907-v17-6-1-chat-storage-guard';
const VERSION='20260907-v17-6-0-stability-security'; const VERSION='20260907-v17-6-1-chat-storage-guard';
const CORE=[ const CORE=[
'./','./index.html', './','./index.html',
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/pdf-engine.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/pdf-engine.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,

View File

@ -1,4 +1,12 @@
import fs from 'node:fs';
import path from 'node:path';
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
async function loadModule(page,file,globalName){
await page.goto('/index.html', { waitUntil:'domcontentloaded' });
const content=fs.readFileSync(path.join(process.cwd(),'public','core',file),'utf8');
await page.addScriptTag({content});
expect(await page.evaluate(name=>Boolean(window[name]),globalName)).toBeTruthy();
}
test('legacy localStorage payload cannot execute XSS', async ({ page }) => { test('legacy localStorage payload cannot execute XSS', async ({ page }) => {
const pageErrors=[]; page.on('pageerror',e=>pageErrors.push(String(e))); const pageErrors=[]; page.on('pageerror',e=>pageErrors.push(String(e)));
await page.addInitScript(() => { await page.addInitScript(() => {
@ -10,15 +18,25 @@ test('legacy localStorage payload cannot execute XSS', async ({ page }) => {
expect(pageErrors.filter(x=>!x.includes('supabase'))).toEqual([]); expect(pageErrors.filter(x=>!x.includes('supabase'))).toEqual([]);
}); });
test('safe insert helper handles foreign reference node', async ({ page }) => { test('safe insert helper handles foreign reference node', async ({ page }) => {
await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await loadModule(page,'sun-safe.js','SunSafe');
const result=await page.evaluate(()=>{const a=document.createElement('div'),b=document.createElement('div'),n=document.createElement('span'),foreign=document.createElement('i');a.appendChild(foreign);document.body.append(a,b);try{window.SunSafe.insertBefore(b,n,foreign);return {ok:true,parent:n.parentNode===b}}catch(e){return {ok:false,error:String(e)}}}); const result=await page.evaluate(()=>{const a=document.createElement('div'),b=document.createElement('div'),n=document.createElement('span'),foreign=document.createElement('i');a.appendChild(foreign);document.body.append(a,b);try{window.SunSafe.insertBefore(b,n,foreign);return {ok:true,parent:n.parentNode===b}}catch(e){return {ok:false,error:String(e)}}});
expect(result).toEqual({ok:true,parent:true}); expect(result).toEqual({ok:true,parent:true});
}); });
test('shared PDF engine produces A4 PDF blob', async ({ page }) => { test('shared PDF engine produces A4 PDF blob', async ({ page }) => {
await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await loadModule(page,'pdf-engine.js','SunPdfEngine');
const result=await page.evaluate(async()=>{const fake=new Uint8Array([255,216,255,217]);const blob=window.SunPdfEngine.fromJpegs([{width:1000,height:1414,bytes:fake}]);const head=new TextDecoder().decode(new Uint8Array(await blob.arrayBuffer()).slice(0,8));return {type:blob.type,size:blob.size,head,w:window.SunPdfEngine.PAGE_W,h:window.SunPdfEngine.PAGE_H}}); const result=await page.evaluate(async()=>{const fake=new Uint8Array([255,216,255,217]);const blob=window.SunPdfEngine.fromJpegs([{width:1000,height:1414,bytes:fake}]);const head=new TextDecoder().decode(new Uint8Array(await blob.arrayBuffer()).slice(0,8));return {type:blob.type,size:blob.size,head,w:window.SunPdfEngine.PAGE_W,h:window.SunPdfEngine.PAGE_H}});
expect(result.type).toBe('application/pdf'); expect(result.size).toBeGreaterThan(150); expect(result.head.startsWith('%PDF-1.4')).toBeTruthy(); expect(result.w/result.h).toBeCloseTo(1/Math.sqrt(2),3); expect(result.type).toBe('application/pdf'); expect(result.size).toBeGreaterThan(150); expect(result.head.startsWith('%PDF-1.4')).toBeTruthy(); expect(result.w/result.h).toBeCloseTo(1/Math.sqrt(2),3);
}); });
test('chat photo guard compresses a large camera image', async ({ page }) => {
await loadModule(page,'performance.js','SunAttachmentGuard');
const result=await page.evaluate(async()=>{
const c=document.createElement('canvas');c.width=3000;c.height=2200;const x=c.getContext('2d');
const g=x.createLinearGradient(0,0,c.width,c.height);g.addColorStop(0,'#172a3a');g.addColorStop(.5,'#d79a4a');g.addColorStop(1,'#f1e3ca');x.fillStyle=g;x.fillRect(0,0,c.width,c.height);
for(let i=0;i<1200;i++){x.fillStyle=`rgba(${i%255},${(i*7)%255},${(i*13)%255},.55)`;x.fillRect((i*31)%3000,(i*47)%2200,90,55)}
const blob=await new Promise(r=>c.toBlob(r,'image/jpeg',1));const input=new File([blob],'camera-photo.jpg',{type:'image/jpeg'});const out=await window.SunAttachmentGuard.prepareAttachment(input);const bm=await createImageBitmap(out.file);const data={original:input.size,size:out.file.size,width:bm.width,height:bm.height,type:out.file.type,name:out.file.name};bm.close();return data;
});
expect(Math.max(result.width,result.height)).toBeLessThanOrEqual(2048);expect(result.size).toBeLessThan(result.original);expect(result.size).toBeLessThanOrEqual(15*1024*1024);expect(result.type).toBe('image/jpeg');expect(result.name.endsWith('.jpg')).toBeTruthy();
});
test('mobile body does not overflow viewport', async ({ page }, testInfo) => { 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); test.skip(testInfo.project.name!=='mobile-390'); await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await page.waitForTimeout(500);
const dims=await page.evaluate(()=>({innerWidth,scrollWidth:document.documentElement.scrollWidth,bodyWidth:document.body.scrollWidth})); const dims=await page.evaluate(()=>({innerWidth,scrollWidth:document.documentElement.scrollWidth,bodyWidth:document.body.scrollWidth}));

View File

@ -3,14 +3,16 @@ import path from 'node:path';
const root=process.cwd(), pub=path.join(root,'public'); const root=process.cwd(), pub=path.join(root,'public');
const read=p=>fs.readFileSync(path.join(pub,p),'utf8'); const read=p=>fs.readFileSync(path.join(pub,p),'utf8');
let bad=0;const check=(v,m)=>{console.log(`${v?'OK':'FAIL'}: ${m}`);if(!v)bad++}; 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'); 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');
check(index.includes('core/stability-v1760.css'),'mobile stability stylesheet loaded'); check(index.includes('core/stability-v1760.css'),'mobile stability stylesheet loaded');
check(css.includes('overflow-x:hidden')&&css.includes('.cats'),'mobile overflow guard present'); check(css.includes('overflow-x:hidden')&&css.includes('.cats'),'mobile overflow guard present');
check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent'); 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('v17-6-0-stability-security'),'service worker cache is v17.6.0'); check(sw.includes('v17-6-1-chat-storage-guard'),'service worker cache is v17.6.1');
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(fs.existsSync(path.join(root,'supabase/functions/caterium-create-employee/index.ts')),'employee Edge Function source is versioned'); check(fs.existsSync(path.join(root,'supabase/functions/caterium-create-employee/index.ts')),'employee Edge Function source is versioned');
check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30,}/i.test(index+runtime),'no client secret-like token'); check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30,}/i.test(index+runtime+performance),'no client secret-like token');
if(bad)process.exit(1); if(bad)process.exit(1);

View File

@ -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 readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
const fail=m=>{console.error('FAIL:',m);process.exitCode=1}; const fail=m=>{console.error('FAIL:',m);process.exitCode=1};
const ok=m=>console.log('OK:',m); 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'); 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');
if(!html.includes('core/sun-safe.js'))fail('SunSafe must load before legacy modules');else ok('shared SunSafe loaded'); 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'); 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}`); 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}`);
@ -27,5 +27,7 @@ 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('v17-6-0-stability-security')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated'); if(!sw.includes('v17-6-1-chat-storage-guard')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated');
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(process.exitCode)process.exit(process.exitCode); if(process.exitCode)process.exit(process.exitCode);