From 5b2926c36bed450b0148f12a813e2b88734b87b3 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 7 Sep 2026 18:53:23 +0300 Subject: [PATCH] =?UTF-8?q?Caterium=20v17.6.2=20=E2=80=94=20operations=20U?= =?UTF-8?q?X?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add read-only developer support refresh, calendar overflow panel, full-page menu editor, configurable route start, and in-route order popup. --- package.json | 4 +- public/core/ops-ux-v1762.js | 259 ++++++++++++++++++++++++++++++++++++ public/core/performance.js | 14 +- public/service-worker.js | 6 +- tests/app.spec.mjs | 28 ++++ tests/release-check.mjs | 12 +- tests/static-security.mjs | 7 +- 7 files changed, 315 insertions(+), 15 deletions(-) create mode 100644 public/core/ops-ux-v1762.js diff --git a/package.json b/package.json index 43a0205..520c5e1 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "caterium-app", "private": true, - "version": "17.6.1", + "version": "17.6.2", "type": "module", "scripts": { - "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/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/ops-ux-v1762.js && node --check public/core/pdf-engine.js", "test:static": "node tests/static-security.mjs", "check:release": "node tests/release-check.mjs", "check:deploy": "npm run check:syntax && npm run test:static && npm run check:release", diff --git a/public/core/ops-ux-v1762.js b/public/core/ops-ux-v1762.js new file mode 100644 index 0000000..7b2b076 --- /dev/null +++ b/public/core/ops-ux-v1762.js @@ -0,0 +1,259 @@ +(()=>{ + 'use strict'; + if(window.SunOpsUXV1762)return; + + const VERSION='17.6.2'; + const RELEASE='20260907-v17-6-2-ops-ux'; + const SUPPORT_POLL_MS=12000; + const ROUTE_BASE_KEY='sunRouteBaseV1'; + const ROUTE_OVERRIDE_KEY='sunRouteStartOverridesV1'; + const ROUTE_DEFAULT={address:'Ленинградская область, Ломоносовский район, деревня Телези, 20',lat:59.701058,lon:29.977269}; + const CATEGORIES=[ + {id:0,label:'Боксы'}, + {id:5,label:'Премиум боксы'}, + {id:1,label:'Посуда и упаковка'}, + {id:2,label:'Дополнительно'}, + {id:3,label:'Напитки'}, + {id:4,label:'Доставка'} + ]; + const $=id=>document.getElementById(id); + const qa=(s,r=document)=>[...r.querySelectorAll(s)]; + const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + const toast=(text,type='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(text,type,ms)}catch(_){}console[type==='error'?'error':'log'](text)}; + const money=n=>`${Math.round(Number(n||0)).toLocaleString('ru-RU')} ₽`; + const readJson=(key,fallback)=>{try{const v=JSON.parse(localStorage.getItem(key)||'');return v??fallback}catch(_){return fallback}}; + const writeJson=(key,value)=>localStorage.setItem(key,JSON.stringify(value)); + const orders=()=>{const v=readJson('sunOrders',[]);return Array.isArray(v)?v:[]}; + const boxes=()=>{const v=readJson('sunBoxes',[]);return Array.isArray(v)?v:[]}; + const cloud=()=>window.SunCloudV2||null; + const support=()=>cloud()?.getSupportMode?.()||null; + const can=(perm)=>Boolean(cloud()?.hasPermission?.(perm)); + const validGeo=x=>x&&Number.isFinite(Number(x.lat))&&Number.isFinite(Number(x.lon)); + const fmtDate=raw=>{if(!raw)return'—';const d=new Date(`${raw}T12:00:00`);return Number.isNaN(d.getTime())?raw:d.toLocaleDateString('ru-RU')}; + + function installStyles(){ + if($('sunOpsUXV1762Style'))return; + const style=document.createElement('style');style.id='sunOpsUXV1762Style';style.textContent=` + .cal-more{cursor:pointer;text-decoration:underline;text-underline-offset:2px;font-weight:700}.cal-more:hover{color:var(--n)} + .sun-v1762-modal{position:fixed;inset:0;z-index:100200;background:rgba(18,28,38,.55);display:grid;place-items:center;padding:18px}.sun-v1762-modal-card{width:min(900px,100%);max-height:92vh;overflow:auto;background:#fff;border-radius:18px;box-shadow:0 24px 80px rgba(0,0,0,.28);padding:20px}.sun-v1762-modal-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.sun-v1762-modal-head h2{margin:0;color:var(--n)}.sun-v1762-modal-close{border:0;background:#f1f3f4;border-radius:10px;width:38px;height:38px;font-size:23px}.sun-v1762-modal-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-top:16px} + .sun-cal-day-list{display:grid;gap:8px;margin-top:15px}.sun-cal-day-row{display:grid;grid-template-columns:72px minmax(0,1fr) 130px auto;gap:10px;align-items:center;border:1px solid #e2e5e8;border-radius:12px;padding:11px 12px}.sun-cal-day-time{font-weight:850;color:var(--n);font-size:16px}.sun-cal-day-row small{display:block;color:#727b83;margin-top:3px}.sun-cal-day-money{text-align:right;font-weight:800;color:var(--n)} + #sun-menu-editor-v1762{max-width:1500px;margin:18px auto}.sun-menu-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}.sun-menu-head h1{margin:0}.sun-menu-tabs{display:flex;gap:7px;overflow-x:auto;margin:16px 0 12px;padding-bottom:3px}.sun-menu-tabs button{white-space:nowrap;border:1px solid #d8dde1;background:#fff;border-radius:999px;padding:9px 13px;font-weight:800;color:#56626d}.sun-menu-tabs button.on{background:var(--n);border-color:var(--n);color:#fff}.sun-menu-toolbar{display:grid;grid-template-columns:minmax(220px,1fr) auto;gap:9px;margin-bottom:12px}.sun-menu-layout{display:grid;grid-template-columns:minmax(380px,.9fr) minmax(430px,1.1fr);gap:12px;align-items:start}.sun-menu-list-pane,.sun-menu-detail-pane{border:1px solid #e1e5e8;border-radius:15px;background:#fff;padding:13px;min-height:620px}.sun-menu-list{display:grid;gap:7px;max-height:72vh;overflow:auto;padding-right:3px}.sun-menu-row{width:100%;display:grid;grid-template-columns:72px minmax(0,1fr) auto;gap:10px;align-items:center;border:1px solid #e5e8eb;border-radius:12px;background:#fff;padding:8px;text-align:left}.sun-menu-row:hover{border-color:#c9a45c;background:#fffdf8}.sun-menu-row img,.sun-menu-row .ph{width:72px;height:58px;object-fit:cover;border-radius:8px;background:#eef1f3}.sun-menu-row b{display:block;color:#263744}.sun-menu-row small{display:block;color:#77818a;margin-top:3px;line-height:1.3}.sun-menu-row-price{text-align:right;font-weight:850;color:var(--n);white-space:nowrap}.sun-menu-empty{padding:34px;text-align:center;color:#7b848c}.sun-menu-detail-placeholder{display:grid;place-items:center;min-height:560px;text-align:center;color:#78838b}.sun-menu-readonly h2{margin-top:0;color:var(--n)}.sun-menu-readonly-photo{width:min(360px,100%);aspect-ratio:4/3;object-fit:cover;border-radius:13px;background:#eef1f2}.sun-menu-readonly-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:14px 0}.sun-menu-readonly-box{background:#f7f8f9;border-radius:10px;padding:10px}.sun-menu-readonly-box small{display:block;color:#747d85}.sun-menu-readonly-box b{display:block;margin-top:4px}.sun-menu-readonly-list{margin:6px 0 0;padding-left:18px}.sun-menu-editor-pane>.dialog{display:block!important;width:100%!important;max-width:none!important;max-height:none!important;overflow:visible!important;border-radius:0!important;padding:0!important;box-shadow:none!important}.sun-menu-editor-pane>.dialog .dialog-head{position:sticky;top:0;background:#fff;z-index:2;padding-bottom:10px}.sun-menu-editor-pane>.dialog .dialog-head button{display:block}.sun-menu-readonly-badge{display:inline-flex;padding:5px 9px;border-radius:999px;background:#fff4da;color:#7b5812;font-size:11px;font-weight:850;margin-left:7px} + .sun-route-start-editor{border:1px solid #dfe4e8;border-radius:12px;padding:12px;background:#f8f9fb;margin-bottom:10px}.sun-route-start-editor label{margin-bottom:8px}.sun-route-start-actions{display:flex;gap:6px;flex-wrap:wrap}.sun-route-start-actions button{flex:1;min-width:120px}.sun-route-start-state{font-size:11px;color:#6f7981;margin-top:7px;min-height:15px}.sun-route-start-active{padding:8px 10px;border-radius:9px;background:#eef5ef;color:#355e43;margin-bottom:9px;font-size:12px}.sun-route-order-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px;margin:14px 0}.sun-route-order-kpi{background:#f7f8fa;border-radius:10px;padding:10px}.sun-route-order-kpi small{display:block;color:#737d86}.sun-route-order-kpi b{display:block;margin-top:5px;color:var(--n);font-size:16px}.sun-route-order-section{border-top:1px solid #e4e7ea;padding-top:12px;margin-top:12px}.sun-route-order-lines{width:100%;border-collapse:collapse}.sun-route-order-lines th,.sun-route-order-lines td{padding:8px;border-bottom:1px solid #e8ebed;text-align:left}.sun-route-order-lines th:nth-child(n+2),.sun-route-order-lines td:nth-child(n+2){text-align:right}.sun-route-order-note{padding:10px 12px;border-radius:10px;background:#fff7e8;margin-top:9px}.sun-route-print-v1762{display:none} + .sun-support-live-status{font-size:11px;color:#cdd7dc;white-space:nowrap}.sun-support-refresh{background:#eef2f3!important;color:#172c38!important} + @media(max-width:950px){.sun-menu-layout{grid-template-columns:1fr}.sun-menu-list-pane,.sun-menu-detail-pane{min-height:0}.sun-menu-list{max-height:none}.sun-menu-detail-placeholder{min-height:220px}.sun-route-order-grid{grid-template-columns:1fr 1fr}} + @media(max-width:650px){.sun-cal-day-row{grid-template-columns:55px minmax(0,1fr)}.sun-cal-day-money,.sun-cal-day-row>button{grid-column:2;text-align:left;justify-self:start}.sun-menu-toolbar{grid-template-columns:1fr}.sun-menu-row{grid-template-columns:58px minmax(0,1fr)}.sun-menu-row img,.sun-menu-row .ph{width:58px;height:52px}.sun-menu-row-price{grid-column:2;text-align:left}.sun-menu-readonly-grid{grid-template-columns:1fr}.sun-route-order-grid{grid-template-columns:1fr 1fr}} + @media print{body.sun-route-print-v1762 *{visibility:hidden!important}body.sun-route-print-v1762 #sunRoutePrintDocV1762,body.sun-route-print-v1762 #sunRoutePrintDocV1762 *{visibility:visible!important}body.sun-route-print-v1762 #sunRoutePrintDocV1762{display:block!important;position:absolute;left:0;top:0;width:100%;padding:8mm;background:#fff}.sun-route-print-table-v1762{width:100%;border-collapse:collapse}.sun-route-print-table-v1762 th,.sun-route-print-table-v1762 td{border:1px solid #ccd3d8;padding:6px;text-align:left}.sun-route-print-table-v1762 th{background:#f4f6f7}@page{size:A4 portrait;margin:8mm}} + `;document.head.appendChild(style); + } + + function modal(id,title,body,actions=''){ + $(`sunV1762Modal_${id}`)?.remove(); + const host=document.createElement('div');host.id=`sunV1762Modal_${id}`;host.className='sun-v1762-modal';host.innerHTML=`

${esc(title)}

${body}${actions?`
${actions}
`:''}
`;document.body.appendChild(host); + const close=()=>host.remove();host.addEventListener('click',e=>{if(e.target===host||e.target.closest('[data-v1762-close]'))close()});return {host,close}; + } + + // ---------- Developer support: full read-only visibility + live refresh ---------- + let supportBusy=false,supportLastAt=0,supportTimer=0,originalHasPermission=null; + function supportReadPermission(permission){ + const p=String(permission||''); + if(p==='app.read'||p.endsWith('.view'))return true; + return false; + } + function patchSupportPermissions(){ + const c=cloud();if(!c||c.__sunV1762SupportPermissions)return false; + originalHasPermission=typeof c.hasPermission==='function'?c.hasPermission.bind(c):()=>false; + c.hasPermission=permission=>c.isSupportMode?.()?supportReadPermission(permission):originalHasPermission(permission); + Object.defineProperty(c,'__sunV1762SupportPermissions',{value:true,configurable:true}); + return true; + } + function renderSupportStatus(){ + const banner=$('sunDevSupportBannerV22'),sm=support();if(!banner||!sm)return; + let refresh=$('sunSupportRefreshV1762');if(!refresh){refresh=document.createElement('button');refresh.type='button';refresh.id='sunSupportRefreshV1762';refresh.className='sun-support-refresh';refresh.textContent='Обновить';const exit=qa('button',banner).find(b=>b!==refresh);banner.insertBefore(refresh,exit||null);refresh.onclick=()=>refreshSupport(true);} + let state=$('sunSupportLiveStatusV1762');if(!state){state=document.createElement('span');state.id='sunSupportLiveStatusV1762';state.className='sun-support-live-status';refresh.before(state);} + state.textContent=supportBusy?'обновляю…':supportLastAt?`live · ${new Date(supportLastAt).toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit',second:'2-digit'})}`:'live'; + } + async function refreshSupport(manual=false){ + const c=cloud(),sm=support();if(!c||!sm||supportBusy||!navigator.onLine||document.hidden)return false; + supportBusy=true;renderSupportStatus(); + try{ + await c.enterSupportWorkspace(sm.workspaceId,sm.name||'Компания'); + supportLastAt=Date.now();patchSupportPermissions(); + try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:c.getWorkspace?.(),support:true,refresh:true}}));}catch(_){} + for(const name of ['render','renderOrders','renderStats','renderClients','sunRefreshStock']){try{window[name]?.()}catch(_){}} + if(manual)toast('Данные компании обновлены. Режим только для чтения сохранён.','success',3200); + return true; + }catch(error){if(manual)toast(error?.message||String(error),'error',6000);return false} + finally{supportBusy=false;renderSupportStatus();} + } + function maintainSupport(){ + patchSupportPermissions(); + const sm=support(); + if(sm){renderSupportStatus();if(!supportTimer)supportTimer=setInterval(()=>refreshSupport(false),SUPPORT_POLL_MS);} + else if(supportTimer){clearInterval(supportTimer);supportTimer=0;supportLastAt=0;} + } + + // ---------- Calendar +more day panel ---------- + const MONTHS={'январь':0,'февраль':1,'март':2,'апрель':3,'май':4,'июнь':5,'июль':6,'август':7,'сентябрь':8,'октябрь':9,'ноябрь':10,'декабрь':11}; + function calendarCellDate(cell){ + const title=$('calendar')?.querySelector('.cal-period-title')?.textContent?.trim().toLowerCase()||''; + const match=title.match(/([а-яё]+)\s+(\d{4})/i);if(!match||MONTHS[match[1]]==null)return null; + const month=MONTHS[match[1]],year=Number(match[2]),cells=qa('#calendar .cal-day'),index=cells.indexOf(cell);if(index<0)return null; + const first=new Date(year,month,1),offset=(first.getDay()+6)%7,start=new Date(year,month,1-offset);start.setDate(start.getDate()+index);return start; + } + function orderTotal(order){ + if(Number.isFinite(Number(order?.total))&&Number(order.total)>0)return Number(order.total); + const catalog=new Map(boxes().map(x=>[String(x.id),x])); + return (order?.lines||[]).reduce((sum,line)=>{const live=catalog.get(String(line.id));const p=Number.isFinite(Number(line.price))?Number(line.price):Number(live?.price||0);return sum+p*Math.max(0,Number(line.qty||0));},0); + } + function showCalendarDay(cell){ + const date=calendarCellDate(cell);if(!date)return; + const key=`${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`; + const list=orders().filter(o=>o.date===key).sort((a,b)=>String(a.time||'').localeCompare(String(b.time||''))); + const label=date.toLocaleDateString('ru-RU',{weekday:'long',day:'numeric',month:'long',year:'numeric'}); + const body=`
${list.map(o=>{const total=orderTotal(o),paid=Math.max(0,Number(o.prepayment||0));return `
${esc(o.time||'—')}
№${esc(o.id)} · ${esc(o.event||'Заказ')}${esc(o.contact||'Клиент не указан')} · ${esc(o.address||'Адрес не указан')}${esc(o.status||'Новый')}${paid?` · предоплата ${esc(money(paid))}`:''}
${esc(money(total))}
`}).join('')||'
Заказов нет.
'}
`; + const ui=modal('calendar-day',label,body);qa('[data-cal-open-v1762]',ui.host).forEach(b=>b.onclick=()=>{ui.close();const id=Number(b.dataset.calOpenV1762);if(typeof window.calendarOpenOrder==='function')window.calendarOpenOrder(id);else window.openOrder?.(id);}); + } + + // ---------- Full-page Menu editor ---------- + let menuCategory=0,menuQuery='',menuActive=false,menuButton=null,menuView=null,editorHome=null,editorDialog=null; + let originalModal=null,originalCloseModal=null,originalSaveBox=null,originalRemoveBox=null; + function menuCanEdit(){if(support())return false;const c=cloud();if(!c?.getSession?.()?.user)return true;return can('catalog.edit')} + function currentCategory(){return CATEGORIES.find(x=>x.id===menuCategory)||CATEGORIES[0]} + function menuItems(){const q=menuQuery.trim().toLowerCase();return boxes().filter(x=>Number(x.category||0)===menuCategory&&(!q||[x.name,x.weight,x.catalogSection,...(x.composition||[])].join(' ').toLowerCase().includes(q)))} + function restoreEditorDialog(){if(editorHome&&editorDialog&&editorDialog.parentNode!==editorHome){editorHome.appendChild(editorDialog);editorDialog.classList.remove('sun-menu-editor-docked');}editorHome?.classList.remove('on');} + function emptyMenuDetail(text='Выберите позицию слева, чтобы открыть карточку.'){ + const pane=$('sunMenuDetailV1762');if(!pane)return;restoreEditorDialog();pane.innerHTML=`
${esc(text)}${!menuCanEdit()?'
только просмотр
':''}
`; + } + function showReadOnlyMenuItem(item){ + restoreEditorDialog();const pane=$('sunMenuDetailV1762');if(!pane)return; + const comp=Array.isArray(item.composition)?item.composition.filter(Boolean):[],ingredients=Array.isArray(item.ingredients)?item.ingredients:[]; + pane.innerHTML=`

${esc(item.name||'Позиция')}только просмотр

${item.photo?``:''}
Категория${esc(currentCategory().label)}
Цена${esc(money(item.price||0))}
Вес${esc(item.weight||'—')}
Количество${Number(item.pieces||0)||'—'}${item.pieces?' шт.':''}
Состав для клиента${comp.length?`
    ${comp.map(x=>`
  • ${esc(x)}
  • `).join('')}
`:'

Не заполнен.

'}
Состав / ТТК${ingredients.length?`
    ${ingredients.map(x=>`
  • ${esc(x?.[0]||'')} — ${esc(x?.[1]??'')} ${esc(x?.[2]||'')}
  • `).join('')}
`:'

Не заполнен.

'}
`; + } + function dockEditor(){ + const pane=$('sunMenuDetailV1762'),home=$('editor'),dialog=home?.querySelector('.dialog')||editorDialog;if(!pane||!home||!dialog)return; + editorHome=home;editorDialog=dialog;home.classList.remove('on');pane.innerHTML='';pane.classList.add('sun-menu-editor-pane');pane.appendChild(dialog);dialog.classList.add('sun-menu-editor-docked'); + } + function renderMenuList(){ + if(!menuView)return;const list=$('sunMenuListV1762'),count=$('sunMenuCountV1762');if(!list)return; + qa('[data-menu-cat-v1762]',menuView).forEach(b=>b.classList.toggle('on',Number(b.dataset.menuCatV1762)===menuCategory)); + const items=menuItems();if(count)count.textContent=`${items.length} поз.`; + list.innerHTML=items.length?items.map(item=>``).join(''):'
В этом разделе ничего не найдено.
'; + qa('[data-menu-item-v1762]',list).forEach(b=>b.onclick=()=>{const item=boxes().find(x=>String(x.id)===String(b.dataset.menuItemV1762));if(!item)return;if(menuCanEdit()){window.editBox?.(item.id);setTimeout(dockEditor,0)}else showReadOnlyMenuItem(item)}); + } + function patchMenuLegacyEditor(){ + if(originalModal)return; + originalModal=window.modal;originalCloseModal=window.closeModal;originalSaveBox=window.saveBox;originalRemoveBox=window.removeBox; + if(typeof originalModal==='function')window.modal=function(id,...args){if(menuActive&&(id==='editor'||id==='manager')){if(id==='editor')setTimeout(dockEditor,0);return;}return originalModal.call(this,id,...args)}; + if(typeof originalCloseModal==='function')window.closeModal=function(id,...args){if(menuActive&&(id==='editor'||id==='manager')){if(id==='editor')emptyMenuDetail('Изменения сохранены. Выберите следующую позицию.');return;}return originalCloseModal.call(this,id,...args)}; + if(typeof originalSaveBox==='function')window.saveBox=function(...args){const r=originalSaveBox.apply(this,args);if(menuActive)setTimeout(()=>{renderMenuList();emptyMenuDetail('Изменения сохранены. Выберите позицию для продолжения.');},0);return r}; + if(typeof originalRemoveBox==='function')window.removeBox=function(...args){const r=originalRemoveBox.apply(this,args);if(menuActive)setTimeout(()=>{renderMenuList();emptyMenuDetail('Позиция удалена.');},0);return r}; + } + function leaveMenu(){if(!menuActive)return;menuActive=false;restoreEditorDialog();} + function openMenu(){ + if(!menuView)return;menuActive=true;patchMenuLegacyEditor();qa('.view').forEach(v=>v.classList.remove('on'));menuView.classList.add('on');qa('header nav button').forEach(b=>b.classList.remove('on'));menuButton?.classList.add('on');renderMenuList();emptyMenuDetail();window.scrollTo({top:0,behavior:'instant'}); + } + function installMenuPage(){ + if($('sun-menu-editor-v1762'))return; + const nav=document.querySelector('header nav');if(!nav)return; + const group=nav.querySelector('.sun-nav-operations')||nav; + menuButton=document.createElement('button');menuButton.type='button';menuButton.textContent='Меню';menuButton.dataset.navLabel='Меню';menuButton.className='sun-nav-button nav-menu';group.appendChild(menuButton); + menuView=document.createElement('section');menuView.id='sun-menu-editor-v1762';menuView.className='view page';menuView.innerHTML=`

Редактор меню

Полноэкранное управление каталогом без всплывающих окон.

${CATEGORIES.map(c=>``).join('')}
`;document.body.appendChild(menuView); + menuButton.onclick=openMenu; + qa('[data-menu-cat-v1762]',menuView).forEach(b=>b.onclick=()=>{menuCategory=Number(b.dataset.menuCatV1762);menuQuery='';$('sunMenuSearchV1762').value='';emptyMenuDetail();renderMenuList();}); + $('sunMenuSearchV1762').oninput=e=>{menuQuery=e.target.value||'';renderMenuList()}; + $('sunMenuAddV1762').onclick=()=>{if(!menuCanEdit())return toast('В этом режиме каталог доступен только для просмотра.','warn');window.editBox?.(null);setTimeout(()=>{const select=$('itemCategory');if(select){select.value=String(menuCategory);select.dispatchEvent(new Event('change',{bubbles:true}))}dockEditor();},0)}; + document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&b!==menuButton)leaveMenu();},true); + const setupOldButton=()=>{const old=qa('#new .catalog-head button').find(b=>/настроить|редактор/i.test(b.textContent||''));if(old&&!old.__sunV1762){old.__sunV1762=true;old.textContent='Меню';old.onclick=e=>{e.preventDefault();e.stopPropagation();openMenu();};}};setupOldButton();setInterval(setupOldButton,4000); + syncMenuPermission(); + } + function syncMenuPermission(){if(!menuButton)return;const c=cloud();const signed=Boolean(c?.getSession?.()?.user);const allowed=!signed||support()||can('catalog.view')||can('catalog.edit');menuButton.hidden=!allowed;menuButton.style.display=allowed?'':'none';const add=$('sunMenuAddV1762');if(add){add.disabled=!menuCanEdit();add.title=menuCanEdit()?'':'Только просмотр';}} + + // ---------- Route start + order popup ---------- + function routeKey(){return `${$('routeDate')?.value||''}|${$('routeCourier')?.value||'all'}`} + function routeBase(){const x=readJson(ROUTE_BASE_KEY,null);return x&&x.address?x:ROUTE_DEFAULT} + function routeOverrides(){const x=readJson(ROUTE_OVERRIDE_KEY,{});return x&&typeof x==='object'&&!Array.isArray(x)?x:{}} + function effectiveRouteStart(){const over=routeOverrides()[routeKey()];return over?.address?over:routeBase()} + function routeOrders(){ + const date=$('routeDate')?.value||'',courier=$('routeCourier')?.value||'';let list=orders().filter(o=>!['Отменён','Отдан заказчику'].includes(o.status||'Новый')&&o.date===date&&(o.address||validGeo(o.geo))); + if(courier)list=list.filter(o=>(o.staffIds||[]).map(String).includes(String(courier))); + const plans=readJson('sunRoutePlansV1',{}),ids=plans[`${date}|${courier||'all'}`]||[],map=new Map(list.map(o=>[String(o.id),o])),ordered=ids.map(id=>map.get(String(id))).filter(Boolean);list.filter(o=>!ids.map(String).includes(String(o.id))).sort((a,b)=>String(a.time||'').localeCompare(String(b.time||''))).forEach(o=>ordered.push(o));return ordered; + } + function hav(a,b){const R=6371,rad=x=>x*Math.PI/180,dLat=rad(Number(b.lat)-Number(a.lat)),dLon=rad(Number(b.lon)-Number(a.lon)),la1=rad(Number(a.lat)),la2=rad(Number(b.lat));const h=Math.sin(dLat/2)**2+Math.cos(la1)*Math.cos(la2)*Math.sin(dLon/2)**2;return 2*R*Math.asin(Math.sqrt(h))} + function routeDistance(list,start){const pts=[];if(validGeo(start))pts.push(start);list.filter(o=>validGeo(o.geo)).forEach(o=>pts.push(o.geo));let total=0;for(let i=1;ipts.push(validGeo(o.geo)?`${Number(o.geo.lat)},${Number(o.geo.lon)}`:String(o.address||'')));return `https://yandex.ru/maps/?rtext=${encodeURIComponent(pts.filter(Boolean).join('~'))}&rtt=auto`;} + async function geocodeRouteBase(address){ + const clean=String(address||'').trim();if(!clean)throw new Error('Введите адрес начала маршрута.'); + const query=/санкт[- ]?петербург|\bспб\b|москва|ленинградск|область|край|республика|телези|гатчин|пушкин|петергоф|ломоносов/i.test(clean)?clean:`Санкт-Петербург, ${clean}`; + const request=window.sunFetchWithTimeout||((url,opts)=>fetch(url,opts)); + let row=null,provider='nominatim'; + try{const url=`https://nominatim.openstreetmap.org/search?format=jsonv2&limit=5&countrycodes=ru&accept-language=ru&q=${encodeURIComponent(query)}`;const res=await request(url,{method:'GET',mode:'cors',credentials:'omit',cache:'default'},6000);if(res.ok){const rows=await res.json();row=Array.isArray(rows)?rows[0]:null;}}catch(_){row=null} + if(!row||!Number.isFinite(Number(row.lon))||!Number.isFinite(Number(row.lat))){try{const url=`https://photon.komoot.io/api/?limit=1&lang=ru&q=${encodeURIComponent(query)}`;const res=await request(url,{method:'GET',mode:'cors',credentials:'omit',cache:'default'},6000);if(res.ok){const data=await res.json(),f=data?.features?.[0],c=f?.geometry?.coordinates,p=f?.properties||{};if(Array.isArray(c)&&Number.isFinite(Number(c[0]))&&Number.isFinite(Number(c[1]))){row={lon:Number(c[0]),lat:Number(c[1]),display_name:[p.name,p.street,p.housenumber,p.city,p.state].filter(Boolean).join(', ')||query};provider='photon';}}}catch(_){row=null}} + if(!row||!Number.isFinite(Number(row.lon))||!Number.isFinite(Number(row.lat)))throw new Error('Не удалось найти адрес начала маршрута. Проверьте адрес и интернет.'); + return {address:clean,lat:Number(row.lat),lon:Number(row.lon),formatted:String(row.display_name||clean),verifiedAt:new Date().toISOString(),provider}; + } + async function saveRouteStart(asDefault=false){ + if(support())return toast('В режиме разработчика данные компании доступны только для просмотра.','warn'); + const input=$('sunRouteStartAddressV1762'),state=$('sunRouteStartStateV1762');if(!input)return; + const text=input.value.trim();if(!text)return toast('Введите адрес начала маршрута.','warn'); + if(state)state.textContent='Ищу адрес на карте…'; + try{ + const geo=await geocodeRouteBase(text); + if(asDefault){writeJson(ROUTE_BASE_KEY,geo);const over=routeOverrides();delete over[routeKey()];writeJson(ROUTE_OVERRIDE_KEY,over);toast('Основная база маршрутов сохранена.','success');} + else{const over=routeOverrides();over[routeKey()]=geo;writeJson(ROUTE_OVERRIDE_KEY,over);toast('Начало маршрута сохранено для выбранной даты и курьера.','success');} + enhanceRoutePage(); + }catch(error){if(state)state.textContent=error?.message||String(error);toast(error?.message||String(error),'error',6000)} + } + function resetRouteStart(){if(support())return toast('В режиме разработчика данные компании доступны только для просмотра.','warn');const over=routeOverrides();delete over[routeKey()];writeJson(ROUTE_OVERRIDE_KEY,over);enhanceRoutePage();toast('Для маршрута снова используется основная база.','success')} + function enhanceRoutePage(){ + const view=$('sun-routes-view'),side=view?.querySelector('.route-side');if(!view?.classList.contains('on')||!side)return; + const start=effectiveRouteStart(),base=routeBase(),isOverride=Boolean(routeOverrides()[routeKey()]); + let box=$('sunRouteStartEditorV1762');if(!box){box=document.createElement('div');box.id='sunRouteStartEditorV1762';box.className='sun-route-start-editor';const h=side.querySelector('h2');h?.insertAdjacentElement('afterend',box);} + box.innerHTML=`
${isOverride?'Старт этого маршрута':'Основная база'}
${esc(start.address||'—')}
${isOverride?'':''}
${isOverride?`Основная база: ${esc(base.address||'—')}`:'Можно задать другой старт только для выбранной даты/курьера.'}
`; + const readOnly=Boolean(support());const addrInput=$('sunRouteStartAddressV1762');if(addrInput)addrInput.disabled=readOnly;const apply=$('sunRouteStartApplyV1762'),def=$('sunRouteStartDefaultV1762'),reset=$('sunRouteStartResetV1762');if(apply){apply.disabled=readOnly;apply.onclick=()=>saveRouteStart(false)}if(def){def.disabled=readOnly;def.onclick=()=>saveRouteStart(true)}if(reset){reset.disabled=readOnly;reset.addEventListener('click',resetRouteStart)}if(readOnly&&$('sunRouteStartStateV1762'))$('sunRouteStartStateV1762').textContent='Режим разработчика: точка старта доступна только для просмотра.'; + qa(':scope > div',side).filter(x=>x!==box&&/Телези,\s*20|Постоянная точка старта/i.test(x.textContent||'')).forEach(x=>x.style.display='none'); + qa(':scope > p.hint',side).filter(x=>/Телези,\s*20/i.test(x.textContent||'')).forEach(x=>x.textContent='Маршрут и печать используют выбранную выше точку старта.'); + const list=routeOrders(),distance=routeDistance(list,start),summary=qa('.route-summary>div',view).find(x=>/Примерно км/i.test(x.querySelector('small')?.textContent||''));if(summary?.querySelector('b'))summary.querySelector('b').textContent=distance.toLocaleString('ru-RU',{maximumFractionDigits:1}); + } + function routeOrderById(id){return orders().find(x=>String(x.id)===String(id))||null} + function routeOrderLines(order){const catalog=new Map(boxes().map(x=>[String(x.id),x]));return (order.lines||[]).map(line=>{const item=catalog.get(String(line.id)),qty=Math.max(0,Number(line.qty||0)),price=Number.isFinite(Number(line.price))?Number(line.price):Number(item?.price||0);return {name:item?.name||line.name||`Позиция ${line.id}`,qty,price,sum:qty*price}}).filter(x=>x.qty>0)} + function showRouteOrder(id){ + const o=routeOrderById(id);if(!o)return toast('Заказ не найден.','warn');const lines=routeOrderLines(o),total=orderTotal(o),paid=Math.max(0,Number(o.prepayment||0)),balance=Math.max(0,total-paid),editable=!support()&&can('orders.edit'); + const body=`
Дата и время${esc(fmtDate(o.date))} · ${esc(o.time||'—')}
Статус${esc(o.status||'Новый')}
Сумма${esc(money(total))}
Остаток${esc(money(balance))}

${esc(o.event||'Заказ')}${o.guestsCount?` · ${esc(o.guestsCount)} гостей`:''}

Клиент: ${esc(o.contact||'—')} · ${esc(o.phone||'—')}

Адрес: ${esc(o.address||'—')}

${o.delivery?`

Доставка: ${esc(money(o.delivery))}

`:''}${o.courierNote?`
Курьеру:
${esc(o.courierNote)}
`:''}${o.note?`
Комментарий:
${esc(o.note)}
`:''}
Меню заказа
${lines.length?lines.map(x=>``).join(''):''}
ПозицияКол.ЦенаСумма
${esc(x.name)}${x.qty}${esc(money(x.price))}${esc(money(x.sum))}
Позиции не указаны.
`; + const phone=String(o.phone||'').replace(/[^+\d]/g,'');const mapUrl=validGeo(o.geo)?`https://yandex.ru/maps/?ll=${encodeURIComponent(`${o.geo.lon},${o.geo.lat}`)}&z=16&pt=${encodeURIComponent(`${o.geo.lon},${o.geo.lat},pm2rdm`)}`:`https://yandex.ru/maps/?text=${encodeURIComponent(o.address||'')}`; + const actions=`${phone?``:''}${editable?'':''}`; + const ui=modal('route-order',`Заказ №${o.id}`,body,actions);ui.host.querySelector('[data-route-call-v1762]')?.addEventListener('click',()=>{location.href=`tel:${phone}`});ui.host.querySelector('[data-route-map-v1762]')?.addEventListener('click',()=>window.open(mapUrl,'_blank','noopener'));ui.host.querySelector('[data-route-edit-v1762]')?.addEventListener('click',()=>{ui.close();window.openOrder?.(Number(o.id))}); + } + async function copyCurrentRoute(){const list=routeOrders(),start=effectiveRouteStart(),courier=$('routeCourier')?.selectedOptions?.[0]?.textContent||'Все курьеры',text=[`Маршрут ${$('routeDate')?.value||''} · ${courier}`,`Старт: ${start.address||'—'}`,...list.map((o,i)=>`${i+1}. ${o.time||'—'} · Заказ №${o.id} · ${o.contact||''}\n${o.address||''}\n${o.phone||''}${o.courierNote?`\nКомментарий: ${o.courierNote}`:''}`),`Яндекс Карты: ${yandexRouteUrl(list,start)}`].join('\n\n');try{await navigator.clipboard.writeText(text);toast('Маршрут скопирован.','success')}catch(_){const ta=document.createElement('textarea');ta.value=text;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();toast('Маршрут скопирован.','success')}} + function printCurrentRoute(){ + $('sunRoutePrintDocV1762')?.remove();const list=routeOrders(),start=effectiveRouteStart(),courier=$('routeCourier')?.selectedOptions?.[0]?.textContent||'Все курьеры',doc=document.createElement('section');doc.id='sunRoutePrintDocV1762';doc.className='sun-route-print-v1762';doc.innerHTML=`

Маршрутный лист

Дата: ${esc($('routeDate')?.value||'—')} · Курьер: ${esc(courier)}

Старт: ${esc(start.address||'—')}

${list.map((o,i)=>``).join('')}
ВремяЗаказ / клиентАдресТелефон
${i+1}${esc(o.time||'—')}№${esc(o.id)} · ${esc(o.contact||o.event||'')}${esc(o.address||'')}${esc(o.phone||'')}

Ссылка: ${esc(yandexRouteUrl(list,start))}

`;document.body.appendChild(doc);document.body.classList.add('sun-route-print-v1762');window.print();setTimeout(()=>{document.body.classList.remove('sun-route-print-v1762');doc.remove();},160); + } + function interceptRouteActions(e){ + const open=e.target.closest('#sun-routes-view [data-route-open]');if(open){e.preventDefault();e.stopImmediatePropagation();showRouteOrder(open.dataset.routeOpen);return} + const start=effectiveRouteStart(),list=routeOrders(); + if(e.target.closest('#sun-routes-view #routeOpenYandex')){e.preventDefault();e.stopImmediatePropagation();window.open(yandexRouteUrl(list,start),'_blank','noopener');return} + if(e.target.closest('#sun-routes-view #routeCopy')){e.preventDefault();e.stopImmediatePropagation();copyCurrentRoute();return} + if(e.target.closest('#sun-routes-view #routePrint')){e.preventDefault();e.stopImmediatePropagation();printCurrentRoute();return} + } + + function loadSelfChecks(){ + return { + version:VERSION,release:RELEASE, + supportReadOnly:Boolean(cloud()?.isSupportMode?.()?can('orders.view')&&!can('orders.edit'):true), + menuPage:Boolean($('sun-menu-editor-v1762')), + routeStartKey:ROUTE_BASE_KEY, + routeOrderPopup:true, + calendarMore:true + }; + } + function boot(){ + installStyles();patchSupportPermissions();installMenuPage();maintainSupport();syncMenuPermission();enhanceRoutePage(); + document.addEventListener('click',e=>{const more=e.target.closest('#calendar .cal-more');if(more){e.preventDefault();e.stopImmediatePropagation();showCalendarDay(more.closest('.cal-day'));return}interceptRouteActions(e)},true); + window.addEventListener('sun:cloud-permissions-changed',()=>{patchSupportPermissions();maintainSupport();syncMenuPermission();if(menuActive){renderMenuList();emptyMenuDetail();}}); + window.addEventListener('sun:cloud-state-applied',()=>{maintainSupport();syncMenuPermission();if(menuActive)renderMenuList();setTimeout(enhanceRoutePage,80)}); + const mo=new MutationObserver(records=>{let route=false,supportBanner=false;for(const r of records){for(const n of r.addedNodes){if(n.nodeType!==1)continue;if(n.id==='routeContent'||n.querySelector?.('#routeContent')||n.closest?.('#sun-routes-view'))route=true;if(n.id==='sunDevSupportBannerV22'||n.querySelector?.('#sunDevSupportBannerV22'))supportBanner=true;}}if(route)setTimeout(enhanceRoutePage,30);if(supportBanner)setTimeout(renderSupportStatus,20);});mo.observe(document.documentElement,{childList:true,subtree:true}); + setInterval(()=>{maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},4000); + window.SunOpsUXV1762={VERSION,RELEASE,refreshSupport,openMenu,showRouteOrder,enhanceRoutePage,checks:loadSelfChecks,disconnect:()=>mo.disconnect()}; + } + const waitForApp=()=>{if(window.SunCloudV2&&document.querySelector('header nav')&&typeof window.editBox==='function')boot();else setTimeout(waitForApp,120)}; + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',waitForApp,{once:true});else waitForApp(); +})(); diff --git a/public/core/performance.js b/public/core/performance.js index c299a3c..68cf4dc 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -53,7 +53,7 @@ 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}; + window.SunAttachmentGuard={VERSION:'17.6.2',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??''); @@ -65,19 +65,23 @@ 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 version=document.getElementById('sunDevReleaseVersion');if(version)version.textContent='17.6.2'; 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=`
Память сервера${esc(d.server_size||d.database_size||'—')}
База PostgreSQL${esc(d.database_size||'—')}
Файлы Storage${esc(d.storage_size||'—')}
Объектов Storage${Number(d.storage_objects||0)}
`; } + function loadOpsUX(){ + if(window.SunOpsUXV1762||document.getElementById('sunOpsUXV1762Script'))return; + const script=document.createElement('script');script.id='sunOpsUXV1762Script';script.src='core/ops-ux-v1762.js?v=20260907-v17-6-2-ops-ux';script.async=true;script.onerror=()=>console.error('[Caterium] Не загрузился модуль ops-ux-v1762.js');document.head.appendChild(script); + } const start=()=>{ - scan(document); + loadOpsUX();scan(document); 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}); 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()}; + window.SunPerformance={scanImages:()=>scan(document),refreshDeveloperMemory:enhanceDeveloperMemory,loadOpsUX,disconnect:()=>mo.disconnect()}; }; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start(); -})(); \ No newline at end of file +})(); diff --git a/public/service-worker.js b/public/service-worker.js index 63260f3..5962654 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,8 +1,8 @@ -const CACHE='sun-catering-pwa-v66-20260907-v17-6-1-chat-storage-guard'; -const VERSION='20260907-v17-6-1-chat-storage-guard'; +const CACHE='sun-catering-pwa-v67-20260907-v17-6-2-ops-ux'; +const VERSION='20260907-v17-6-2-ops-ux'; const CORE=[ './','./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/ops-ux-v1762.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`, './offer-gallery/001.jpg','./offer-gallery/002.jpg', './catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg', './sun-logo.png','./caterium-login-logo.png','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest', diff --git a/tests/app.spec.mjs b/tests/app.spec.mjs index 3e9f788..aef0992 100644 --- a/tests/app.spec.mjs +++ b/tests/app.spec.mjs @@ -37,6 +37,34 @@ test('chat photo guard compresses a large camera image', async ({ page }) => { }); 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('v17.6.2 operations UX boots with menu and route features', async ({ page }) => { + await page.goto('/index.html', { waitUntil:'domcontentloaded' }); + await page.evaluate(()=>{ + if(!document.querySelector('header nav')){ + const header=document.createElement('header'); + const nav=document.createElement('nav'); + header.appendChild(nav); + document.body.appendChild(header); + } + window.SunCloudV2={ + getSupportMode:()=>null, + isSupportMode:()=>false, + hasPermission:()=>true, + getSession:()=>({user:{id:'e2e'}}) + }; + window.editBox=()=>{}; + }); + const content=fs.readFileSync(path.join(process.cwd(),'public','core','ops-ux-v1762.js'),'utf8'); + await page.addScriptTag({content}); + await page.waitForFunction(()=>Boolean(window.SunOpsUXV1762),null,{timeout:5000}); + const checks=await page.evaluate(()=>window.SunOpsUXV1762.checks()); + expect(checks.version).toBe('17.6.2'); + expect(checks.menuPage).toBeTruthy(); + expect(checks.routeStartKey).toBe('sunRouteBaseV1'); + expect(checks.routeOrderPopup).toBeTruthy(); + expect(checks.calendarMore).toBeTruthy(); + expect(await page.locator('header nav button', {hasText:'Меню'}).count()).toBeGreaterThan(0); +}); 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); const dims=await page.evaluate(()=>({innerWidth,scrollWidth:document.documentElement.scrollWidth,bodyWidth:document.body.scrollWidth})); diff --git a/tests/release-check.mjs b/tests/release-check.mjs index 2dd3150..91636f0 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -3,16 +3,22 @@ import path from 'node:path'; const root=process.cwd(), pub=path.join(root,'public'); 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++}; -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'); +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'),ops=read('core/ops-ux-v1762.js'); 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(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent'); check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer'); check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox'); check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4'); -check(sw.includes('v17-6-1-chat-storage-guard'),'service worker cache is v17.6.1'); +check(sw.includes('v17-6-2-ops-ux')&&sw.includes('ops-ux-v1762.js'),'service worker cache is v17.6.2'); 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('ops-ux-v1762.js'),'operations UX module is loaded by performance core'); +check(ops.includes('supportReadPermission')&&ops.includes('SUPPORT_POLL_MS=12000'),'developer support read-only live refresh is versioned'); +check(ops.includes('sun-menu-editor-v1762')&&ops.includes('Премиум боксы'),'full-page menu editor is versioned'); +check(ops.includes('showCalendarDay')&&ops.includes('.cal-more'),'calendar overflow panel is versioned'); +check(ops.includes("ROUTE_BASE_KEY='sunRouteBaseV1'")&&ops.includes('saveRouteStart'),'configurable route origin is versioned'); +check(ops.includes('showRouteOrder')&&ops.includes('data-route-open'),'route order modal 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+performance),'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+ops),'no client secret-like token'); if(bad)process.exit(1); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index 2dbafee..4a20c73 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -6,7 +6,7 @@ const readPub=p=>fs.readFileSync(path.join(pub,p),'utf8'); const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8'); const fail=m=>{console.error('FAIL:',m);process.exitCode=1}; const ok=m=>console.log('OK:',m); -const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'); +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'),ops=readPub('core/ops-ux-v1762.js'); if(!html.includes('core/sun-safe.js'))fail('SunSafe must load before legacy modules');else ok('shared SunSafe loaded'); if(html.includes('offer-gallery-data.js')||fs.existsSync(path.join(pub,'offer-gallery-data.js')))fail('blocking offer-gallery-data.js still present');else ok('base64 gallery removed'); for(const raw of ['${b.name}','${o.event}','${o.address||','value="${x[0]}"','value="${x[2]}"']) if(legacy.includes(raw)) fail(`legacy bootstrap contains raw HTML interpolation ${raw}`); @@ -27,7 +27,10 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos'); const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x)); if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed'); -if(!sw.includes('v17-6-1-chat-storage-guard')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated'); +if(!sw.includes('v17-6-2-ops-ux')||!sw.includes('ops-ux-v1762.js')||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(!performance.includes('ops-ux-v1762.js')||!performance.includes('SunOpsUXV1762'))fail('ops UX loader missing');else ok('ops UX loader present'); +for(const marker of ['SUPPORT_POLL_MS=12000','supportReadPermission','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}`); +if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops))fail('possible secret in ops UX module');else ok('ops UX module has no hard-coded secret'); if(process.exitCode)process.exit(process.exitCode);