Menu Renderer — BR 4.1

Контракты

Mini-bundle для рендера меню на мониторе. Отдельная сборка (Vite), деплоится в Catalog Service/resources/static/menu-renderer/.

Где живёт

В монорепо erp-admin/:

erp-admin/
├── apps/
│   ├── menu-renderer/    ← НОВОЕ. Vite project
│   │   ├── package.json
│   │   ├── vite.config.ts
│   │   ├── index.html (template для SSR)
│   │   ├── src/
│   │   │   ├── main.ts
│   │   │   ├── templates/
│   │   │   │   ├── grid.ts
│   │   │   │   ├── slider.ts
│   │   │   │   └── list.ts
│   │   │   ├── live-update.ts (WebSocket + polling)
│   │   │   ├── styles/
│   │   │   │   └── shared.css
│   │   │   └── utils.ts
│   │   └── tsconfig.json
│   └── menu-renderer-offline/  ← НОВОЕ. Bundle для offline ZIP (без WebSocket)
└── ...

Build → dist/menu-renderer/{index.js, style.css} — копируется CI/CD в Catalog Service.

Что делаем

Vanilla JS / TS клиент рендера

  • apps/menu-renderer/src/main.ts:

    • Парсит embedded JSON меню из <script id="menu-data">
    • Определяет шаблон по menu.template
    • Вызывает renderTemplate() который рисует DOM
    • Подключает live-update (WebSocket → polling fallback)
  • apps/menu-renderer/src/templates/grid.ts:

    • Renders сетку карточек с заголовками категорий
    • CSS Grid + clamp() для адаптивности
    • Auto-перелистывание категорий каждые 15 сек если все не помещаются
  • apps/menu-renderer/src/templates/slider.ts:

    • Один товар на экран с auto-advance каждые 5 сек
    • Crossfade transition
  • apps/menu-renderer/src/templates/list.ts:

    • Длинный список с большой ценой
    • Группировка по категориям

Live-update

  • apps/menu-renderer/src/live-update.ts:

    • connect(slug) — открыть WebSocket /r/{slug}/stream
    • На menu_updatedfetch /r/{slug}/data → сравнить version_hash → rerender(newData) если изменилось
    • Reconnect с экспоненциальным backoff (1s → 2s → 4s → 8s → 30s)
    • Если connection lost > 60 сек → переключиться на polling (setInterval 30 сек)
    • При восстановлении WebSocket — отключить polling
    • Heartbeat: ответ pong на ping от сервера; если нет ping > 60 сек — считаем lost
  • При menu_unpublished / menu_archived — показать full-screen banner «Меню снято с публикации»

Адаптивность

  • CSS-переменные:
    :root {
      --columns-grid: 4;
      --font-base: 18px;
    }
    @media (min-width: 3000px) { /* 4K */
      :root { --columns-grid: 6; --font-base: 28px; }
    }
    @media (orientation: portrait) { /* 9:16 */
      :root { --columns-grid: 2; }
    }
  • Тестировать на 3 разрешениях: 1920×1080, 3840×2160, 1080×1920

Шрифты и assets

  • Inter (или подобный) — bundled в bundle (Inter-Bold.woff2 ~80KB) для гарантированного рендера без интернета
  • Иконки категорий — SVG из системы (если задана icon_url) или fallback emoji

Server-side template (Thymeleaf или Mustache)

  • В Catalog Service resources/templates/menu.html:
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title th:text="${menu.name}"></title>
  <link rel="stylesheet" href="/static/menu-renderer/style.css">
</head>
<body>
  <div id="menu-root"></div>
  <script id="menu-data" type="application/json" th:utext="${menuJson}"></script>
  <script src="/static/menu-renderer/index.js"></script>
</body>
</html>
  • Catalog Service подставляет menuJson (resolved menu с overrides + stop-list filter) и slug для WebSocket-URL
  • Client JS читает menu-data и рендерит

Offline ZIP version

  • apps/menu-renderer-offline/src/main.ts:

    • То же что live-версия, но БЕЗ live-update модуля
    • Меню вшито прямо в bundle при сборке (server-side инжект через template’ы при создании ZIP)
  • OfflineZipBuilder в Catalog Service:

    • Использует тот же template menu.html, но JS из apps/menu-renderer-offline/
    • Скачивает картинки product → конвертирует пути к assets/images/{filename}
    • Упаковывает всё в ZIP

Preview-режим

  • При ?preview=1:
    • В правом нижнем углу 🟡 PREVIEW индикатор (фиксированный)
    • Не подключается WebSocket / polling
    • Banner «Меню снято с публикации» не показывается даже если archived (для тестирования)

CI / deployment

  • pnpm --filter menu-renderer builddist/menu-renderer/index.js, style.css
  • CI копирует bundle в erp-catalog-service/src/main/resources/static/menu-renderer/ перед сборкой Spring Boot
  • Версионирование bundle через hash (index.{hash}.js) — кэширование 1 год

Не делаем (P1+)

  • ❌ Server-side rendering (Astro/Next) — пока vanilla TS достаточно
  • ❌ Multi-language UI — все надписи на русском
  • ❌ Анимации beyond crossfade — добавим позже если надо
  • ❌ Touch-события для планшетов — это монитор, не киоск
  • ❌ Сбор метрик (FCP / LCP / heartbeat) — P1+

Verification

  1. На demo-coffee опубликовать меню → открыть https://erp-test.nirbi.ru/r/{slug} в Chrome — рендер появляется
  2. Запустить Chrome в kiosk-mode (F11) — никаких scrollbars, выглядит как реклама
  3. Изменить override в редакторе — через ≤30 сек kiosk-вкладка обновилась без F5
  4. Отключить интернет на машине → подождать → kiosk показывает «Нет соединения, последнее обновление в HH:MM»
  5. Включить интернет → переподключение → меню снова актуально
  6. Снять с публикации → kiosk показывает «Меню снято с публикации»

Ссылки