diff --git a/docs/CALENDAR-PRINT-20260917.md b/docs/CALENDAR-PRINT-20260917.md
new file mode 100644
index 0000000..5add7bc
--- /dev/null
+++ b/docs/CALENDAR-PRINT-20260917.md
@@ -0,0 +1,7 @@
+# Compact calendar days and printing
+
+Month cells keep the existing detailed cards for one to three orders. With four or more orders, each event becomes one line containing time, order number and event name. Payment colors and click-to-open behavior remain available. The first nine events are shown; the existing overflow dialog opens every order for busier days.
+
+The landscape A4 print view fits nine lines inside each fixed-height date cell. Orders beyond nine appear once on a continuation sheet, grouped by date; the cell count points to that sheet. Printing retains the existing omission of financial amounts. Calendar data is unchanged.
+
+Regression coverage checks three/four/nine/eleven orders, opening an overflow order, printed row bounds, complete and unique order IDs across calendar and continuation, and absence of prices in print. PDF rendering was checked separately: nine orders fit on one A4 landscape page; an eleven-order day creates a second page containing its two remaining events.
diff --git a/docs/release-manifest.json b/docs/release-manifest.json
index 8e69141..c630145 100644
--- a/docs/release-manifest.json
+++ b/docs/release-manifest.json
@@ -11,7 +11,7 @@
"serverReady": true,
"workspaceAutoDiscovery": true,
"invitesTemporarilyDisabled": false,
- "pwaCache": "v88-20260917-banquet-menu",
+ "pwaCache": "v89-20260917-calendar-compact",
"fullOfferDescriptions": true,
"dynamicOfferRows": true,
"pdfOfferDescriptionFix": true,
diff --git a/public/core/performance.js b/public/core/performance.js
index 1619e36..a9161d3 100644
--- a/public/core/performance.js
+++ b/public/core/performance.js
@@ -1,7 +1,7 @@
(()=>{
'use strict';
const VERSION='17.7.3';
- const RELEASE='20260917-banquet-menu';
+ const RELEASE='20260917-calendar-compact';
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
function installAuthBoot(){
diff --git a/public/index.html b/public/index.html
index 93ae887..2156b85 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,4 +1,4 @@
-
-
+
diff --git a/public/service-worker.js b/public/service-worker.js
index 3df97cb..04bbdd6 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,5 +1,5 @@
-const CACHE='sun-catering-pwa-v88-20260917-banquet-menu';
-const VERSION='20260917-banquet-menu';
+const CACHE='sun-catering-pwa-v89-20260917-calendar-compact';
+const VERSION='20260917-calendar-compact';
const CORE=[
'./','./index.html',`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
diff --git a/tests/calendar-fixture.mjs b/tests/calendar-fixture.mjs
new file mode 100644
index 0000000..b3962f7
--- /dev/null
+++ b/tests/calendar-fixture.mjs
@@ -0,0 +1,23 @@
+export async function bootCalendar(page,context,counts=[3,4,9,11]){
+ await context.addInitScript(()=>{window.print=()=>{};window.close=()=>{};localStorage.setItem('sunCalendarMode','month');localStorage.setItem('sunOrderStatusFeatureEnabledV1','1')});
+ await page.route('**://api.caterium.ru/**',r=>r.abort());
+ await page.route('**://*.supabase.co/**',r=>r.abort());
+ await page.goto('/index.html',{waitUntil:'domcontentloaded'});
+ await page.waitForFunction(()=>window.CateriumDataV1773&&window.SunAdminRBACV3&&window.__cateriumOrderEnhancementsV1775);
+ const dates=await page.evaluate(counts=>{
+ window.SunCloudV2={getSession:()=>({user:{id:'calendar-test'}}),getWorkspace:()=>({id:'calendar-test',role:'admin',permissions:{}}),getClient:()=>null,status:()=>({connected:true,signedIn:true,membershipsLoaded:true}),hasPermission:()=>true};
+ const now=new Date(),keys=[];orders.splice(0);
+ counts.forEach((count,day)=>{
+ const d=new Date(now.getFullYear(),now.getMonth(),10+day),date=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;keys.push(date);
+ for(let i=0;i{
+ const dates=await bootCalendar(page,context);
+ const day=i=>page.locator(`.cal-day[data-date="${dates[i]}"]`);
+ await expect(day(0)).not.toHaveClass(/cal-day--compact/);
+ await expect(day(0).locator('.cal-event:visible')).toHaveCount(3);
+ await expect(day(1)).toHaveClass(/cal-day--compact/);
+ await expect(day(1).locator('.cal-event:visible')).toHaveCount(4);
+ await expect(day(2).locator('.cal-event:visible')).toHaveCount(9);
+ await expect(day(2).locator('.cal-more')).toHaveCount(0);
+ expect(await day(2).locator('.cal-event').evaluateAll(rows=>rows.every(row=>row.getBoundingClientRect().height<26))).toBe(true);
+ await expect(day(3).locator('.cal-event:visible')).toHaveCount(9);
+ await expect(day(3).locator('.cal-more')).toHaveText('+ ещё 2');
+ await day(3).locator('.cal-more').click();
+ await expect(page.locator('[data-cal-open-v1762]')).toHaveCount(11);
+ await page.locator('[data-cal-open-v1762="1310"]').click();
+ expect(await page.evaluate(()=>draft.id)).toBe(1310);
+});
+
+test('calendar print fits nine orders inside the day and includes overflow on a continuation sheet',async({page,context})=>{
+ const dates=await bootCalendar(page,context),popup=await printCalendar(page,context);
+ await popup.emulateMedia({media:'print'});
+ await expect(popup.locator(`.cal-day[data-date="${dates[2]}"] .cal-event`)).toHaveCount(9);
+ const fit=await popup.locator('.cal-day').evaluateAll(days=>{
+ return days.every(day=>{const cell=day.getBoundingClientRect();return [...day.querySelectorAll('.cal-event')].every(row=>{const box=row.getBoundingClientRect();return box.top>=cell.top&&box.bottom<=cell.bottom-1})});
+ });
+ expect(fit).toBe(true);
+ await expect(popup.locator('.cal-print-overflow .cal-event')).toHaveCount(2);
+ await expect(popup.locator('.cal-print-overflow [data-order-id="1310"]')).toBeVisible();
+ const ids=await popup.locator('.cal-event').evaluateAll(rows=>rows.map(r=>r.dataset.orderId));
+ expect(ids).toHaveLength(27);expect(new Set(ids).size).toBe(27);
+ expect(await popup.locator('.cal-print-overflow').evaluate(el=>getComputedStyle(el).breakBefore)).toBe('page');
+ expect(await popup.locator('body').innerText()).not.toContain('₽');
+ await popup.close();
+});
diff --git a/tests/playwright.config.mjs b/tests/playwright.config.mjs
index fe2591d..8a3f3dd 100644
--- a/tests/playwright.config.mjs
+++ b/tests/playwright.config.mjs
@@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
import {fileURLToPath} from 'node:url';
export default defineConfig({
testDir:'.',
- testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs'],
+ testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs'],
timeout:30000,
use:{baseURL:'http://127.0.0.1:4173'},
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
diff --git a/tests/release-check.mjs b/tests/release-check.mjs
index 9e6e8fc..8ba47de 100644
--- a/tests/release-check.mjs
+++ b/tests/release-check.mjs
@@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent')
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
-check(sw.includes('v88-20260917-banquet-menu')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
-check(index.includes('20260917-banquet-menu')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
+check(sw.includes('v89-20260917-calendar-compact')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
+check(index.includes('20260917-calendar-compact')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
@@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
check(releaseManifest.channel==='production','release manifest channel is production');
-check(String(releaseManifest.pwaCache||'').includes('v88-20260917-banquet-menu'),'release manifest points to current PWA cache');
+check(String(releaseManifest.pwaCache||'').includes('v89-20260917-calendar-compact'),'release manifest points to current PWA cache');
check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=12000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
@@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
check(pkg.version==='17.7.3','package version is v17.7.3');
-check(index.includes('20260917-banquet-menu'),'index cache bust is v17.7.3');
-check(sw.includes('v88-20260917-banquet-menu')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
+check(index.includes('20260917-calendar-compact'),'index cache bust is v17.7.3');
+check(sw.includes('v89-20260917-calendar-compact')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
diff --git a/tests/static-security.mjs b/tests/static-security.mjs
index 888c4d4..9b93d08 100644
--- a/tests/static-security.mjs
+++ b/tests/static-security.mjs
@@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
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('20260917-banquet-menu')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
-if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260917-banquet-menu')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
+if(!sw.includes('20260917-calendar-compact')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
+if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260917-calendar-compact')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');