Эта страница переведена с английского. Читать страницу на английском
Статус: черновик, реализован в 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-effect | button, tr | Эффект в формате JSON, выполняемый по щелчку (строки: по двойному щелчку или Enter) |
aria-keyshortcuts | button | Сочетание клавиш, например Control+N |
data-zeyn-export | button | Формат экспорта таблицы на том же экране (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-colindex | th | Имя поля столбца, текущая сортировка, позиция |
data-filter, data-options | th | Вид поля фильтра (text, number, money, date, datetime, time, choice, bool) и варианты выбора [{value,label}] |
data-pin | th | start или end для закреплённых столбцов (также столбцы выбора и действий строки) |
data-edit, data-optional | th | Вид ввода редактируемого столбца, для пользователей, которые могут сохранять |
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-menu | tr | Ключ строки, версия строки, пункты контекстного меню |
data-v | td | Входное значение редактируемой ячейки |
data-zeyn-detail, aria-expanded | tr | Экран подробностей строки в виде {screen, params}; открыт ли он |
data-zeyn-expand | button в строке | Открывает или закрывает подробности, группу или потомков строки |
data-zeyn-level, aria-level, data-zeyn-path | tr в treegrid | Глубина (0 наверху) в сгруппированной или древовидной таблице; путь значений групп или ключей предков, отправляемый при открытии строки |
z-group | tr | Строка заголовка группы; z-branch-more это строка «Загрузить ещё» ветви |
data-zeyn-run | button в строке | Элемент строки для запуска (a0 это первое действие строки) |
data-agg | tfoot td | Итоги, которые показывает столбец (sum avg min max count) |
data-zeyn-bulk | div перед таблицей | Панель выбора с кнопками data-zeyn-bulk-run |
data-zeyn-menu | корень экрана | Контекстное меню экрана |
data-zeyn-native-menu="off" | html | Контекстное меню браузера отключено, кроме текстовых полей ввода |
data-zeyn-rows | tbody | Куда попадают строки |
data-zeyn-page | кнопка пейджера | prev или next |
data-zeyn-page-info | span | Текст вида 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-more | div | Сторожевой элемент бесконечной прокрутки, содержит кнопку «Загрузить ещё» |
data-zeyn-submit | form | Имя action отправки |
data-zeyn-editor | input | Клиентский редактор пользовательского типа поля |
Эффекты:
| Эффект | 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¶ms&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:content | document | Элемент с новым содержимым (навигация, ленивые панели). Оба скрипта настраивают то, что он содержит. |
zeyn:errors | форма | Ошибки 422 после того, как среда выполнения их показала. Редактор строк и пошаговые формы используют его. |
zeyn:effect | document | Эффект, которого среда выполнения не знает: { 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() (по умолчанию) | 404 | 404 | нет |
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¶ms | { tools: [{ name, description, inputSchema, kind, screen, node, writes, confirm }], texts } для экрана в поле зрения. zeyn.js регистрирует их в document.modelContext |
POST /_zeyn/mcp | JSON-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 }.