В июньском релизе много всего: удобная пакетная синхронизация узлов от внешней системы до устройства - "контракты", онлайн-обработчики событий, интеграция мультимедиа-элементов с s3-хранилищем, чтобы не писать ничего при обмене документов с картинками, печатные формы в веб- и мобильном клиенте, мощная система таймеров/воркеров и многое другое.
Удобная S3 обертка для мобильного и веб-клиента для работы с файлами и изображениями
UI-элементы в NodaLogic могут работать с физическими файлами мультимедиа и в целом все UI-элементы воспринимают абсолютные пути к файлам(физическим файлам на клиентах и сервере), но тогда самому как то нужно делать и синхронизацию. Но что, если просто сразу грузить на S3-совместимое хранилище, получать постоянную ссылку и работать с ней, как с идентификатором файла. Для устройства где файл появился такая ссылка – просто указывает на физический файл (не скачивает, он уже есть). Для устройства получателя – автоматически скачивает один раз, выполняя все необходимое (показ прогресс-бара, размещение). Такая ссылка удобна в том плане, что может быть прочитана где угодно напрямую, не нагружая бекенд, без дополнительных телодвижений по синхронизации.
Для этого в бекенде есть роут(/api/s3/upload-url), который работает с s3 (авторизация с S3 хранилищем не хранится на клиентах) . Клиент обращается к бекенду (серверу NodaLogic) получает временный токен на загрузку и в ответ получает публичную ссылку (s3 в режиме public). Если нужно сделать непубличным на чтение, то есть два варианта: ограничить доступ к бакету либо чтение сделать через бекенд (ключи доступа хранятся на бекенде, бекенд скачивает, хранит у себя, раздает по авторизации). Для второго варианта надо в настройках приложения включить s3backend proxy и выбрать соответствующие настройки. Надо сказать что публичные ссылки с органичением или без интереснее в случае с s3, в режиме проксирования теряются преимущества s3 хранилища(в режиме s3 клиенты напрямую работают с хранилищем, в ). В случае проксирования не скачивания роут также надо будет поменять под это (сейчас у него проксируется upload, не download).
Если вы хостите свои решения на nmaker, вам ничего дополнительно делать не нужно. Если вы скачали и развернули NodaLogic у себя то вам нужно будет прописать настройки s3 хранилища в app.py , переменная s3. Само s3-хранилище, соответственно также будет на вашей стороне.
Визуальные элементы с поддержкой S3

В элементах, работающих с изображениями достаточно указать флаг "s3":true чтобы можно было передавать в качестве источника не локальные имена файлов а s3-ссылки. Это удобно тем, что при передаче таких данных на другой клиент(другое устройство) не надо синхронизировать картинки – элементы сделают все сами. При первом открытии такой ссылки оно скачается (и покажет прогресс-бар) при последующих – будет брать уже локально с диска. Скачивание асинхронное, с крутящимся колесиком на каждой картинке. Если это галерея или слайдер - то скачивание каждой картинки будет асинхронно независимо и на каждой будет свой прогресс-бар.

Список элементов экрана, работающих с флагом s3
Picture
ImageSlider
MediaGallery
PhotoButton
GalleryButton
Активные элементы (камера и галерея) при этом понимают, что им надо упаковать не просто в файл, а сразу еще закачать на s3 и разместить ссылку. Т.е. например в MediaGallery будет помещен не массив файлов, а массив s3 ссылок
Функции для работы с s3 для Python и NodaScript
s3put(<абсолютное имя файла>) – помещает файл в s3 (синхронно)
s3get(ссылка) – скачивает файл по ссылке и возвращает путь
Пример (на NodaScript, но точно также будет для Python)
url = s3put(_data.photo); //синхронная выгрузка в s3, на выходе ссылка
path = s3get(url); //сохранение с s3, на выходе - путь
message("Загрузили в "+string(url));Таймеры сервера и клиента

