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:
parent
fd5b4bf413
commit
672a8e2237
3
.github/workflows/qa.yml
vendored
3
.github/workflows/qa.yml
vendored
@ -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
|
||||||
|
|||||||
26
docs/releases/V17.6.1-CHANGES.txt
Normal file
26
docs/releases/V17.6.1-CHANGES.txt
Normal 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.
|
||||||
45
ops/sql/SUPABASE-V17.6.1-CHAT-STORAGE-GUARD.sql
Normal file
45
ops/sql/SUPABASE-V17.6.1-CHAT-STORAGE-GUARD.sql
Normal 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;
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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();
|
||||||
})();
|
})();
|
||||||
@ -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}`,
|
||||||
|
|||||||
@ -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}));
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user