Перейти к содержимому

Протокол среды выполнения

Эта страница переведена с английского. Читать страницу на английском

Статус: черновик, реализован в zeyn::server и assets/zeyn/*.js (отдаётся как /_zeyn/zeyn.js).

Это контракт между сервером Zeyn и средой выполнения в браузере. Экраны рендерятся на сервере. Среда выполнения в браузере это небольшой скрипт, который отправляет действия пользователя на сервер и применяет то, что приходит в ответ. Код приложения никогда не пишет JavaScript.

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

GET {route}?params={json} возвращает полный HTML-документ. params это JSON параметров экрана в URL-кодировке; он опускается, если у экрана нет параметров.

Маршрут с сегментами пути, например /orders/{id}, читает эти поля params из пути (/orders/42), а остальные поля из params. Значение сегмента, которое не разбирается, получает ответ 404. Значения кодируются в процентной кодировке, включая точки (%2E), поэтому URL экрана никогда не выглядит для среды выполнения как файл. Экран, отвечающий Screen::moved_to, получает 301 Moved Permanently с новым URL в Location.

Неудавшаяся страница отвечает своим статусом (404, 403, 500, ...) и экраном ошибки приложения (App::not_found, App::server_error) или короткого встроенного сообщения, внутри оболочки. Запрос с x-zeyn-partial: 1 получает только этот экран с x-zeyn-partial: 1 и x-zeyn-title, поэтому среда выполнения подставляет его без загрузки страницы. Подробности ошибок никогда не попадают в браузер.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="zeyn-csrf" content="TOKEN">
  <title>Products · Inventory</title>
  <link rel="stylesheet" href="/_zeyn/zeyn.css?v=BUILD">
  <script type="module" src="/_zeyn/zeyn.js?v=BUILD"></script>
</head>
<body>
  <a class="z-skip" href="#z-main">Skip to content</a>
  <div class="z-app">
    <nav class="z-nav" aria-label="Main">...</nav>
    <main class="z-main" id="z-main">SCREEN</main>
  </div>
  <div class="z-toasts" role="status" aria-live="polite"></div>
</body>
</html>

Меню перечисляет только те экраны, которые текущий пользователь может открыть. У текущего пункта есть aria-current="page".

Каждый скрипт и таблица стилей фреймворка в head несут ?v= и идентификатор сборки (meta[name=zeyn-build], из ZEYN_BUILD_ID или хеш исполняемого файла); theme.css несёт хеш текста темы. Запрос с текущим значением получает Cache-Control: public, max-age=31536000, immutable, поэтому тёплая загрузка страницы не запрашивает ни один из них; любой другой запрос (старая вкладка после развёртывания, без v) получает no-cache и ETag. Модули, которые среда выполнения импортирует позже, берут тот же v из URL импортирующего их скрипта.

Каждая страница загружает zeyn.js (запросы, эффекты, окна, формы, меню, сочетания клавиш и делегирование событий), nav.js, components.js и, в оболочке приложения, pro.js и enterprise.js. Остальное загружается только на страницах, которые это используют, в том числе когда nav.js подставляет экран: zeyn.js импортирует среду таблицы grid.js при первом появлении в содержимом [data-zeyn-grid] (полные страницы с таблицей называют её в <link rel="modulepreload">), а components.js импортирует catalog.js (сетки карточек, поля поиска), media.js (плееры, встраивания), files.js (загрузки), charts.js и assistant.js (панели чата или первое нажатие кнопки запуска). Их таблицы стилей остаются в head, поэтому вид не меняется, когда приходит скрипт. На панели showcase это 5 скриптов и 258 КБ (74 КБ по сети) вместо 9 скриптов и 403 КБ (116 КБ).

2. Разметка экрана

Корень каждого экрана несёт его имя и параметры:

<section class="z-screen" data-zeyn-screen="product_list" data-zeyn-params="null" aria-labelledby="product_list-title">

Окна используют class="z-window" role="dialog" aria-modal="true" и стиль --z-width.

Каждый элемент тела обёрнут в элемент узла. Его путь это адрес, используемый конечными точками ниже. Контейнеры (Row, Stack, Panel, Card, Tabs, Split) так же оборачивают каждого потомка, а путь потомка добавляет его индекс: третий потомок первого потомка элемента тела 1 имеет путь "1.0.2".

<div class="z-node" data-zeyn-node="1">
  <div class="z-node" data-zeyn-node="1.0">...</div>
</div>

Конечные точки принимают node как строку с точками или, для элементов тела, как простое число (0). Пути содержат не более 16 уровней из цифр; всё остальное отклоняется с 422.

3. Атрибуты, которые читает среда выполнения

АтрибутУЗначение
data-zeyn-effectbutton, trЭффект в формате JSON, выполняемый по щелчку (строки: по двойному щелчку или Enter)
aria-keyshortcutsbuttonСочетание клавиш, например Control+N
data-zeyn-exportbuttonФормат экспорта таблицы на том же экране (csv, xlsx)
data-zeyn-gridкорень таблицыПомечает таблицу; также есть data-zeyn-mode, data-zeyn-paging, data-zeyn-page-size, data-zeyn-total, data-zeyn-next, data-zeyn-offset
data-field, aria-sort, aria-colindexthИмя поля столбца, текущая сортировка, позиция
data-filter, data-optionsthВид поля фильтра (text, number, money, date, datetime, time, choice, bool) и варианты выбора [{value,label}]
data-pinthstart или end для закреплённых столбцов (также столбцы выбора и действий строки)
data-edit, data-optionalthВид ввода редактируемого столбца, для пользователей, которые могут сохранять
data-zeyn-texts, data-zeyn-columns, data-zeyn-viewкорень таблицыЛокализованные тексты элементов управления, все столбцы [{field,label,hidden,pin,width}], сохранённое представление
data-zeyn-viewsкорень таблицыserver, когда представления сохраняются на сервере
data-zeyn-version-field, data-zeyn-exportableкорень таблицыИмя поля версии строки для редактирования в ячейке; csv, когда таблицу можно экспортировать
data-zeyn-key, data-zeyn-version, data-zeyn-menutrКлюч строки, версия строки, пункты контекстного меню
data-vtdВходное значение редактируемой ячейки
data-zeyn-detail, aria-expandedtrЭкран подробностей строки в виде {screen, params}; открыт ли он
data-zeyn-expandbutton в строкеОткрывает или закрывает подробности, группу или потомков строки
data-zeyn-level, aria-level, data-zeyn-pathtr в treegridГлубина (0 наверху) в сгруппированной или древовидной таблице; путь значений групп или ключей предков, отправляемый при открытии строки
z-grouptrСтрока заголовка группы; z-branch-more это строка «Загрузить ещё» ветви
data-zeyn-runbutton в строкеЭлемент строки для запуска (a0 это первое действие строки)
data-aggtfoot tdИтоги, которые показывает столбец (sum avg min max count)
data-zeyn-bulkdiv перед таблицейПанель выбора с кнопками data-zeyn-bulk-run
data-zeyn-menuкорень экранаКонтекстное меню экрана
data-zeyn-native-menu="off"htmlКонтекстное меню браузера отключено, кроме текстовых полей ввода
data-zeyn-rowstbodyКуда попадают строки
data-zeyn-pageкнопка пейджераprev или next
data-zeyn-page-infospanТекст вида 1-50 of 230, построенный из локализованных шаблонов в корне таблицы
data-zeyn-text-range, data-zeyn-text-range-total, data-zeyn-text-loadedкорень таблицыЛокализованные шаблоны: {from}-{to}, {from}-{to} of {total}, {count} rows loaded
data-zeyn-moredivСторожевой элемент бесконечной прокрутки, содержит кнопку «Загрузить ещё»
data-zeyn-submitformИмя action отправки
data-zeyn-editorinputКлиентский редактор пользовательского типа поля

Эффекты:

ЭффектJSONСреда выполнения делает
Open window{"effect":"open_window","screen":"product_form","params":{...}}Загружает экран (раздел 4.1) в модальный <dialog>
Navigate{"effect":"navigate","screen":"...","params":{...}}Загружает /_zeyn/route/{screen}?params=, который перенаправляет на страницу, в форме с путём, если у её маршрута есть сегменты
Toast{"effect":"toast","message":"Saved"}Показывает уведомление на 4 секунды
Close and refresh{"effect":"close_and_refresh"}Закрывает верхний диалог и перезагружает все таблицы на экране под ним

4. Конечные точки

Все запросы POST отправляют JSON, должны нести заголовок x-zeyn-csrf с токеном из метатега zeyn-csrf, иначе отклоняются с 403.

Публичный экран, открытый без сеанса, приходит с пустым метатегом zeyn-csrf и без Set-Cookie, чтобы его можно было кэшировать. Перед своим первым POST среда выполнения вызывает GET /_zeyn/csrf, который отвечает { "token": "..." } с Cache-Control: no-store, устанавливает cookie zeyn_csrf, если у браузера её нет, и отклоняет запросы, у которых Sec-Fetch-Site не равен same-origin. Токен попадает в метатег; если вызов не удался, POST уходит без токена и отклоняется как любой другой. Ничего не повторяется молча.

Навигация внутри приложения (nav.js) запрашивает экран с x-zeyn-partial: 1 и получает только его HTML, с заголовком страницы в x-zeyn-title. Когда экран задаёт теги head (описание, канонический URL, изображение предпросмотра, robots, JSON-LD), тело начинается с <template data-zeyn-head>, содержащего их; nav.js удаляет все элементы [data-zeyn-head] из head документа, переносит туда теги шаблона и отбрасывает шаблон. Экран без тегов head не отправляет шаблон, поэтому старые теги только удаляются.

4.1 Фрагмент экрана

GET /_zeyn/screen/{screen}?params={json} возвращает только HTML экрана, без оболочки страницы. Используется для окон и подробностей строк: открытие строки с data-zeyn-detail загружает её экран в новый tr.z-detail прямо под ней (одна ячейка на всю строку, содержащая role="region"), а закрытие удаляет эту строку. Разрешение экрана проверяется как у любого экрана; таблицы внутри подробностей обращаются к серверу с собственным именем и параметрами экрана подробностей.

4.2 Строки таблицы

POST /_zeyn/grid

{
  "screen": "product_list",
  "params": null,
  "node": 0,
  "query": {
    "page": { "type": "offset", "offset": 0, "limit": 50 },
    "sort": [{ "field": "code", "desc": false }],
    "filters": [{ "field": "name", "contains": "tea" }, { "field": "price", "from": "10", "to": "99.90" }],
    "with_total": true,
    "aggregates": [{ "field": "price", "op": "sum" }]
  },
  "view": { "columns": [{ "field": "code", "width": 140, "pin": "start" }, { "field": "name" }] },
  "head": false
}

Фильтры могут задавать contains, starts_with, equals, from, to, any_of и is (см. docs/grid.md). view.columns это раскладка столбцов, в которой рендерятся строки; при head: true ответ также содержит head, строку заголовка.

Для бесконечной прокрутки page имеет вид { "type": "after", "cursor": "...", "limit": 100 }, а cursor равен null для первой порции.

Ответ 200:

{ "rows": "<tr>...</tr>", "foot": "<tr>...</tr>", "head": null, "count": 50, "total": 230, "next": "50" }

foot это строка итогов, если итоги были запрошены, иначе null.

total и next могут быть null. Среда выполнения заменяет строки для таблиц с постраничным выводом и добавляет их при бесконечной прокрутке.

Сгруппированные и древовидные таблицы (<table role="treegrid">) отправляют path вместе с POST /_zeyn/grid, чтобы загрузить ветвь: значения групп (или ключи предков) открываемой строки, как они указаны в её data-zeyn-path. Ответ имеет ту же форму; rows содержит строки групп или строки на уровень глубже, а total их считает. Среда выполнения вставляет их после открытой строки и удаляет все более глубокие строки, следующие за ней, когда строка закрывается. Путь длиннее, чем уровни группировки таблицы (или 32 в дереве), даёт 404.

4.2.1 Другие конечные точки таблицы

Конечная точкаТелоДелает
POST /_zeyn/grid/view{screen, params, node, view}Сохраняет представление пользователя (null удаляет его). 204; 404 без хранилища представлений
POST /_zeyn/grid/run{screen, params, node, key, item}Запускает элемент строки (a0 действие строки, m2.1 путь в меню). Сервер загружает строку по ключу и строит входные данные. {effects}
POST /_zeyn/grid/bulk{screen, params, node, action, keys, all, query}Запускает массовое действие с выбранными ключами или, при all, со всеми строками, подходящими под query.filters. {effects, count}
POST /_zeyn/grid/edit{screen, params, node, key, values, view}Сохраняет редактирование в ячейке; values содержит изменённые столбцы и версию строки в том виде, как их отправляет форма. {row}; 403 для столбцов, которые нельзя редактировать, 409, 422
GET /_zeyn/grid/export?screen&params&node&q={sort,filters,columns}Отдаёт потоком все подходящие строки в виде CSV
POST /_zeyn/menu{screen, params, item}Запускает пункт-действие контекстного меню экрана

4.2.2 Виртуальная прокрутка

Таблицы с data-zeyn-paging="virtual" показывают один прокручиваемый список по всем строкам, сколько бы их ни было. Сервер рендерит первый блок (data-zeyn-page-size строк, смещение 0) с data-zeyn-total: первые 64 строки лежат в таблице, остальные в <template data-zeyn-rows-later> после неё, а при 40 столбцах и более в шаблоне лежат все строки. У корня таблицы также есть data-zeyn-row-height (в пикселях) и переменная CSS --z-row-height.

Среда выполнения:

  • Помещает тело таблицы в прокручиваемый контейнер, внутренний распорный элемент которого имеет высоту total * rowHeight пикселей, но не более 8 000 000 пикселей. Выше предела положение прокрутки сопоставляется с индексом строки по пропорции (row = scrollTop / (spacerHeight - viewport) * (total - visibleRows)), поэтому 1 000 000 строк по-прежнему доходят до конца.
  • Хранит в DOM только видимые строки плюс 20 строк выше и ниже, позиционируя их через transform: translateY(...). У строк фиксированная высота; длинный текст обрезается многоточием.
  • Загружает блоки по data-zeyn-page-size строк через POST /_zeyn/grid с page: { "type": "offset", "offset": blockStart, "limit": blockSize } и with_total: false после первого вызова. Держит в кэше до 20 блоков и не более 100 000 ячеек (вытесняется давно не используемый; 7 блоков для таблицы с 63 столбцами, но не меньше 3), причём блоки, сохранённые для reload, считаются в том же лимите и вытесняются первыми, заранее загружает следующий блок в направлении прокрутки и отменяет запросы блоков, ушедших из поля зрения (AbortController).
  • Никогда не очищает видимые строки, пока блок загружается. Отсутствующие строки показывают строку-заполнитель с aria-busy="true" до прихода их блока, поэтому экран не мигает.
  • Изменение сортировки и фильтров сбрасывает кэш и прокручивает к началу.
  • Клавиатура: клавиши со стрелками, Page Up, Page Down, Home и End перемещают активную строку и прокручивают её в поле зрения. У таблицы aria-rowcount = total, а у каждой строки aria-rowindex.
  • Текст позиции (data-zeyn-page-info) использует data-zeyn-text-range-total.

4.3 Отправка формы

POST /_zeyn/form

{ "screen": "product_form", "params": { "id": 7 }, "node": 0, "values": { "code": "P-001", "price": "1.234,56", "active": true } }

Значения отправляются так, как их ввёл пользователь: строки для полей ввода и списков, булевы значения для флажков. Сервер разбирает их по типу поля.

Сервер исходит из модели, загруженной экраном, и перезаписывает только те поля, которые есть в форме и не только для чтения. Остальные значения от клиента игнорируются. Ключи никогда не приходят от клиента.

Ответы:

  • 200 { "effects": [ ... ] }: сохранено; выполните эффекты по порядку.
  • 422 { "errors": [ { "field": "code", "rule": { "rule": "required" }, "message": "Code is required." } ] }: покажите каждое сообщение под его полем (#{scope}-{field}-error), установите aria-invalid="true" на поле ввода и переведите фокус на первое неверное поле. Ошибки с пустым field показываются в верхней части формы.

4.4 Ошибки

Любая конечная точка может ответить:

СтатусТелоЗначение
403{ "error": "forbidden", "message": "..." }Нет разрешения, guard отказал или неверный токен CSRF
404{ "error": "not_found", "message": "..." }Неизвестный экран или узел
409{ "error": "conflict", "message": "..." }Кто-то другой изменил запись
422см. 4.3Валидация
504{ "error": "timeout", "message": "..." }Вызов занял больше времени, чем допускает сервер
500{ "error": "internal", "message": "An unexpected error occurred." }Подробности только в журнале сервера

Среда выполнения показывает message как уведомление об ошибке.

4.5 Язык

POST /_zeyn/locale с { "locale": "tr" } сохраняет выбор в cookie zeyn_locale и отвечает 204. Среда выполнения перезагружает страницу. Без cookie сервер выбирает по Accept-Language, затем по языку приложения по умолчанию. <html lang> всегда соответствует выбранной локали.

Оболочка рендерит выбор языка (select[data-zeyn-locale]), когда у приложения больше одной локали.

4.6 Тема

Оболочка рендерит выбор темы (select[data-zeyn-theme-picker]), если только приложение не скрывает его или не предлагает единственную тему. Каждый вариант несёт data-scheme (light, dark, пусто для системной) и data-custom для тем приложения. При изменении среда выполнения сразу устанавливает data-theme и data-zeyn-theme на <html> и метатег color-scheme, а затем отправляет POST /_zeyn/theme с { "theme": "dark" }. Сервер отвечает 204 с cookie zeyn_theme или 404 для темы, которую приложение не предлагает. Страницы рендерятся с темой из этой cookie, поэтому нет ни перезагрузки, ни вспышки. См. docs/theming.md.

4.7 Конечные точки корпоративных функций

enterprise.js и enterprise.css загружаются рядом со средой выполнения. Подробности в docs/enterprise.md.

Конечная точкаНазначение
POST /_zeyn/reveal {screen, params, node, field, key?}Исходное значение скрытого поля, {value}; требует разрешения на снятие маски, всегда попадает в аудит
POST /_zeyn/reason {screen, reason}Причина для экрана, помеченного reason; 204 с cookie HttpOnly
POST /_zeyn/import {screen, params, node, csv, commit}Предпросмотр CSV (commit: false) или импорт
POST /_zeyn/keepalive«Остаться в системе» после предупреждения о простое; 204
GET /_zeyn/health, GET /_zeyn/readyПроверки живости и готовности; health не ограничивается по частоте, readiness только с probe_ips
GET /_zeyn/jobsСтатус заданий для пользователей с разрешением на задания

Каждый ответ несёт x-zeyn-build. Отказы, на которые реагирует среда выполнения: 428 reason_required (запрашивает причину и повторяет запрос) и 401 reauth_required (просит пользователя войти заново и повторяет запрос).

5. Безопасность

  • Каждый ответ устанавливает Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data: blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none', X-Content-Type-Options: nosniff и Referrer-Policy: same-origin. App::csp добавляет перечисленные хосты https в img-src, media-src (и blob:), frame-src, connect-src и font-src; остальные директивы никогда не меняются. Текст заголовка строится один раз при запуске.
  • Никаких встроенных скриптов и встроенных обработчиков событий. Встроенные атрибуты style используются только для пользовательских свойств CSS (--z-width, --z-flex) и фиксированной ширины столбцов, поэтому политика добавляет style-src-attr 'unsafe-inline'. Скрипты остаются под строгим ограничением.
  • Токен CSRF хранится в cookie HttpOnly, SameSite=Strict и сравнивается за постоянное время с заголовком x-zeyn-csrf.
  • Страницы, форма totp и GET /_zeyn/csrf никогда не несут токен в том виде, в каком он лежит в cookie: каждый ответ получает свежую случайную маску, за которой идёт токен XOR маска (128 шестнадцатеричных цифр), чтобы gzip не мог его раскрыть (BREACH). Сервер снимает маску перед сравнением и по-прежнему принимает обычный токен из 64 цифр.
  • Динамические ответы от 1 КБ (страницы, частичные экраны, блоки таблиц, экспорт) сжимаются gzip на уровне 2, когда браузер это принимает, с Vary: Accept-Encoding. Никогда не сжимаются: ответы, у которых уже есть Content-Encoding (ресурсы фреймворка, сжатые один раз и кэшированные), диапазоны (Content-Range и любой запрос с Range), видео, аудио, изображения, кроме SVG, application/octet-stream, архивы и PDF, text/event-stream, application/x-ndjson (поток ассистента) и обновления до WebSocket.
  • Каждый вызов проверяет по порядку: аутентификацию, доступ к экрану, guard feature, доступ action узла, а затем запускает interceptor и action.

6. Поведение среды выполнения

  • Без JavaScript страницы всё равно рендерятся с первой страницей каждой таблицы. Формам и постраничному выводу нужна среда выполнения.
  • Клавиатура: клавиши со стрелками перемещаются между строками таблицы, Enter открывает строку, Escape закрывает верхний диалог, сочетания клавиш из aria-keyshortcuts работают везде, кроме текстовых полей ввода. В таблицах: пробел выбирает строку, F2 редактирует её, Ctrl+Z отменяет последнее редактирование, Shift+F10 или клавиша контекстного меню открывает меню строки или заголовка, Shift+щелчок сортирует по нескольким столбцам, Вправо и Влево (зеркально на страницах с письмом справа налево) открывают и закрывают подробности, группу или потомков строки, а Влево на дочерней строке переходит к её родителю. Вправо на строке переходит в её ячейки: стрелки, Home, End, Ctrl+Home, Ctrl+End, Tab и Shift+Tab перемещают курсор ячейки, Shift расширяет диапазон (aria-selected на ячейках), Ctrl+C копирует диапазон как текст с табуляцией, а Ctrl+V вставляет в редактируемые ячейки, по одному POST /_zeyn/grid/edit на изменённую строку. Таблицы с 40 и более столбцами держат на странице только столбцы в поле зрения (colgroup с фиксированной шириной, распорки td.z-col-gap с colspan, aria-colindex на ячейках).
  • Щелчок правой кнопкой открывает меню Zeyn там, где оно определено (заголовки, строки с data-zeyn-menu, экраны с data-zeyn-menu); везде остальное остаётся собственное меню браузера.
  • Фокус переходит в диалог при его открытии и возвращается к открывшему элементу при закрытии.
  • Клиентские проверки (required, minlength, maxlength, min, max, type="email") выполняются перед отправкой через API проверки ограничений браузера. Сервер всегда проверяет снова.

7. Библиотека компонентов

Компоненты zeyn::ui отдаются ещё двумя ресурсами, подключаемыми в head страницы после среды выполнения: /_zeyn/components.js и /_zeyn/components.css. Как и среде выполнения, им не нужен вход в систему.

7.1 Конечные точки

Все это POST с JSON и заголовком CSRF, и они сначала проверяют доступ к экрану и guard feature.

Конечная точкаТелоОтвет
/_zeyn/node{ screen, params, node }HTML одного узла с первой страницей его таблиц. Используется ленивыми вкладками.
/_zeyn/data{ screen, params, node, key, input }JSON из источника данных key узла: варианты поиска (key это имя поля), потомки дерева (key пуст), проверки шагов (key равен _step). Разрешение action источника проверяется, и interceptor выполняются, как для таблиц. Входные данные, не подходящие action, отклоняются с 404.
/_zeyn/parse{ kind, text, label }Как сервер читает введённую дату или время на языке, в часовом поясе и формате часов пользователя: { ok, text, iso, utc, zone } или { ok: false, message }. kind это date, time, datetime, local или presets (сегодня и быстрые диапазоны).

7.2 Значения, которые отправляют формы

  • Lookups отправляют идентификаторы как текст: "7" или "3,7,9" для нескольких.
  • Lines отправляют "{field}": "rows" и по одному значению на ячейку как "{field}.{line}.{column}", плюс "{field}.{line}._i", индекс загруженной строки, из которой возникла эта строка (пусто для новых строк). Сервер восстанавливает каждую строку из её загруженной строки, поэтому меняются только показанные редактируемые столбцы.
  • Ошибки валидации строки используют { "field": "lines", "rule": { "rule": "line", "value": { "row": 0, "field": "quantity" } } }.

7.3 События

СобытиеУПодробности
zeyn:contentdocumentЭлемент с новым содержимым (навигация, ленивые панели). Оба скрипта настраивают то, что он содержит.
zeyn:errorsформаОшибки 422 после того, как среда выполнения их показала. Редактор строк и пошаговые формы используют его.
zeyn:effectdocumentЭффект, которого среда выполнения не знает: { effect, source, run }. run(next) запускает другой эффект. Используется confirm.

Эффект {"effect":"confirm","message":"...","title":"...","ok":"...","cancel":"...","then":{...}} спрашивает в модальном диалоге предупреждения и выполняет then только после подтверждения пользователем.

7.4 Медиа

Video и Audio отдаются через /_zeyn/media.js и /_zeyn/media.css, подключаемые после библиотеки компонентов. См. docs/media.md.

Конечная точкаДелает
GET /_zeyn/media/{source}/{id}Файл зарегистрированного источника медиа с запросами диапазонов: 200, 206, 304, 416.
GET /_zeyn/media/position?source=&id={ at, kept }, где остановился вошедший в систему пользователь; kept равен false, когда источник не хранит позиции (тогда плеер использует локальное хранилище).
POST /_zeyn/media/position{ source, id, at } с заголовком CSRF сохраняет её: 204, 422 для неверной позиции.

7.5 Острова (islands)

С feature islands экран, содержащий div[data-zeyn-island], отдаётся с <meta name="zeyn-islands"> (имя острова в URL { js, wasm }), /_zeyn/islands.js и политикой с 'wasm-unsafe-eval'; остальные страницы не меняются. Частичный ответ, содержащий остров, несёт x-zeyn-islands: 1, и nav.js загружает его целиком, если в документе нет политики для островов.

Атрибут или конечная точкаЗначение
data-zeyn-island, data-zeyn-load, data-zeyn-propsИмя острова, visible, interaction или idle, JSON props
data-zeyn-island-fallbackЗапасное содержимое, скрывается после монтирования острова
data-zeyn-island-stateУстанавливается загрузчиком: loading, ready, failed
GET /_zeyn/islands/{file}Собранный файл из манифеста, кэшируется на год
POST /_zeyn/data с key = удалённое имяТипизированный вызов из острова со всеми проверками из 7.1

См. docs/islands.md.

7.6 Реальное время

С feature realtime элемент div[data-zeyn-live="topic"] помечает узлы, которые обновляются, когда сервер уведомляет тему. Страницы, содержащие такой элемент, подключают /_zeyn/live.js; частичный ответ, содержащий его, несёт x-zeyn-live: 1, и nav.js импортирует скрипт. zeyn:reload на элементе перезагружает таблицы внутри него на месте.

Конечная точкаДелает
GET /_zeyn/live/topics?t=a,b&csrf=WebSocket; текстовые кадры { "topic": "a" }
GET /_zeyn/live/doc/{kind}/{doc}?csrf=WebSocket; двоичные кадры 0x01 + обновление Y (v1) в обе стороны

Рукопожатия проверяют origin, токен CSRF по cookie, вошедшего пользователя и разрешения до переключения протокола. См. docs/realtime.md.

Плеер отправляет отменяемое событие zeyn:media-adaptive ({ media, kind, url, hls, dash }) на своём корне перед переходом к прогрессивным файлам; библиотека потоковой передачи вызывает preventDefault(), чтобы взять управление на себя.

8. Оболочка уровня настольного приложения

/_zeyn/pro.js и /_zeyn/pro.css загружаются после библиотеки компонентов. Как и среде выполнения, им не нужен вход в систему.

8.1 Команды и палитра

Команды объявляются в Rust (Command::open, Command::navigate, Command::effect, Command::action) и регистрируются через Feature::command. Оболочка рендерит по одной скрытой кнопке на каждую команду, которую пользователь может запустить (guard feature пропускают и доступ команды разрешает), внутри div[data-zeyn-commands]:

АтрибутЗначение
data-zeyn-commandИдентификатор команды
data-zeyn-effectЭффект команды-эффекта, запускаемый средой выполнения по щелчку
data-zeyn-command-runИдентификатор команды-action, запускаемой через POST /_zeyn/command
aria-keyshortcutsНормализованное сочетание клавиш (Control+Alt+N); обработка сочетаний средой выполнения нажимает кнопку
data-hint, data-iconСочетание клавиш в том виде, как его видят пользователи, имя значка
data-text-* на контейнереЛокализованные тексты палитры

App::check отклоняет сочетания клавиш, которые нельзя прочитать, сочетания из одного символа (WCAG 2.1.4), собственное сочетание палитры, сочетания или идентификаторы, используемые двумя командами, сочетания, которые браузер никогда не передаёт странице (BROWSER_RESERVED: Ctrl+N, Ctrl+T, Ctrl+W, Ctrl+Tab, Alt+F4 и их формы для macOS) и, если команда не вызывает .override_browser(), сочетания, которые использует браузер (BROWSER_DEFAULTS: печать, сохранение, поиск, адресная строка, перезагрузка, масштаб, инструменты разработчика, копирование и вставка, от F1 до F12). Кнопки панели инструментов следуют тем же правилам во время выполнения: такое сочетание не привязывается, а причина записывается в журнал. Свободные комбинации это Alt+Shift+<буква> и Ctrl+Alt+<буква>.

Alt+Shift+P (Option+Shift+P в macOS) по умолчанию открывает палитру; App::palette_shortcut("Ctrl+Shift+P") меняет его, в том числе на клавишу, которую использует браузер, сознательно. Сочетание находится в data-palette-keys, а буквы сопоставляются по физической клавише, поэтому Option и AltGr его не ломают. Палитра также открывается из button[data-zeyn-palette-open] в меню. Это модальный <dialog> с ARIA combobox и listbox. Она перечисляет команды приложения, кнопки панели инструментов, команды панели меню и пункты контекстного меню текущего экрана (верхнего окна, если оно открыто) и экраны в меню. Сопоставление нечёткое и игнорирует регистр и диакритические знаки; восемь последних запущенных команд идут первыми (ключ localStorage zeyn.palette.recent). Escape закрывает её, и фокус возвращается туда, где он был.

Конечная точкаТелоОтвет
POST /_zeyn/command{ command }Запускает команду-action с входными данными, зафиксированными на сервере: CSRF, guard feature, разрешение action и interceptor. { effects }; 404 для неизвестных идентификаторов и команд-эффектов
POST /_zeyn/search{ query }Запускает каждый Feature::search, который пользователь может запустить (CSRF, guard feature, разрешение action, interceptor), с запросом, обрезанным до 100 символов. { hits: [{ label, group, url }] }, не более 20, только пути приложения

Когда у контейнера есть data-search, палитра отправляет запрос через 150 мс после последней клавиши (два символа и более) и перечисляет найденное под командами. Отрисовывается только ответ на последний запрос. Выбор результата отправляет zeyn:navigate с базовым путём перед url; #anchor в нём прокручивает к этому элементу после замены экрана.

Среда выполнения слушает zeyn:run на document, чтобы другие скрипты выполняли работу через неё: { effects, source } запускает эффекты, { screen, item } запускает пункт контекстного меню экрана, { error } показывает уведомление об ошибке.

8.2 Панель меню, панель инструментов, инспектор свойств, строка состояния

РазметкаПоведение
ul[role=menubar][data-zeyn-menubar]Menubar по W3C APG. Верхние пункты это button[role=menuitem][aria-haspopup=menu] с перемещаемой точкой табуляции; каждый управляет ul[role=menu], который hidden, пока закрыт. Пункты несут атрибуты команд из 8.1. Влево и Вправо переходят между меню, Вниз, Enter и пробел открывают, Вверх открывает на последнем пункте, Home и End переходят к краям, буква переходит к следующему пункту, начинающемуся с неё, Escape закрывает и возвращает на панель, Tab и щелчок снаружи закрывают. Меню и пункты, которые пользователь не может запустить, не рендерятся.
div[role=toolbar][aria-orientation=vertical][data-zeyn-toolbox]Группа переключателей button.z-tool[aria-pressed]. Стрелки, Home и End перемещают точку табуляции; щелчок, Enter или пробел включает один инструмент, отправляет zeyn:tool (detail.tool) и записывает data-text-selected в строку состояния.
section.z-inspectorФорма с группами Form::section (details.z-form-section с телами role=group), сохраняемая через POST /_zeyn/form по пути узла инспектора. Разделы с неверными полями открываются перед проверками браузера и после 422.
div[data-zeyn-statusbar]Элементы в начале и конце и [data-zeyn-status-live] (role=status, aria-live=polite). zeyn:status (detail.text) от любого элемента экрана заменяет живой текст.

8.3 Закрепляемые панели

div.z-dock[data-zeyn-dock] это сетка из четырёх section.z-dock-area[data-area] (left, center, bottom, right) и трёх разделителей div.z-dock-bar[role=separator][data-edge]. Размеры это пользовательские свойства --z-dock-left, --z-dock-right (процент ширины) и --z-dock-bottom (процент высоты), от 10 до 60. У каждой области есть role=tablist из перетаскиваемых button.z-dock-tab[data-panel], кнопка параметров и панели role=tabpanel, содержащие дочерние узлы (пути как в любом контейнере; при перемещении панели они не меняются). Боковые области без открытых панелей скрыты.

Клавиатура: Влево и Вправо (зеркально на страницах с письмом справа налево), Home и End переходят между открытыми вкладками области; Delete закрывает панель в фокусе; Shift+F10 или клавиша контекстного меню открывает параметры области (переместить в другую область, закрыть, показать закрытые панели); стрелки на разделителе меняют размер на 5 процентов, Home и End переходят к пределам. Пункт панели меню, созданный через MenuList::panels, перечисляет каждую панель как menuitemcheckbox. Изменения объявляются в [data-zeyn-dock-live].

Конечная точкаТелоОтвет
POST /_zeyn/dock{ screen, params, node, save: false }{ layout }: сохранённая раскладка вошедшего пользователя или null
POST /_zeyn/dock{ screen, params, node, save: true, layout }204; layout: null удаляет её

layout это { sizes: { left, right, bottom }, panels: [{ id, area, open, active }] } в порядке вкладок. Сервер проверяет CSRF, доступ к экрану и guard feature, оставляет только собственные панели дока (каждую один раз) и ограничивает размеры, затем сохраняет раскладку в хранилище представлений приложения (App::datagrid_views) по арендатору, экрану и узлу. Без хранилища конечная точка отвечает 404, а без пользователя 403; тогда браузер хранит раскладку в localStorage.

9. ИИ-агенты: MCP и WebMCP

Внешние ИИ-агенты попадают в приложение через две двери, обе по умолчанию закрыты и включаются через App::mcp:

НастройкаКонечная точка MCPИнструменты WebMCPЗапись
Mcp::off() (по умолчанию)404404нет
Mcp::read_only()включенавключенынет
Mcp::on()включенавключенысохранение форм и действия API, которые пишут

.server(false) и .webmcp(false) закрывают одну из дверей. Новые проекты из zeyn new начинаются с Mcp::read_only().

Почему закрыто: конечная точка позволяет любому обладателю действительного заголовка Authorization управлять приложением через модель, а моделью можно управлять с помощью данных, которые она читает. Фреймворк готовит каждое приложение к агентам (инструменты генерируются из кода, писать ничего не нужно), но решение о том, открывать ли эту дверь и разрешать ли агентам запись, принимают те, кто эксплуатирует приложение. Режим только для чтения включается одной строкой.

9.1 Инструменты

Инструменты строятся из приложения для вошедшего пользователя:

  • один инструмент поиска на каждую таблицу ({screen}_{node}_search, вход: DataQuery) и, при записи, один инструмент сохранения на каждую форму ({screen}_{node}_save, вход: значения формы как введены) каждого маршрутизируемого экрана, который открывается без params;
  • один инструмент на каждый опубликованный action API (api_{feature}_{action}). Action, входом которого является DataQuery, читает; любой другой action API считается записью.

Описание строится из заголовка экрана, первого абзаца комментария /// экрана и action и их указаний ai = "...". Таблицы и формы, которыми пользователь не может пользоваться, и экраны и actions с пометкой ai = false никогда не перечисляются и отклоняются при вызове.

#[action(confirm)] помечает action, который модель может запустить только после того, как пользователь подтвердит его в приложении. /_zeyn/mcp такие инструменты ни перечисляет, ни запускает (на этом канале никто не может подтвердить). Инструменты WebMCP спрашивают в модальном диалоге (zeyn:confirm) и затем отправляют confirmed: true вместе с формой; встроенный ассистент спрашивает в своём чате (см. docs/assistant.md).

9.2 Конечные точки

Конечная точкаДелает
GET /_zeyn/tools?screen&params{ tools: [{ name, description, inputSchema, kind, screen, node, writes, confirm }], texts } для экрана в поле зрения. zeyn.js регистрирует их в document.modelContext
POST /_zeyn/mcpJSON-RPC: initialize, tools/list, tools/call. Нужен заголовок Authorization и вошедший пользователь
POST /_zeyn/form с via_agent: trueСохранение формы через WebMCP; нужен Mcp::on() с WebMCP. confirmed: true только после подтверждения пользователем

Каждый вызов агента выполняется от имени пользователя: доступ к экрану, guard, разрешение action, interceptor, ограничения частоты и лимит времени. Вызовы агентов всегда попадают в аудит (via_agent: true), а скрытые поля для них всегда остаются скрытыми, даже если пользователь может их раскрыть. OpenAPI помечает каждую операцию x-zeyn-ai: false для ai = false, иначе { hint, confirm, writes }.