Несмотря на то, что существуют команды запуска таймеров из кода, прописывать таймеры в разделе конфигурации удобно и повышает читаемость конфигурации – зашел, и сразу видно что где работает.
В таймере сразу надо указать где он будет запускаться – на сервере или на Android-клиенте.
Для Android можно указать галочку Worker это означает что таймер работает на воркере – особом механизме Андроид, обеспечивающим бесперебойное и энергоэффективное выполнение даже когда приложение выключено. У такого режима есть платформенное ограничение Андроида – не чаще раз в 15 минут. Зато работает, как я уже написал всегда - даже после перезагрузки.
Без галки Worker это обычные таймеры, их можно делать хоть каждую секунду. Они также как и воркеры будут рабоать вне контекста экрана узла, т.е. в фоне, но для NodaLogic это не проблема – UI команды отлично работают из фона – хоть по событиям с сервера, хоть с таймеров или еще каких то общих событий.
По серверным обработчикам все тоже самое, их можно делать на любой интервал. Выполняться они будут именно на сервере а не в веб-клиенте.
По выполнению таймера сразу выполняются обработчики, привязанные к нему (принцип тот же что и с общими событиями). Для сервера это может быть только python, для Андроид – любой движок. В data обработчика кладется переменная timer_id с ИД таймера, который вызвал событие, чтобы обработчик понимал что это за событие. Для python обработчиков, еще она же кладется в input_data.
Пакетная синхронизация данных внешняя система-сервер-клиенты через "Контракты"
Не смотря на то, что существует уже несколько способов донести узлы до устройств (Rooms, система мессенджинга), они все ориентированы на быструю доставку одиночных или небольшого числа узлов одновременно, нежели на пакетную передачу сотен тысяч узлов. Эту нишу закрывают «контракты». Это инструмент пакетной синхронизации узлов, ориентированный на большие загрузки через файлы с докачкой. Также контракты берут на себя отслеживание изменений на устройствах через механизм подтверждений, т.е. другими словами отдают только то, что действительно еще не доставлено(изменилось) чтобы не качать лишние данные.
Контракты это система синхронизации и передачи узлов из внешней системы на сервер и клиенты с отслеживанием изменений. Внешняя система скидывает данные на URL и ни о чем не заботится, а сервер раздает это на устройства, принимает ack от устройств и не отправляет только те узлы, которые еще не приняты или содержат изменения.
Работает это по запросу от клиента через файлы. Т.е. клиент подписывается на получение данных по одному или нескольким контактам и тягает инфу при запуске, по расписанию или вручную.
Тут нет FCM или веб-сокета как в других подсистемах доставки NL, потому что это ориентировано больше на выкачивание больших пакетов через файлы.
Контракты не привязаны к конкретной конфигурации, одни и те же данные могут быть использованы в разных конфигурациях.
Контракт может принимать данные для конкретных классов, может данные сразу с упакованным классом (узлы без конфигураций или «самостоятельные узлы»). Со стороны клиента просто скидывается массив объектов вида [{“_id”:<внутренний id>,…}] а система сама их нормализует и превращает в удобоваримые документы системы - узлы.

Т.е. другими словами можно сказать так: внешняя система (например 1С) шлет на API контракта массив объектов когда ей это удобно. Эти объекты попадая на сервер, становятся классами, которые указаны в контракте (причем один объект может стать сразу несколькими узлами разных классов из разных конфигураций). Устройства подписываются на получение данных и периодически скачивают узлы, высылая подтверждение о получении. Если во внешней системе уже выгруженные данные изменятся и она выгрузит еще раз, то измененные узлы дойдут до всех подписанных клиентов.В целом, довольно простой механизм, ориентированный на групповую синхронизацию
Небольшая справка по видам синхронизации в NodaLogic чтобы понять какой механизм выбрать:
1) Синхронизация узлов с устройствами через механизм Rooms. Работает как широковещательная подписка через либо WebSocket либо FCM и направлена прежде всего на быструю доставку. Узлы (полученные через API или собственные) регистрируются в комнате, и рассылаются устройствам, подключенным к комнате. Комнат может быть сколько угодно. Обращение через псевдонимы. Регистрация через команду _register. Одно устройство подключено к 1й группе. Есть механизм pending, хранение сообщений
2) Синхронизация узлов через систему мессенджинга(https://habr.com/ru/articles/1034202/) – как в человекочитаемых чатах так и не отображаемая в чатах (обработчик-обработчик). Можно отправлять p2p , сообщение в группе, сообщение для конфигурации в целом. Также ориентирован на как можно более быструю доставку одного или нескольких узлов. Тут уже роутинг привязан к пользователям (и всем устройствам пользователя) , также хранение и все механизмы гарантированной доставки
3) Синхронизация датасетов. Этот механизм по сути синхронизация целиком некоеего неизменяемого набора внешних данных. Т.е. по простому – выгрузили справочник в JSON и загрузили на устройстве при запуске. Целиком, без отслеживания изменений. Чтобы это шевелилось есть индексы. Датасеты привязаны к конфигурации. Кстати, при удалении конфигуаии все что с ней связано – подчищается, в т.ч. датасеты, а контракты не связаны с конфой. В целом раньше задача передачи справочников решалась через датасеты, сейчас есть выбор какой механизм использвоать.
4) Контракты описанные в этой статье. В отличии от п.1 и п.2 – это пакетная передача узлов, ориентированная на большие данные. Но при этом не молниеносно-быстрая. Кроме того, сразу есть система отслеживания изменений. В отличии от п.3 – это узлы а не просто JSON, т.е. их можно менять, у них есть обработчики, интерфейс и т.д. Т.е. датасеты – это неизменяемые данные (только для ссылок), а тут обычные узлы.
Для тогда чтобы организовать контракт и начать принимать в него данные надо:
Зайти в Контракты на сервере, создать новый контракт.
Выбрать классы в которые он будет доставлять данные
На устройстве зайти в контракты, добавить контракт, отсканировать QR код с сервера
Контракт готов для приема и синхронизации. Его API можно скопировать из карточки и использовать в своем решении
После того как контракт скачивается возникает общее событие onContractReceived на которое при необходимости можно повесить обработчик, в _data обработчика можно получить список id принятых узлов в виде массива в ключе "nodes", а в ключ "contract_uid" - uid контракта
Можно просмотреть узлы, полученные через контракт, в интерфейса мобильного приложения просто кликнув на него в Контрактах. В целом это обычные узлы. Их можно разместить в разделах интерфейса или не размещать (а только использовать в документах как ссылки или для поиска). Если это большой справочник, может и не стоит размещать в разделах.

