Внешние меню — Шаблоны монитора

Источник

Спецификация рендера для канала tv_screen — рекламный монитор над кассой. Открывается в Chromium kiosk-mode на мини-ПК или встроенном SoC телевизора. Описывает 3 готовых шаблона + общие технические детали (live-обновление, адаптивность, offline ZIP).


Целевое железо и установка

Железо

  • Min: Win 10/11 + Chrome / Edge с поддержкой WebView2
  • Рекомендуется: Intel N95/N100 моноблок 1080p или встроенный SoC телевизора
  • Сеть: интернет (для live URL) либо Wi-Fi для offline ZIP
  • Разрешения: 1920×1080 (16:9 FHD), 3840×2160 (16:9 4K), 1080×1920 (9:16 вертикальный)

Установка (Live URL)

Касса/мини-ПК запускает Chromium в kiosk-mode при старте Windows:

  1. В планировщике задач (taskschd.msc) создать задачу «При входе пользователя» → запустить:
    C:\Program Files\Google\Chrome\Application\chrome.exe ^
      --kiosk ^
      --noerrdialogs --disable-translate --no-first-run ^
      --disable-features=TranslateUI --disable-pinch ^
      https://erp-test.nirbi.ru/r/bar-mainscreen
    
  2. Или ярлык в shell:startup с теми же параметрами

Установка (Offline ZIP)

  1. Скачать .zip из админки → распаковать на C:\menu\
  2. Создать ярлык: chrome.exe --kiosk file:///C:/menu/index.html

Общие технические детали

Загрузка страницы

GET /r/{slug} отдаёт HTML с:

  • Embedded JSON меню в <script id="menu-data" type="application/json">{...}</script> (предотвращает дополнительный round-trip)
  • Inlined CSS в <style> (для FCP < 100ms)
  • JS bundle (~50KB gzipped) для WebSocket + рендера

Live-обновление

Приоритет 1: WebSocket /r/{slug}/stream

const ws = new WebSocket(`wss://${host}/r/${slug}/stream`);
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === 'menu_updated' && msg.version_hash !== currentHash) {
    refetchAndRerender();
  }
  if (msg.type === 'menu_unpublished' || msg.type === 'menu_archived') {
    showOfflineBanner();
  }
};

Приоритет 2: Polling fallback (если WebSocket упал и не восстанавливается):

  • Раз в 30 сек GET /r/{slug}/data
  • Сравнить version_hash с текущим — если изменился, перерисовать
  • Бесплатно — uses HTTP cache

Reconnect логика

  • При обрыве WebSocket — экспоненциальный backoff (1s, 2s, 4s, 8s, max 30s)
  • Если connection lost > 60 сек — переключиться на polling
  • При восстановлении WebSocket — отключить polling

Heartbeat

  • Каждые 25 сек сервер шлёт { "type": "ping" }
  • Клиент отвечает { "type": "pong" }
  • Если клиент не получил ping за 60 сек — считаем connection lost

Heartbeat-индикатор для админа

  • В правом нижнем углу маленький индикатор (видим в preview-mode):
    • 🟢 «Live» — WebSocket активен
    • 🟡 «Polling» — polling-режим
    • 🔴 «Offline» — нет соединения

В обычном режиме (не preview) — индикатор скрыт, чтобы не мешать рекламе.


Шаблон grid — Сетка карточек

Классическая сетка карточек товаров. Подходит для кофеен, бургерных, ресторанов с ассортиментом 20-50 позиций.

┌─ Header ─────────────────────────────────────────────┐
│              [Логотип] / [Название меню]             │
├──────────────────────────────────────────────────────┤
│  ── Кофе ────────────────────────────────────────    │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐    │
│  │ 🖼      │ │ 🖼      │ │ 🖼      │ │ 🖼      │    │
│  │ [фото]  │ │ [фото]  │ │ [фото]  │ │ [фото]  │    │
│  ├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤    │
│  │Капучино │ │ Латте   │ │ Эспрес. │ │ Раф     │    │
│  │  290 ₽  │ │  310 ₽  │ │  250 ₽  │ │  330 ₽  │    │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘    │
│                                                      │
│  ── Десерты ─────────────────────────────────────    │
│  ┌─────────┐ ┌─────────┐                            │
│  │ ...     │ │ ...     │                            │
│  └─────────┘ └─────────┘                            │
└──────────────────────────────────────────────────────┘

Параметры layout

  • Колонок в сетке per resolution:
    • 1080p (1920×1080): 4 колонки
    • 4K (3840×2160): 6 колонок
    • 9:16 (1080×1920): 2 колонки
  • Карточка: aspect-ratio: 3/4, фото вверху ~60% высоты, под ним название и цена
  • Заголовок категории: 36px, отступ сверху 24px
  • Без скролла — если товары не помещаются, авто-перелистывание категорий каждые 15 сек (плавная анимация)

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

CSS clamp + media queries:

.grid {
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: clamp(16px, 1.5vw, 32px);
}
@media (orientation: portrait) { --cols: 2; }
@media (min-width: 3000px) { --cols: 6; }
@media (min-width: 1900px) and (orientation: landscape) { --cols: 4; }

Анимации

  • При обновлении меню (live) — плавное cross-fade нового состояния поверх старого (300 ms)
  • Новые товары — fade-in
  • Удалённые товары — fade-out

Шаблон slider — Крупный фокус

Один товар на весь экран, авто-перелистывание. Подходит для рекламного режима — большая фотка + крупные цена и описание.

┌──────────────────────────────────────────────────────┐
│                                                      │
│       ┌──────────────────┐    Категория «Кофе»       │
│       │                  │                          │
│       │      🖼          │    КАПУЧИНО              │
│       │   [фото 60%]     │                          │
│       │                  │    Двойной эспрессо +    │
│       │                  │    мягкая молочная пенка │
│       └──────────────────┘                          │
│                                                      │
│                              290 ₽                  │
│                                                      │
│   ● ○ ○ ○ ○                                         │
└──────────────────────────────────────────────────────┘

Параметры

  • Тайминг автоперелистывания: 5 сек на товар (настраиваемо в external_menu в P1, в P0 hardcode)
  • Переход: slide-left 600 ms, easing ease-in-out
  • Pagination dots внизу
  • Категория — в левом верхнем углу мелким текстом

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

  • На 16:9 — фото слева, текст справа
  • На 9:16 — фото сверху, текст снизу

Шаблон list — Длинный список

Для табло «Бизнес-ланч»: список с большой ценой справа. Подходит когда товаров мало (до 12) и нужно быстро прочесть.

┌──────────────────────────────────────────────────────┐
│   Бизнес-ланч                          до 16:00      │
├──────────────────────────────────────────────────────┤
│   ── Супы ──────────────────────────────────────     │
│   Борщ                                       250 ₽   │
│   Грибной крем                               280 ₽   │
│                                                      │
│   ── Горячее ──────────────────────────────────      │
│   Котлета по-киевски                         450 ₽   │
│   Паста карбонара                            520 ₽   │
│                                                      │
│   ── Напитки ─────────────────────────────────       │
│   Морс ягодный                               150 ₽   │
└──────────────────────────────────────────────────────┘

Параметры

  • Один экран — без скролла. Если товары не помещаются — авто-перелистывание страниц 8 сек
  • Большой шрифт — minimum 32px на 1080p, 48px на 4K
  • Цена — выровнена справа, dotted leader between

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

  • На 9:16 — pure list без двух колонок
  • На 16:9 — может быть две колонки (если товаров > 8)

Поведение при ошибках

СценарийЧто показываем
Меню archived (404 от /r/{slug})Полноэкранный banner: «Меню снято с публикации» + контактная инфа франшизы (опционально)
Меню в draft (404)Аналогично
connection lost > 60s AND polling-fallback тоже падаетПолупрозрачный banner внизу: «Нет соединения. Показано последнее обновление в 14:32» — содержимое продолжает крутиться
HTTP 500 при загрузке страницыБазовая HTML «Сервис временно недоступен. Попробуйте позже» + auto-reload через 60 сек
WebSocket close с code=4040 (server says menu archived)Banner «Меню больше не доступно»

Preview-режим

/r/{slug}?preview=1 (с preview-токеном):

  • Работает даже для draft меню
  • В правом нижнем углу — индикатор 🟡 PREVIEW (видимо)
  • Не подключается к WebSocket / polling — статичный snapshot
  • Используется владельцем для проверки перед публикацией

Preview-токен:

  • Устанавливается админкой (страницей конструктора) через cookie _external_menu_preview со временем жизни 1 час
  • Cookie HttpOnly + SameSite=Lax + secure
  • Сервер при ?preview=1 проверяет cookie — если нет / истёк → 401

Offline ZIP — содержимое

GET /external-menus/{id}/export.zip отдаёт архив:

ERP-POS-menu-bar-mainscreen-2026-04-28.zip
├── index.html         ← entry point (двойной клик)
├── style.css          ← inlined fonts + theming
├── menu.json          ← snapshot данных меню
├── assets/
│   ├── images/
│   │   ├── product-1.webp
│   │   ├── product-2.webp
│   │   └── ...
│   └── fonts/
│       └── Inter-Bold.woff2
└── README.txt         ← «Откройте index.html в Chrome / Edge для отображения меню»

Особенности offline режима

  • Без auto-refresh — snapshot на момент скачивания
  • Без stop-list обновлений — товары которые в стопе на момент скачивания скрыты, дальше заморожено
  • Шаблон тот же что выбран в админке (grid / slider / list)
  • Размер архива — обычно 2-15 MB (зависит от количества товаров и размера фотографий)
  • При смене меню — нужно скачать новый ZIP

Метрики (для аналитики, P1)

В P0 — никаких. В P1 можно собирать:

  • Heartbeat’ы от мониторов (косвенно показывает что монитор работает)
  • Время рендера / FCP / LCP
  • Ошибки JS

Ссылки