Контракты, если они настроены скачиваются при входе в приложение, вручную (из карточки контракта) и можно в настройках приложения задать таймер скачивания. Так как он на воркере, не чаще чем раз в 15 минут ,но зато будет качать, даже когда приложение на запущено

Печатные формы на сервере, в веб- и мобильном клиенте

Добавилась подсистема печати. Она будет расширяться и эволюционировать в плане способов формирования печатных форм, сейчас заложены основы и сделан один из способов – через HTML макет, через Jinja.
Работает это так. Каждая печатная форма – это отдельный класс узла с видом PrintForm.
В нем задается
У каких узлов будет выводиться эта печатная форма в команде Печать. Это не обязательно узлы данных. В примере есть например «Печать этикеток» - это обработка, она собирает выборку узлов.

Макет печатной формы, в котором указываются переменные, которые будут взяты из data узла (напоминаю узел у нас это не документ, к которому прицеплена печатаная форма а узел-печатная форма). Откуда возьмутся поля в data узла – ниже.

По поводу макета могу сказать следующее: 1) любая LLM влет генерирует Jinja-HTML макеты по словесному описанию. Я ни одного сам не написал. Позже добавлю ИИ-генератор, но пока скопипаситить из чата – не слишком затруднительно 2) Я вот тут в 22м году еще для SimpleUI писал как вытаскивать формы из 1С для того же jinja-макета. Ничего не поменялось – до сих пор актуально. https://infostart.ru/1c/articles/1716745/ У 1С есть табличный редактор, у меня пока нет.
Обработчик. Когда из узла, к которому печатаная форма привязана выбирают ее (или запускают на печать удаленно) то у узла-печатной формы возникает событие onInput с listener = onStartForm на которое неплохо было бы повестить обработчик, который сформирует _data – данные, которые пойдут в печатную форму, т.е. в шаблон.
self._data["rows"] = [
{
"number": 1,
"product": "Стеллаж металлический 100*40",
"quantity": 2,
},
{
"number": 2,
"product": "Крышка контейнера 100*40",
"quantity": 10,
},
{
"number": 3,
"product": "Контейнер пластиковый 500*180",
"quantity": 5,
},
]В input_data такого обработчика передается – переменная _basement_data - это data узла, который вызвал форму печати. Ну и смысл этого обработчика положить в _data своего узла то, что будет напечатано, шаблон будет обращаться к переменным узла.
Важно! Это серверный обработчик. Про мобильный клиент – ниже.
Вот обработчик, который вызывается из узла – ячейки, по сути передает просто данные ячейки, чтобы их можно было вызвать из шаблона в переменну cell
basement_data = input_data["_basement_data"] #данные узла, из которого печатается форма
self._data["cell"] = basement_dataДля большего удобства в шаблоне доступ к данным через точку, люой вложенности
<div class="label">
<div class="text">{{ cell.name }}</div>
<img class="qr" src="{{ qr(cell.name) }}" alt="QR">
</div>
И для большего удобства также добавлен хелпер qr(string) который просто выводит qr.
Итого печатную форму можно распечатать на принтер или сохранить в PDF. Для PDF должен быть установлен модуль WeasyPrint. Не не стал его включать в requirements на GitHub по причине того, что для разных ОС он ставится по разному. Если вы его не поставите, то он просто выведет ошибку.
Печать на мобильных устройствах.
Не стал пока делать локальную печать через Jinja на мобильной платформе как это было на SimpleUI. На мой взгляд если что то актуально для мобильного устройства именно локально, то это печать через ZPL и прочие принтерные языки низкого уровня на мобильный принтер, а не PDF-документы. Это реализуется по-другому и макет тут не поможет.
Но вместо этого, можно подключить печать на мобильном устройстве через сервер, причем сделано это интересно.
Для этого надо включить кнопку Show print on mobile device и в тулбаре появляется команда печати с подключёнными печатными формами

Можно просто нажать предпросмотр, отправить на принтер, подключенный с мобильного или сохранить в PDF уже на мобильном устройстве.
А можно «отправить сразу на принтер». Это имеется ввиду на сетевой принтер с сервера. Т.е. мы с мобильного обращаемся к узлу печатной-форме, передаем ей наши данные _basement_data и говорим «сформируй и отправь сразу на принтер».
Но на какой принтер?
Есть два варианта:
1. Самый простой – просто забить принтер в настройках мобильного приложения. Это может быть сетевой путь или имя для CUPS -принтера Линукс.
2. А можно этот идентификатор (сетевой путь ил CUPS-имя) засунуть в QR и наклеить на принтер (физический). Тогда такой сценарий – в помещении несколько принт-станций, пользователь подходит, запускает печать, у него автоматом запускается скан QR принтера и сразу отправляется печать на этот принтер.

Настройки прямой печати задаются в приложении – тип принтера, порядок прямой печати.
Также из обработчиков можно прост отравлять запросы на роуты
POST /client/api/print-form/pdf - отправляет PDF
POST /client/api/print-form/print - прямая печать
Скрытый текст
curl -u "user@example.com:password" \
-X POST "https://server/client/api/print-form/pdf" \
-H "Content-Type: application/json" \
-d '{
"print_form_class": "config_uid$CellLabelsPrintForm",
"_data": {
"cells": [
{"name": "A-01"},
{"name": "A-02"}
]
}
}' \
--output labels.pdf
RAW printer, по умолчанию
curl -u "user@example.com:password" \
-X POST "https://server/client/api/print-form/print" \
-H "Content-Type: application/json" \
-d '{
"print_form_class": "config_uid$CellLabelsPrintForm",
"printer_name": "192.168.1.50:9100",
"printer_type": "raw",
"_data": {
"cells": [
{"name": "A-01"},
{"name": "A-02"}
]
}
}'
curl -u "user@example.com:password" \
-X POST "https://server/client/api/print-form/print" \
-H "Content-Type: application/json" \
-d '{
"print_form_class": "config_uid$CellLabelsPrintForm",
"printer_name": "Office_Printer",
"printer_type": "cups",
"_data": {
"cells": [
{"name": "A-01"},
{"name": "A-02"}
]
}
}'Предросмотр HTML и PDF
Добавлены на мобильном клиенте команды для предпросмотра PDF и HTML в отдельном окне. Если вы каким то образом из кода сделали печатный документ и хотите его показать пользователю чтобы он отправил его на принтер или просто сохранил то в NodaScript и Python добавлены команды
PreviewPDFFile(путь к файлу) - открывает PDF файл в экране предпросмотра
PreviewHTMLFile(путь к файлу) - открывает сохраненный HTML в экране предпросмотра
PreviewHTMLString(строка) - открывает HTML-строку в режиме предпросмотра
Теги

Это совсем мелочь и в целом можно обойтись существующими возможностями разметки – выводить теги в обложках(элемент Text c обводкой), но я подумал, что удобнее иметь такой простой инструмент под рукой.
Итак в _data достаточно поместить массив тегов (массив на случай если тегов>1) в ключ "_tags":["my_tag1","my_tag2"] и в обложке появятся теги внизу. Цвет тега генерится по md5-хешу (одинаковый алгоритм в мобильном и веб-клиенте), цвет текста подстраивается
Если не надо автоматический цвет то можно задать явно в формате _tags:[{"id":"mytag","color":"#FFF555"}]
Для веб-клиента можно включить галочку «Отображать облако тегов» и добавляется облако-фильтр по тегам. Для мобильного, я посчитал это неуместным(негде разместить).
В общем работает просто как фильтр.

Онлайн – обработчики
В мобильном клиенте можно на события повесить онлайн обработчик, вместо(или вместе) с другими видами локальных обработчиков – Python или NodaScript.
Работает это просто – в HTTP-запрос передается текущее состояние data узла (или общего события) которое вызывает обработчик, запрос отправляется на некий другой бекенд там эти данные анализируются, возможно отправляются новые данные, туда же в _data, и возможно отправляются команды UI (в массив commands), которые должен выполнить клиент при этом – вывести что то на экране, показать сообщение, открыть диалог и т.д.

Это несколько противоречит концепции NodaLogic где упор больше на самостоятельные, оффлайновые объекты-узлы, чтобы не быть привязанным к онлайну и не нагружать сервер лишними событиями. Но иногда может быть полезно залезть на сервер и сделать что то там напрямую и также это сделано для совместимости с SimpleUI.
В отличии от SimpleUI, где между клиентом и сервером путешествовал строковый стек команд и переменных вперемешку, теперь и объекта отправляется _data как есть т.е. в JSON-типизированном виде, больше не надо делать преобразования в строку и обратно. А команды отправляются отдельно, в массиве commands зато с аргументами. И что важно, это массив команд, т.е. очередность выполнения задает разработчик. Думаю, это более гибкая концепция. Более подробно о составе команд можно почитать тут https://nodalogic-txt-ru.readthedocs.io/ru/latest/http.html
Пример того, что отправляется и уходит
Пример запроса
{
"_data": {
"_id": "config_uid$Goods$123",
"name": "Товар 001",
"qty": 1
},
"input_data": {
"barcode": "4601234567890"
}
}
и то что возвращается с сервера:
{
"_data": {
"_id": "config_uid$Goods$123",
"name": "Товар 001",
"barcode": "4601234567890",
"qty": 1,
"status": "checked"
},
"_commands": [
{
"command": "SetTitle",
"argument": ["Товар проверен"]
},
{
"command": "Refresh"
},
{
"command": "message",
"argument": ["Штрихкод принят"]
}
]
}
Для того, чтобы использовать онлайн обработчики вы в типе движка выбираете HTTP Request и в параметре указываете метод запроса, статический параметр запроса, который идет на сервер

В настройках приложения обязательно указать Online handlers url (Буквально запрос пойдет на него +/<имя метода в событии> ) имя пользователя и пароль.

Текстовый и триграмм-индексы

В прошлых релизах у узлов появились хеш-индексы https://nodalogic-txt-ru.readthedocs.io/ru/latest/indexes.html. Зачем они вообще? Потому что NL по сути NoSQL без индексов о скорости можно забыть. Зато с индексами все летает. В новом релизе добавились text_index - по сути аналог SQL LIKE %expression% - вхождение подстроки.
И более интересный trigram_index он уже обеспечивает нечеткий поиск, причем на хороших скоростях что на мобильном клиенте, что на сервере.

Работает это для метода findByIndex() - также как и для хеш индекса. Просто в случае с триграммами это будет список похожих
И также триграмм индексы работают в поиске по узлам в разделах если в разделе есть узлы с подключенными триграмм-индексами.
Дополнения по части UI/UX
Надпись Text с обработчиком нажатия.
Кнопка занимает много места, надпись компактнее. У кнопки правда есть анимация нажатия, в общем есть выбор. Просто разместите clicked:true и можно отлавливать события
Таже в Text добавилось свойство underline – подчеркнутый текст наряду с bold и italic
Темная тема и переключатель тем и night-ключи

Наконец то добавилась темная тема интерфейса в мобильное приложение. Платформа отрисовывается как надо, но элементы, заданные пользователем в разметке, могут содержать цвета, установленные непосредственно. И вот эти цвета в темной теме тоже должны быть приятны глазу. Поэтому для этих целей во всех элементах где
как-то задается цвет, добавлены _night-ключи – это цвет (как обычно в HEX) который будет использован для темной темы. Если он не задан, будет использован просто цвет либо цвет по умолчанию. В общем сейчас у любого элемента можно задать и цвет для светлой темы и отдельно цвет для темной темы например color и color_nigth























