# SparkU API — свои интеграции > SparkU выводит данные скриптов и программ в вырез MacBook: скрипт отправляет значения через локальный UNIX-сокет (HTTP/1.1 или CLI `sparku`), SparkU рисует их индикатором, строкой или карточкой, передаёт обратно нажатия кнопок и может задать человеку вопрос. ## Обзор > Что такое открытый API SparkU и для чего скрипты выводят данные на островок. ### Что это Открытый API SparkU — способ показать на островке (вырезе экрана) данные, которые собирает **ваш собственный скрипт или программа**: прогресс сборки, метрики сервера, таймер, очередь задач. Скрипт не рисует интерфейс — он только присылает значения в готовые слоты, которые вы сами настроили заранее. Типичные сценарии: - **CI/деплой** — этап пайплайна, ветка, ссылка на сборку, кнопка «Открыть логи». - **Сборки** — процент, стадия, сколько осталось. - **Мониторинг сервера** — CPU, память, задержка, статус «работает / тормозит / лежит». - **Таймеры** — обратный отсчёт до конца встречи, чайника, бэкапа. - **ИИ-агенты спрашивают человека** — агент присылает вопрос с вариантами ответа и ждёт, пока вы нажмёте на островке. ### Модель: человек описывает форму, скрипт заполняет её ``` Настройки → Свои интеграции скрипт / программа ┌───────────────────────────────┐ │ sparku push deploy progress=0.7 │ «Деплой» адрес: deploy │ │ curl --unix-socket … /v1/… │ поля: title, progress, stage │ ▼ │ индикатор, строка, карточка │ ~/.sparku/run/api.sock │ кнопки, приоритет, срок жизни │ ▼ └──────────────┬────────────────┘ значения полей └──────────────────► элементы островка ``` **Интеграцию** — название, адрес (slug), поля и то, как они выглядят на островке, — создаёт только человек: руками в редакторе, мастером по заготовке или импортом файла `.sparku` с явным согласием. Скрипт получает адрес и токен и с этого момента может только **записывать значения** в уже существующие поля. Добавить новый элемент, кнопку или карточку скрипт не может — состав островка неизменен, меняется только содержимое. > [!WARNING] > Свои интеграции — экспериментальная функция. Она может вести себя непредсказуемо > (карточки и островок иногда отображаются не так, как задумано), а частые обновления > из скриптов повышают расход процессора и батареи. Функция выключена по умолчанию — > включить её нужно осознанно в Настройках, прочитав предупреждение. ### Только локально, без сети API работает через UNIX-сокет `~/.sparku/run/api.sock` и существует, только пока функция включена в настройках. Сетевого порта нет: писать в интеграцию может любая программа **на этом же Mac**, запущенная от вашего имени, — то есть то же самое, что может делать любой ваш скрипт и так. Токен нужен для адресации и защиты от опечаток и чужих скриптов, а не как граница доступа между процессами одного пользователя. ### Дальше - [Быстрый старт](/docs/api/ru/quickstart.md) — включить функцию, поставить CLI, отправить первое значение. - [Понятия](/docs/api/ru/concepts.md) — интеграция, slug, токен, поля, элементы островка и карточки, приоритет, срок жизни значений. - [Типы полей](/docs/api/ru/fields.md) — что и как присылать в `push`, тексты ошибок. - [CLI sparku](/docs/api/ru/cli.md) — все команды, флаги, коды выхода. ## Быстрый старт > Включить функцию, поставить sparku и отправить первое значение на островок. ### 1. Включить функцию Настройки SparkU → раздел «Интеграции» → внизу блок «Свои интеграции» (бейдж «ЭКСПЕРИМЕНТ») → «Включить…». Появится предупреждение: функция экспериментальная, возможно непредвиденное поведение и повышенный расход процессора и батареи. Кнопка «Включить» не срабатывает по Return — нужно нажать её явно. После согласия в боковом списке настроек появляется раздел «Свои интеграции» и открывается мастер новой интеграции. ### 2. Поставить CLI В том же разделе настроек — строка «sparku» с кнопкой «Установить…». Она кладёт символическую ссылку `/usr/local/bin/sparku`, указывающую на утилиту внутри `SparkU.app`. Если `/usr/local/bin` недоступен для записи, macOS один раз спросит пароль администратора. Проверить, что команда доступна: ```bash sparku version ``` ### 3. Создать интеграцию мастером В мастере — пять заготовок: «Задача с прогрессом», «Метрики сервера», «Таймер», «Статус сборки», «Список». Каждая уже содержит поля, индикатор, строку и карточку — остаётся дать название и адрес (slug) или принять предложенный. «Настрою сам» создаёт пустую интеграцию без полей. Возьмём заготовку «Статус сборки» с адресом `build`. У неё уже есть поля `stage` (состояние), `branch` (текст), `started` (время), `link` (ссылка) и кнопка «Открыть» на карточке. ### 4. Скопировать токен В редакторе интеграции — раздел «Подключение»: строка `export SPARKU_TOKEN_BUILD=…` и готовые примеры на curl, Python, Node, Swift. Скопируйте экспорт и выполните его в терминале, из которого будете тестировать: ```bash export SPARKU_TOKEN_BUILD=spk_ваш_токен ``` `sparku` сам найдёт эту переменную по адресу интеграции — передавать токен каждый раз вручную не нужно. ### 5. Первый push ```bash sparku push build stage=running branch=main started=+0s ``` На островке должен обновиться индикатор и строка плашки. Раскройте островок — увидите карточку «Статус сборки» с кнопкой «Открыть». ### 6. Проверить, что всё работает ```bash sparku doctor build ``` Команда по шагам печатает: есть ли сокет, отвечает ли SparkU, найден ли токен, существует ли интеграция и какие у неё поля. Если что-то не так — `doctor` укажет, на каком шаге. ### 7. Снять значения ```bash sparku end build ``` Очищает все значения интеграции — на островке для неё не останется данных (элементы с видимостью «Пока есть данные» пропадут). ### То же самое через curl Без CLI, напрямую через UNIX-сокет: ```bash curl --unix-socket ~/.sparku/run/api.sock \ -H "Authorization: Bearer $SPARKU_TOKEN_BUILD" \ -d '{"stage": "running", "branch": "main", "started": "+0s"}' \ http://sparku/v1/integrations/build/fields ``` Снять значения: ```bash curl --unix-socket ~/.sparku/run/api.sock -X DELETE \ -H "Authorization: Bearer $SPARKU_TOKEN_BUILD" \ http://sparku/v1/integrations/build/fields ``` ### Дальше - [Понятия](/docs/api/ru/concepts.md) — как устроены поля, островок, карточка и токены. - [Типы полей](/docs/api/ru/fields.md) — что можно присылать в `push` для каждого типа. - [CLI sparku](/docs/api/ru/cli.md) — все команды и коды выхода. ## Понятия > Интеграция, slug, токен, поля, островок, карточка, приоритет и срок жизни значений. ### Интеграция Интеграция — то, что вы собрали в Настройках: название, адрес (slug), набор типизированных полей и то, как они показываются на островке (индикатор, строка плашки, карточка). Структура интеграции статична: скрипт присылает значения, но не может добавить поле, кнопку или элемент сверх того, что вы настроили. Одна интеграция даёт ровно один индикатор и одну строку плашки на островке — если нужно две строки, это две отдельные интеграции. Выключенная интеграция отвечает на запись `409 integration_disabled` и ничего не показывает; конфигурация и токен при этом сохраняются. ### Slug (адрес) Slug — то, как скрипт находит интеграцию: `/v1/integrations/{slug}/…`, `sparku push {slug} …`. Правила: - строчные латинские буквы, цифры и дефис; - начинается с буквы или цифры, не заканчивается дефисом; - не длиннее 32 символов. Примеры годных slug: `deploy`, `build`, `server`, `home-server`. При импорте `.sparku` и совпадении адреса SparkU сам подбирает свободный: `deploy` → `deploy-2`. ### Токен У каждой интеграции свой токен, выпущенный при её создании. Он хранится в Keychain и не защищает от других процессов вашей учётной записи — они и так имеют доступ к сокету. Его задача — адресация, защита от опечаток и чужих скриптов, а также выборочный отзыв доступа перевыпуском токена. **Где взять.** Настройки SparkU → Свои интеграции → нужная интеграция → раздел «Подключение»: строка `export SPARKU_TOKEN_<АДРЕС>=…` и готовые примеры кода. **Как передавать.** Заголовком `Authorization: Bearer <токен>` в каждом запросе. **Порядок поиска в CLI** (`sparku` пробует по очереди, пока не найдёт непустое значение): 1. `--token <токен>` 2. `--token-file <путь>` 3. переменная окружения `SPARKU_TOKEN_<АДРЕС>` (адрес — заглавными буквами, дефисы → `_`; например для `home-server` — `SPARKU_TOKEN_HOME_SERVER`) 4. переменная `SPARKU_TOKEN` 5. переменная `SPARKU_TOKEN_FILE` (путь к файлу с токеном) `SPARKU_TOKEN` без суффикса адреса удобен, когда скрипт пишет в одну интеграцию; `SPARKU_TOKEN_<АДРЕС>` — когда в несколько сразу. ### Поля Поле — именованный типизированный слот: ключ (`progress`, `cpu`), тип ([типы полей](/docs/api/ru/fields.md)) и подпись. Ключ поля — строчные латинские буквы, цифры и `_`, начинается с буквы, не длиннее 32 символов (это же имя переменной окружения `SPARKU_FIELD_<КЛЮЧ>` для shell-действий кнопок, поэтому дефис в ключе не допускается). У интеграции не больше 32 полей. Запись полей — **всё или ничего**: если хоть одно значение не проходит проверку, `POST …/fields` отклоняет весь запрос, и на островке не оказывается наполовину обновлённого состояния. ### Островок: индикатор, строка, карточка #### Индикатор (свёрнутый островок, слот 28 pt) По одному на интеграцию. Стили: | Стиль | Что показывает | Источник | |---|---|---| | Значок | статичный SF Symbol | — | | Кольцо | прогресс-кольцо со значком или числом внутри | поле `progress` или `number` с `min`/`max` | | Значение | короткая строка до 4 символов | `text`, `number`, `progress`, `state`, `bool` | | Эквалайзер | анимация как у «Медиа» | поле `bool` включает движение | | Крутилка | вращающийся индикатор | поле `bool` включает вращение | Поле `state` можно назначить дополнительно (`stateField`) — его значение подменяет значок, цвет и анимацию индикатора, не меняя его тип. #### Строка плашки (одна на интеграцию) | Стиль | Что показывает | Источник | |---|---|---| | Текст | шаблон `{title} · {progress}` | любые поля через `{ключ}` | | Таймер | живой отсчёт | поле `time` | | Шкала | подпись и мини-полоса | поле `progress` или `number` с `min`/`max` | | Аватары | стек кружков с инициалами, «+N» сверх трёх | поле `list` | **Шаблон строки** (`{ключ}`): подставляет значение в виде по умолчанию для типа поля. `{{` и `}}` — литеральные скобки, форматных спецификаторов нет. Вид по умолчанию: число — до двух знаков после точки плюс единица (`21°C`, `120 ms`), прогресс — `43%`, состояние — его подпись, время — отсчёт до момента или время с момента (`4:59`, `1:02:03`), список — число строк, булево — «да»/«нет». Нет значения — прочерк (`—`). #### Карточка (раскрытый островок) Собирается из шаблона со слотами — свободной сетки нет, чтобы визуальный ряд оставался единым со встроенными интеграциями. | Шаблон | Прообраз | Слоты / поля | |---|---|---| | Значение | Валюта | главное (`number`/`progress`/`text`), подпись | | Статус | агенты | главное (`state`), подпись, «с какого момента» (`time`) | | Прогресс | Pomodoro | главное (`progress`/`number`), подпись, отсчёт (`time`) | | Таймер | Pomodoro | главное (`time`), подпись, кольцо (`progress`) | | Шкалы | Монитор ресурсов | 2–4 поля `progress`/`number` (с `min`/`max`) рядом | | Лимит | Claude Code | быстрое окно (`progress`), медленное окно (`progress`), подпись | | Метрики | агенты | 2–4 поля `text`/`number`/`progress`/`state`/`bool`/`time` колонками | | Список | Календарь, Todo | одно поле `list` | | Сейчас | Медиа | заголовок (`text`, обязательно), подпись, обложка (`image`), шкала (`progress`) | | График | — | одно поле `number` — рисуется по накопленной истории | У карточки есть общая шапка и ряд из 0–3 элементов управления. Карточка может запрашивать обычную или широкую ширину (как встроенные интеграции), и подходящий компактный вариант. ### Элементы управления и их действия Элемент — кнопка, кнопка-капсула, тумблер (управляет полем `bool`) или меню (выбирает значение поля `state`). До трёх на карточку. Что делает нажатие — решает человек в редакторе, а не скрипт: | Действие | Что происходит | |---|---| | `event` | посылает событие слушателям `GET …/events` / `sparku listen`; без слушателя элемент неактивен | | `url` | открывает ссылку, файл или приложение (адрес — статический или `{поле}`) | | `shell` | выполняет команду; значения полей передаются **только** через переменные окружения `SPARKU_FIELD_<КЛЮЧ>`, никогда не подставляются в текст команды | | `shortcut` | запускает команду приложения «Команды» по имени | Слушатель получает событие о любом нажатии независимо от того, какое действие настроено сверх этого. ### Приоритет Островок — общий ресурс: окантовка выреза, маячок внимания и очередь диалогов устроены как стек заявок, а не «первый занял — все ждут». У пользовательской интеграции три уровня приоритета: - **Низкий** - **Обычный** — наравне со встроенными интеграциями SparkU (по умолчанию) - **Высокий** — выше встроенных При равном приоритете побеждает заявка, поданная **последней**. Обновление уже поданной заявки не выдвигает её вперёд: модуль, который раз в секунду сообщает прогресс, не отбирает дорожку у того, кто заявился после него. ### Срок жизни значений (TTL) Значения полей живут **только в памяти** и не переживают перезапуск SparkU — конфигурация интеграции при этом сохраняется всегда. По умолчанию значения живут до перезапуска SparkU или до явного `sparku end`; интеграция может задать свой срок, а конкретный запрос — переопределить его параметром `?ttl=`: ``` POST /v1/integrations/deploy/fields?ttl=5m POST /v1/integrations/deploy/fields?ttl=90 ``` `ttl` — число секунд или `5m` / `1h30m` (те же единицы, что у времени полей), не больше 7 дней. Пустой или отсутствующий параметр — срок из настроек интеграции. Каждая новая запись отсчитывает срок заново. `sparku push` поддерживает тот же параметр флагом `--ttl` (см. [CLI](/docs/api/ru/cli.md#push)). ### Привязка к процессу Срок не спасает от упавшего скрипта: он успел написать «Деплой 40 %» и умер, а островок показывает это до конца срока. Привязка решает это иначе — значения живут, пока жив процесс: ``` POST /v1/integrations/deploy/fields?bind=process # процесс, который прислал запрос POST /v1/integrations/deploy/fields?bind=4211 # процесс с этим PID POST /v1/integrations/deploy/fields?bind=none # снять привязку ``` Процесс завершился — значения снимаются сразу, как от `sparku end`. Привязка остаётся и у следующих записей без `bind`; новая `bind` её заменяет. Срок жизни при этом тоже действует: что наступит раньше. `bind=process` — для программ, которые сами держат соединение (скрипт на Python, сервис на Node). `curl` и `sparku` завершаются сразу после запроса, поэтому им нужен PID скрипта: в shell это `?bind=$$`, а `sparku push --bind` подставляет его сам. ### Notify-ограничение `POST …/notify` (вспышка + строка на несколько секунд) можно вызывать не чаще **раза в 2 секунды на интеграцию**. Более частый вызов получает `429 too_many_notices` с заголовком `Retry-After`. Ограничение не связано с записью значений через `fields` — она принимается без ограничений, на экран просто попадает не чаще кадра. ### История чисел Для графика (шаблон карточки «График») и тренда SparkU копит историю каждого числового поля: не больше одной точки в секунду (запись внутри той же секунды заменяет последнюю точку), 120 точек на поле, только в памяти — история пропадает вместе со значениями при перезапуске SparkU. ## Типы полей > Как поля приводятся из строки CLI и из JSON, примеры push и тексты ошибок 422. ### Как работает приведение `POST /v1/integrations/{slug}/fields` принимает JSON-объект `{"ключ": значение}`. Каждое значение приводится к типу поля, которое объявили в редакторе. Если хоть одно не проходит проверку — отклоняется весь запрос (`422 invalid_fields`), с текстом ошибки для каждого поля. `sparku push` умеет два синтаксиса присваивания: - `поле=значение` — отправляет **строку**, сервер сам приводит её к типу поля; - `поле:=json` — отправляет значение **как есть**, распарсив как JSON (`items:='[{"title":"a"}]'`, `done:=true`, `state:=null`). `null` (через `поле:=null`) очищает поле, не удаляя его из схемы. ### Таблица типов | Тип | Значение в `push` | Приведение из строки CLI | Примечание | |---|---|---|---| | `text` | `"prod"` | берётся как есть | обрезается по лимиту длины (по умолчанию 120 символов), не отклоняется | | `number` | `72` | `Double(строка)` | единица, `min`/`max` для шкал и кольца; копит историю для графика | | `progress` | `0.7` | число 0…1, либо `"70%"` | строго 0…1 после приведения | | `bool` | `true` | `true/false/yes/no/on/off/1/0` (без учёта регистра) | для тумблеров и эквалайзера | | `state` | `"failed"` | строка, должна входить в список состояний поля | у каждого состояния — иконка, цвет, анимация, вспышка при переходе | | `time` | ISO 8601 или `+5m` | `+5m`, `-1h30m`, ISO 8601 | таймеры тикают внутри SparkU — слать `push` каждую секунду не нужно | | `image` | путь или base64 (≤ 256 КБ) | путь `/…` или `~/…`, либо base64 (можно с `data:...,`) | только PNG/JPEG | | `url` | `"https://…"` | строка со схемой | для действий кнопок; `javascript:` и `data:` отклоняются | | `list` | `[{"title": "…"}]` | только `поле:=json` | до 50 строк | ### Примеры push для каждого типа ```bash sparku push deploy title="Деплой прода" sparku push server cpu=37.5 sparku push build progress=70% # эквивалент progress=0.7 sparku push server ready=true sparku push build stage=failed # значение должно быть объявлено в поле sparku push timer ends=+5m # через 5 минут от текущего момента sparku push timer ends=2026-10-01T12:00:00Z # ISO 8601 sparku push now cover=~/cover.png sparku push deploy link=https://ci.example.com/builds/482 sparku push queue items:='[{"title": "Сборка"}, {"title": "Тесты", "state": "running"}]' ``` ### Формат `time` Принимается одно из: - **Относительное смещение от текущего момента**: `+5m`, `-1h30m`, `+90s`, `+2d`. Единицы — `d`, `h`, `m`, `s`, можно комбинировать, но только в порядке убывания (`1h30m`, не `30m1h`). Одно поле `time` работает и таймером (`+5m` — обратный отсчёт), и секундомером (`-10m` — время, прошедшее с момента). - **ISO 8601** с временем и часовым поясом (`2026-10-01T12:00:00Z`, с долями секунды тоже подходит). - **UNIX-время** — только числом в `поле:=1699999999`, не строкой в `поле=…`. ### `list` — объекты строк Каждый элемент — объект `{"title", "subtitle"?, "value"?, "state"?, "icon"?}` или просто строка (тогда она становится `title`). Обязателен только `title`. Не больше 50 элементов на запись. ```bash sparku push queue items:='[ "Простая строка", {"title": "Сборка", "subtitle": "main", "state": "running"}, {"title": "Тесты", "value": "42/50", "icon": "checkmark"} ]' ``` ### `image` - Абсолютный путь (`/Users/…`) или путь с `~` — файл должен существовать и быть PNG или JPEG. - Или содержимое в base64 (можно с префиксом `data:image/png;base64,` — он отбрасывается). Ограничение — 256 КБ после декодирования; больше — отклоняется. ### Тексты ошибок 422 (из кода) Ответ `422 invalid_fields` содержит `fields: [{field, message}]`, а объединённое сообщение — `field: message; field2: message2`. Примеры реальных текстов: ``` progress: ожидалось 0…1, пришло 70 progress: ожидалось число 0…1, пришло «много» cpu: ожидалось число, пришло «abc» ready: ожидалось true или false, пришло «maybe» stage: ожидалось одно из: queued, running, passed, failed; пришло «cancelled» ends: ожидалось время (ISO 8601, +5m, -1h30m или UNIX-время), пришло «завтра» cover: файла нет: /Users/me/cover.png cover: поддерживаются только PNG и JPEG cover: base64 не разобран или картинка больше 256 КБ link: ожидалась ссылка со схемой (https://…), пришло «example.com» link: схема «javascript» не поддерживается items: ожидался массив, пришло объект items: не больше 50 строк, пришло 63 items[0]: нет title title: ожидалась строка, пришло массив ``` Ошибка не объявленного поля: ``` поле не объявлено в интеграции «deploy»; есть: title, progress, stage поле не объявлено: у интеграции «deploy» пока нет полей ``` Оба текста одновременно попадают в ответ API и в журнал интеграции в Настройках. ## CLI sparku > Все команды sparku, их флаги, порядок поиска токена и коды выхода. ### Установка Настройки SparkU → Свои интеграции → строка «sparku» → «Установить…». Ставится символическая ссылка `/usr/local/bin/sparku`, указывающая на утилиту внутри `SparkU.app/Contents/Helpers/sparku`; ссылка обновляется вместе с приложением. ```bash sparku version sparku help ``` ### Токен Токен ищется по порядку, пока не найдётся непустое значение: 1. `--token <токен>` 2. `--token-file <путь>` 3. `SPARKU_TOKEN_<АДРЕС>` (адрес заглавными буквами, дефисы → `_`) 4. `SPARKU_TOKEN` 5. `SPARKU_TOKEN_FILE` (путь к файлу с токеном) Адрес сокета можно переопределить переменной `SPARKU_SOCKET` или флагом `--socket` (по умолчанию `~/.sparku/run/api.sock`). > [!NOTE] > Недоступность SparkU проверяется **до** проверки токена: `sparku push … || [ $? > -eq 69 ]` в скрипте надёжно ловит «SparkU не запущен», даже если токен вдобавок не > настроен. ### push Записывает поля (upsert — частичное обновление, старые значения не заявленных ключей не трогаются). ``` sparku push <адрес> поле=значение … [поле:=json …] [--ttl 5m] [--bind] ``` - `поле=значение` — строка, сервер сама приводит её к типу поля; - `поле:=json` — значение как есть, распарсенное как JSON; - `--ttl` — срок жизни значений для этой записи: число секунд или `5m` / `1h30m` (иначе — срок по умолчанию из настроек интеграции); - `--bind` — значения живут, пока жив скрипт, который вызвал `sparku`: он завершился или упал — значения сняты (см. [привязку к процессу](/docs/api/ru/concepts.md#привязка-к-процессу)). ```bash sparku push deploy progress=0.4 stage=build sparku push deploy progress=70% --ttl 10m sparku push deploy stage=build --bind sparku push queue items:='[{"title": "Сборка"}]' ``` ### end Снимает все значения интеграции (как истечение TTL, только сразу). ``` sparku end <адрес> ``` ```bash sparku end deploy ``` ### notify Вспышка индикатора и строка на несколько секунд (не чаще раза в 2 секунды на интеграцию). ``` sparku notify <адрес> ["текст"] [--state значение] [--color red] [--duration 4] ``` - текст — позиционный аргумент, до 120 символов (длиннее обрезается); - `--state` — значение поля-состояния (берёт из него подпись, значок и цвет); - `--color` — семантический цвет (`blue`, `green`, `orange`, `red`, `purple`, `pink`, `yellow`, `teal`, `indigo`, `gray`, `success`, `warning`, `error`); - `--duration` — 1…10 секунд, по умолчанию 4. Нужен хотя бы `текст` или `--state`. ```bash sparku notify deploy "Сборка упала" --state failed sparku notify server "CPU 95%" --color red --duration 6 ``` ### glow Координатная подсветка: пятна света в заданных местах контура островка. Каждый вызов заменяет кадр целиком. ``` sparku glow <адрес> позиция:цвет[:яркость[:ширина]] … [--intensity 1.2] [--ttl 5] sparku glow <адрес> --off ``` - позиция — 0…1 слева направо по ширине островка, 0.5 — середина под вырезом; - цвет — семантическое имя (`blue`, `red`, `success`, …) или `#RRGGBB`; - яркость — 0…1, по умолчанию 1; ширина — полуширина пятна в долях ширины островка, по умолчанию 0.08; - `--intensity` — толщина обводки и сила свечения, 0.2…3; - `--ttl` — сколько кадр горит без следующего, по умолчанию 5 секунд; - `--off` — погасить. ```bash sparku glow deploy 0.2:green 0.5:orange:0.6 0.8:#ff3b30 sparku glow build 0.5:blue:1:0.5 --ttl 1h # широкая полоса на час sparku glow deploy --off ``` Для визуализаций, где кадры идут десятками в секунду, шлите `POST …/glow` в сокет напрямую, без запуска процесса на каждый кадр — см. [HTTP API](/docs/api/ru/http.md). ### ask Вопрос человеку на островке. Ждёт ответа (соединение держится, пока не ответят или не истечёт `--timeout`). ``` sparku ask <адрес> "вопрос" [-o id:подпись …] [--multi] [--custom] [--accept Да --decline Нет --comment] [--title …] [--timeout 10m] [--attach файл] ``` - `-o id:подпись` (повторяемый) — вариант ответа; без `-o` и без `--custom` вопрос становится подтверждением двумя кнопками; - `--multi` — можно выбрать несколько вариантов (нужно ≥ 2 вариантов); - `--custom` — добавляет поле свободного ответа; - `--accept` / `--decline` — подписи кнопок подтверждения (по умолчанию «Да» / «Нет»); - `--comment` — у отказа можно оставить комментарий; - `--title` — заголовок вопроса; - `--timeout` — от 5 секунд до суток, по умолчанию 10 минут; формат — число секунд или `90s` / `10m` / `2h`; - `--attach` — абсолютный (или относительный от текущей директории) путь к файлу, который можно открыть из вопроса. Ответ печатается в stdout: выбранные варианты — id по одному на строку, свой ответ — текстом, причина отказа (`--comment`) — тоже текстом. ```bash if sparku ask deploy "Деплоить?"; then ./deploy.sh; fi env=$(sparku ask deploy "Куда?" -o prod:Прод -o stage:Стейджинг) sparku ask deploy "Удалить ветку?" --accept Удалить --decline Оставить --comment ``` У интеграции может быть не больше одного вопроса без ответа одновременно — повторный `ask` получит `409 dialog_pending`. ### listen Печатает нажатия элементов карточки. ``` sparku listen <адрес> [--exec "команда"] [--reconnect] ``` - без `--exec` — каждое событие построчно в stdout в виде NDJSON (удобно для `jq`, `while read`); - с `--exec` — на каждое событие запускается команда; данные приходят ей **только** через окружение (`SPARKU_SLUG`, `SPARKU_CONTROL`, `SPARKU_VALUE`, `SPARKU_EVENT`), в текст команды ничего не подставляется; - `--reconnect` — переподключаться, если SparkU закрыл соединение (например, перезапускается), вместо того чтобы завершиться с ошибкой. ```bash sparku listen deploy --exec './on-click.sh' # $SPARKU_CONTROL, $SPARKU_VALUE sparku listen deploy --reconnect | jq . ``` ### import Предлагает интеграцию из файла `.sparku` человеку и ждёт согласия в настройках SparkU. ``` sparku import <файл.sparku> ``` При согласии печатает в stdout `export SPARKU_TOKEN_<АДРЕС>=…`, поэтому установщик может сделать: ```bash eval "$(sparku import deploy.sparku)" ``` Отказ человека — код выхода `77`. ### doctor Проверяет окружение по шагам: сокет, версия SparkU, (если указан адрес) токен и существование интеграции. ``` sparku doctor [адрес] ``` ```bash sparku doctor sparku doctor deploy ``` ### openapi Описание API в формате OpenAPI 3.1 (JSON) — для генераторов клиентов и ИИ-агентов. ``` sparku openapi [адрес] ``` Без адреса — общая схема всех маршрутов `/v1`; SparkU для неё не нужен, она же лежит на сайте: [/openapi.json](/openapi.json). С адресом — схема этой интеграции: тело `push` перечисляет её поля с типами, границами и значениями состояний, так что сгенерированный клиент не пропустит опечатку в имени поля. ```bash sparku openapi deploy > deploy.openapi.json ``` ### version / help ``` sparku version sparku help ``` ### Коды выхода #### `push`, `end`, `doctor`, `notify`, `listen`, `import` Коды — из ``, чтобы не пересекаться с кодами `ask`: | Код | Значение | Когда | |---|---|---| | `0` | успех | | | `64` | неверные аргументы (`EX_USAGE`) | опечатка в флаге, не хватает обязательного аргумента | | `65` | значения не прошли проверку (`EX_DATAERR`) | HTTP 400, 411, 413, 422 | | `67` | нет интеграции с таким адресом (`EX_NOUSER`) | HTTP 404 | | `69` | SparkU недоступен или API выключен (`EX_UNAVAILABLE`) | нет сокета, HTTP 409, 503 | | `70` | внутренняя ошибка SparkU (`EX_SOFTWARE`) | HTTP 5xx | | `75` | слишком часто, повторите позже (`EX_TEMPFAIL`) | HTTP 429 (`notify` чаще раза в 2 с) | | `76` | непонятный ответ (`EX_PROTOCOL`) | версии CLI и приложения разошлись | | `77` | нет токена или он не подходит (`EX_NOPERM`) | HTTP 401, 403; отказ `sparku import` | `69` проверяется первым: скрипт может надёжно отличить «SparkU не запущен» от любой другой ошибки, даже если токен тоже не настроен. #### `ask` Отдельная схема — не пересекается с ``, поэтому в shell работает `if sparku ask deploy "Деплоить?"; then …`: | Код | Значение | |---|---| | `0` | ответ получен (выбор или свой текст — в stdout) | | `1` | отказ (`declined`) | | `2` | время вышло, вопрос сняли на островке («Скрыть») или SparkU закрылся | `409 dialog_pending` (у интеграции уже есть вопрос без ответа) возвращается как код `69`, как и остальные ошибки недоступности. ## HTTP API > Транспорт, авторизация и полный список маршрутов /v1 — запрос, тело, ответ, коды ошибок. > [!TIP] > Машиночитаемое описание всех маршрутов — [/openapi.json](/openapi.json) (OpenAPI 3.1). > Схему с полями конкретной интеграции печатает [`sparku openapi <адрес>`](/docs/api/ru/cli.md#openapi). ### Транспорт SparkU слушает HTTP/1.1 на UNIX-сокете `~/.sparku/run/api.sock`. Сокет существует, только пока в Настройках включены «Свои интеграции» и SparkU запущен — если файла нет, функция выключена или приложение не работает (см. [Если не работает](/docs/api/ru/troubleshooting.md)). Права на сокет — `0700` на каталог и `0600` на файл: доступ только тому же пользователю. Никакого TCP-порта нет, поэтому запросы можно слать только с этого же Mac. ```bash curl --unix-socket ~/.sparku/run/api.sock http://sparku/v1/health ``` Хост в URL (`http://sparku/...`) не резолвится — соединение всё равно идёт через сокет, имя нужно только `curl`, чтобы собрать корректный запрос. Тело запросов и ответов — JSON (`Content-Type: application/json; charset=utf-8`). Кодировка тела — `Content-Length`; `Transfer-Encoding: chunked` не поддерживается. ### Авторизация Все маршруты интеграции (`/v1/integrations/{slug}/...`) требуют заголовок: ``` Authorization: Bearer <токен> ``` Токен выдаётся на интеграцию при её создании (Настройки → Свои интеграции → «slug» → Подключение) или при импорте `.sparku` ([Файлы .sparku и импорт](/docs/api/ru/packages.md)). Без заголовка — `401 unauthorized`, с чужим или отозванным токеном — `401 invalid_token`. `GET /v1/health` и `POST /v1/imports` токена не требуют: первому нечего защищать, а второй ещё не знает, к какой интеграции относится запрос — интеграции пока нет. ### Маршруты | Метод | Путь | Назначение | |---|---|---| | `GET` | `/v1/health` | версия протокола и приложения | | `GET` | `/v1/integrations/{slug}` | схема полей и текущие значения | | `POST` | `/v1/integrations/{slug}/fields` | частичная запись значений (upsert) | | `DELETE` | `/v1/integrations/{slug}/fields` | очистить значения | | `POST` | `/v1/integrations/{slug}/notify` | вспышка и строка на несколько секунд | | `POST` | `/v1/integrations/{slug}/glow` | пятна света в заданных местах контура | | `DELETE` | `/v1/integrations/{slug}/glow` | погасить пятна | | `POST` | `/v1/integrations/{slug}/dialogs` | вопрос человеку — [Вопросы человеку](/docs/api/ru/dialogs.md) | | `GET` | `/v1/integrations/{slug}/events` | поток нажатий (SSE) — [Кнопки и события](/docs/api/ru/events.md) | | `POST` | `/v1/imports` | импорт `.sparku` — [Файлы .sparku и импорт](/docs/api/ru/packages.md) | Метод, которого маршрут не поддерживает, отвечает `405 method_not_allowed` с заголовком `Allow`. Неизвестный путь под `/v1` — `404 not_found`. #### `GET /v1/health` Без токена. Не проверяет, что интеграция существует, — просто «SparkU здесь». ```bash curl --unix-socket ~/.sparku/run/api.sock http://sparku/v1/health ``` ```json {"status": "ok", "api_version": 1, "app_version": "2.4.0"} ``` #### `GET /v1/integrations/{slug}` Схема полей интеграции и текущие значения (если они ещё не истекли по TTL). ```bash curl --unix-socket ~/.sparku/run/api.sock \ -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \ http://sparku/v1/integrations/deploy ``` ```json { "slug": "deploy", "name": "Деплой", "enabled": true, "fields": [ {"key": "progress", "type": "progress", "label": "Прогресс"}, {"key": "stage", "type": "state", "label": "Этап", "states": ["running", "done", "failed"]}, {"key": "title", "type": "text", "label": "Сервис", "max_length": 60} ], "values": {"progress": 0.4, "stage": "running", "title": "api · prod"}, "updated_at": "2026-09-28T10:15:00Z", "expires_at": "2026-09-28T11:15:00Z" } ``` `expires_at` в ответе нет, если у значений нет срока жизни. Если значений ещё не было или они истекли, `values` — пустой объект, `updated_at`/`expires_at` отсутствуют. Коды: `200`, `404 unknown_integration`, `401`. #### `POST /v1/integrations/{slug}/fields` Частичная запись значений — upsert: перечисленные поля меняются, остальные не трогаются. `null` у поля очищает его. Запись **всё или ничего**: если хоть одно поле не проходит проверку, не применяется ни одно. ```bash curl --unix-socket ~/.sparku/run/api.sock \ -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \ -H "Content-Type: application/json" \ -d '{"progress": 0.72, "stage": "running", "title": "api · prod"}' \ http://sparku/v1/integrations/deploy/fields ``` ```json {"ok": true, "updated": ["progress", "stage", "title"], "cleared": []} ``` Типы значений в теле — см. [Типы полей](/docs/api/ru/fields.md). Тело `{"progress": null}` попадает в `cleared`, а не в `updated`. **`?ttl=`** — на сколько запомнить значения этой записи: число секунд (`?ttl=90`) или относительное время (`?ttl=5m`, `?ttl=1h30m`), не больше 7 дней. Без параметра действует срок жизни из настроек интеграции (по умолчанию — «до перезапуска SparkU»). Отсчёт начинается заново от каждой записи. Если срок задан, ответ дополнительно содержит `expires_at`: ```json {"ok": true, "updated": ["progress"], "cleared": [], "expires_at": "2026-09-28T11:20:00Z"} ``` **`?bind=`** — привязать значения к процессу: `process` (тот, что прислал запрос), PID (`?bind=$$` из shell) или `none` (снять привязку). Процесс завершился — значения сняты. Ответ с привязкой содержит `bound_pid`. Подробнее — [привязка к процессу](/docs/api/ru/concepts.md#привязка-к-процессу). Коды: `200`, `400 empty_body`, `400 invalid_json`, `400 invalid_ttl`, `400 invalid_bind`, `409 integration_disabled`, `422 invalid_fields`, `401`, `404`. `422 invalid_fields` перечисляет каждое неверное поле: ```json {"error": {"code": "invalid_fields", "message": "progress: ожидалось 0…1, пришло 70", "fields": [{"field": "progress", "message": "progress: ожидалось 0…1, пришло 70"}]}} ``` #### `DELETE /v1/integrations/{slug}/fields` Очищает все значения интеграции — эквивалент `sparku end`. Ответ — `204 No Content`, тела нет. ```bash curl --unix-socket ~/.sparku/run/api.sock -X DELETE \ -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \ http://sparku/v1/integrations/deploy/fields ``` Коды: `204`, `401`, `404`. Работает и для выключенной интеграции. #### `POST /v1/integrations/{slug}/notify` Вспышка на окантовке и/или переключение строки на несколько секунд. Нужно хотя бы одно из `text` и `state`. ```bash curl --unix-socket ~/.sparku/run/api.sock \ -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \ -d '{"text": "Сборка упала", "state": "failed", "color": "red", "duration": 4}' \ http://sparku/v1/integrations/deploy/notify ``` | Поле | Тип | Правило | |---|---|---| | `text` | строка | обрезается до 120 символов (с `…`) | | `state` | строка | значение любого поля типа `state` у интеграции | | `color` | строка | одно из семантических имён (`blue`, `red`, `success`, …) | | `duration` | число | 1…10 секунд, по умолчанию 4 | ```json {"ok": true} ``` Не чаще раза в 2 секунды на интеграцию: следующий вызов раньше — `429 too_many_notices` с заголовком `Retry-After` (секунды до разрешённого вызова). Коды: `200`, `400 invalid_json`, `409 integration_disabled`, `409 notify_disabled` (notify выключен в настройках интеграции), `422 invalid_notice`, `429 too_many_notices`, `401`, `404`. #### `POST /v1/integrations/{slug}/glow` Координатная подсветка: свет загорается в заданных местах контура островка и стоит там, пока не придёт следующий кадр. Подходит для визуализации звука, нескольких параллельных задач «по участкам», шкал. ```bash curl --unix-socket ~/.sparku/run/api.sock \ -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \ -d '{"spots": [{"position": 0.2, "color": "green"}, {"position": 0.8, "color": "#FF3B30", "intensity": 0.6, "width": 0.15}]}' \ "http://sparku/v1/integrations/deploy/glow?ttl=10" ``` | Поле | Тип | Правило | |---|---|---| | `spots` | массив | обязателен, не больше 64 пятен; пустой массив — темно | | `spots[].position` | число | 0…1 слева направо по ширине островка, 0.5 — середина под вырезом | | `spots[].color` | строка | семантическое имя (`blue`, `red`, `success`, …) или `#RRGGBB` | | `spots[].intensity` | число | 0…1, по умолчанию 1 | | `spots[].width` | число | полуширина пятна в долях ширины островка, 0.005…1, по умолчанию 0.08 | | `intensity` | число | толщина обводки и сила свечения, 0.2…3, по умолчанию 1 | ```json {"ok": true} ``` - Каждый запрос заменяет кадр целиком. Соседние пятна, которые перекрываются, смешивают цвета. - Частота не ограничена: шлите кадры хоть 60 раз в секунду, на островок попадёт не больше ~30. - `?ttl=` — сколько кадр горит без следующего: секунды или `5m` / `1h30m`, по умолчанию 5 секунд. Скрипт упал — подсветка гаснет сама. - Пока горит вспышка или статус другого виджета, она принимает форму ваших пятен: цвета её, а яркость вдоль контура — от пятен (не ниже 30 %). Между интеграциями с пятнами решает приоритет интеграции. - `DELETE …/glow` и `sparku end` гасят пятна сразу. Коды: `200`, `400 invalid_ttl`, `409 integration_disabled`, `422 invalid_glow`, `401`, `404`. #### `POST /v1/integrations/{slug}/dialogs` Вопрос человеку на островке. Подробности — [Вопросы человеку](/docs/api/ru/dialogs.md). #### `GET /v1/integrations/{slug}/events` Поток нажатий в карточке (SSE). Подробности — [Кнопки и события](/docs/api/ru/events.md). #### `POST /v1/imports` Импорт `.sparku`. Подробности — [Файлы .sparku и импорт](/docs/api/ru/packages.md). ### Единый формат ошибок Любая ошибка — JSON вида: ```json {"error": {"code": "invalid_fields", "message": "человекочитаемый текст"}} ``` `message` пишется для человека и рассчитан на то, что клиент — чаще всего shell-скрипт: текст можно выводить в терминал как есть. Некоторые ошибки добавляют поля в объект `error` (например `fields` у `invalid_fields`). ### Коды ошибок | Код | HTTP | Где | |---|---|---| | `not_found` | 404 | путь не под `/v1` или не существует | | `unknown_integration` | 404 | нет интеграции с таким `slug` | | `unauthorized` | 401 | нет заголовка `Authorization: Bearer …` | | `invalid_token` | 401 | токен не подходит к интеграции | | `method_not_allowed` | 405 | метод не поддерживается на этом пути | | `empty_body` | 400 | тело запроса пустое (`fields`) | | `invalid_json` | 400 | тело — не JSON-объект | | `invalid_ttl` | 400 | `?ttl=` не разобрать | | `invalid_bind` | 400 | `?bind=` не `process`, `none` или PID живого процесса | | `invalid_fields` | 422 | значение(я) не прошли проверку типа | | `invalid_notice` | 422 | `notify`: неверные `text`/`state`/`color`/`duration` | | `invalid_glow` | 422 | `glow`: неверные пятна, цвет или интенсивность | | `invalid_dialog` | 422 | `dialogs`: тело вопроса не разобрать | | `invalid_package` | 422 | `imports`: файл `.sparku` не разобрать | | `integration_disabled` | 409 | интеграция выключена в Настройках | | `notify_disabled` | 409 | notify выключен в настройках интеграции | | `dialog_pending` | 409 | у интеграции уже есть вопрос без ответа | | `import_pending` | 409 | SparkU уже предлагает человеку другой импорт | | `import_declined` | 403 | человек отклонил предложенную интеграцию | | `too_many_notices` | 429 | `notify` чаще раза в 2 секунды | | `too_many_connections` | 503 | больше 64 одновременных соединений | | `unavailable` | 503 | SparkU ещё не готов принимать запросы | | `withdrawn` | 503 | SparkU закрывается или API выключили во время ожидания | | `bad_request` | 400 / 411 / 413 / 431 / 505 | нарушение HTTP: плохие заголовки, нет `Content-Length`, тело больше 1 МБ, слишком большие заголовки, неподдерживаемая версия | > [!NOTE] > `bad_request` — единый код для ошибок разбора HTTP; конкретную причину и статус > смотрите в `message`. ### Коалесценция и лимиты - **`fields`** принимает запросы без ограничения частоты, но на островок попадает не чаще одного обновления за кадр отрисовки — быстрый цикл `push` не мигает промежуточными значениями. - **`notify`** — не чаще раза в 2 секунды на интеграцию (`429` при превышении). - **`glow`** — без ограничения частоты, на островок попадает не больше ~30 кадров в секунду. - **Диалог** — не больше одного ожидающего вопроса на интеграцию (`409 dialog_pending`). - **Импорт** — не больше одного предложения на всё приложение одновременно (`409 import_pending`). - **Соединения** — не больше 64 одновременных на весь сервер (`503 too_many_connections`). - **Тело запроса** — не больше 1 МБ (лимит рассчитан на поле `image` в base64). - **История чисел** для графика и тренда — не больше точки в секунду, 120 точек на поле, только в памяти процесса SparkU. ### Совместимость `/v1` Внутри `/v1` изменения только добавляющие: новые маршруты и новые поля ответа, но не изменение уже существующего поведения. Скрипт, написанный сегодня, продолжит работать после обновления SparkU без правок. ## Кнопки и события > Поток /events, at-most-once доставка, sparku listen в NDJSON и с --exec, другие действия кнопок. ### Кто такой слушатель Кнопка, капсула, тумблер или меню в карточке может отправлять «событие слушателю» ([Типы полей](/docs/api/ru/fields.md), элементы управления). Слушатель — это клиент, который держит открытым `GET /v1/integrations/{slug}/events`. Пока поток открыт хотя бы у одного клиента, элементы с действием «событие» активны; без слушателя — неактивны, нажимать некуда. Доставка **at-most-once**, без очереди: событие, случившееся, пока никто не слушал, пропадает безвозвратно. Переподключение слушателя не возвращает пропущенные нажатия. ### Поток `GET …/events` ```bash curl --unix-socket ~/.sparku/run/api.sock \ -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \ http://sparku/v1/integrations/deploy/events ``` Ответ — `text/event-stream`, соединение не закрывается, пока его не закроет одна из сторон. Первым событием приходит `hello` со списком элементов управления карточки: ``` event: hello data: {"integration":"deploy","controls":[{"id":"pause","kind":"toggle"},{"id":"restart","kind":"button"}]} ``` Дальше — `control` на каждое нажатие: ``` event: control data: {"integration":"deploy","control":"pause","value":true,"at":"2026-09-28T10:15:03Z"} ``` `value` есть только у тумблера (`bool`) и меню (`state`) — то, во что переключили элемент. У кнопки и капсулы `value` в событии нет. Раз в 15 секунд сервер пишет комментарий SSE: ``` : ping ``` Клиенты его игнорируют; сервер по ошибке записи такого пульса узнаёт, что клиент пропал (сон, обрыв сети), и закрывает соединение — держать мёртвого слушателя незачем. Пока поток открыт, отправлять в него ничего не нужно и нельзя: всё, что клиент пришлёт после запроса, сервер игнорирует. ### `sparku listen` ``` sparku listen [--exec "команда"] [--reconnect] ``` Без `--exec` каждое событие печатается в stdout строкой JSON (NDJSON) — удобно для `jq` и `while read`: ```bash sparku listen deploy | while read -r line; do control=$(echo "$line" | jq -r .control) value=$(echo "$line" | jq -r .value) echo "нажали $control = $value" done ``` С `--exec` на каждое событие `control` запускается команда; `hello` в неё не попадает. Данные события приходят обработчику **только через переменные окружения**, не в аргументах и не в тексте команды: | Переменная | Значение | |---|---| | `SPARKU_SLUG` | адрес интеграции | | `SPARKU_CONTROL` | id элемента управления | | `SPARKU_VALUE` | значение тумблера/меню (`true`/`false` или id состояния); у кнопки пусто | | `SPARKU_EVENT` | всё событие целиком, как пришло по SSE (JSON-строка) | ```bash sparku listen deploy --exec './on-click.sh' ``` ```bash #!/bin/bash # on-click.sh case "$SPARKU_CONTROL" in pause) echo "пауза: $SPARKU_VALUE" ;; restart) ./restart-deploy.sh ;; esac ``` `--reconnect` переподключается, пока SparkU недоступен или API выключили, вместо того чтобы завершиться. Без него `listen` завершается кодом `69`, когда соединение обрывается. ```bash sparku listen deploy --reconnect --exec './on-click.sh' & ``` > [!NOTE] > `sparku listen` требует токен так же, как `push`: `--token`, `--token-file` или > `SPARKU_TOKEN_<АДРЕС>` / `SPARKU_TOKEN`. ### Пример на Python (без зависимостей) Читает SSE прямо из UNIX-сокета через `http.client`: ```python import http.client, json, os, socket class SparkU(http.client.HTTPConnection): def __init__(self): super().__init__("sparku") def connect(self): self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(os.path.expanduser("~/.sparku/run/api.sock")) token = os.environ["SPARKU_TOKEN_DEPLOY"] api = SparkU() api.request("GET", "/v1/integrations/deploy/events", headers={ "Authorization": f"Bearer {token}", }) response = api.getresponse() buffer = "" for chunk in iter(lambda: response.read(1), b""): buffer += chunk.decode("utf-8") if buffer.endswith("\n\n"): for line in buffer.splitlines(): if line.startswith("data: "): event = json.loads(line[len("data: "):]) if "control" in event: print("нажали", event["control"], event.get("value")) buffer = "" ``` ### Пример на Node ```js const http = require("node:http"); const os = require("node:os"); const request = http.request({ socketPath: `${os.homedir()}/.sparku/run/api.sock`, path: "/v1/integrations/deploy/events", headers: { Authorization: `Bearer ${process.env.SPARKU_TOKEN_DEPLOY}` }, }); request.on("response", (response) => { let buffer = ""; response.on("data", (chunk) => { buffer += chunk.toString("utf8"); const frames = buffer.split("\n\n"); buffer = frames.pop(); for (const frame of frames) { const line = frame.split("\n").find((l) => l.startsWith("data: ")); if (!line) continue; const event = JSON.parse(line.slice("data: ".length)); if (event.control) console.log("нажали", event.control, event.value); } }); }); request.end(); ``` ### Другие действия кнопок Событие слушателю — не единственное, что может делать элемент управления. Что именно, задаёт человек в редакторе интеграции (не скрипт): - **Открыть ссылку, файл или приложение** — `{ключ}` в адресе подставляется значением поля с процентным кодированием; шаблон из одного поля-ссылки (`{url}`) берётся как есть. - **Выполнить shell-команду** — текст команды пишет человек в редакторе. Значения полей **никогда не подставляются в текст команды**, только в окружение — `SPARKU_FIELD_<КЛЮЧ>` (ключ — в верхнем регистре), плюс `SPARKU_SLUG`, `SPARKU_CONTROL` и `SPARKU_VALUE` для тумблера/меню. - **Запустить команду «Команды» (Shortcuts)** — по имени, без shell: `shortcuts run <имя>`. Слушатели получают событие о нажатии независимо от того, какое из этих действий настроено, — оно выполняется наравне с отправкой в поток. ## Вопросы человеку > POST …/dialogs и sparku ask — варианты, подтверждение, таймаут, вложение; как агент спрашивает разрешения. ### Как это работает `POST /v1/integrations/{slug}/dialogs` показывает вопрос на островке и держит соединение открытым, пока человек не ответит (или вопрос не снят). Ответ приходит в теле того же HTTP-ответа — ждать нужно `curl` или HTTP-клиенту, который умеет не закрывать соединение по таймауту раньше срока `timeout` из тела запроса. У интеграции — не больше одного вопроса без ответа одновременно: второй `dialogs` раньше ответа на первый получает `409 dialog_pending`. Диалоги должны быть отдельно включены в настройках интеграции. ### Тело запроса Вопрос с вариантами — есть `options` или `custom`: ```json { "message": "Выкатить в прод?", "title": "Деплой", "options": ["yes", {"id": "no", "label": "Отмена", "detail": "ничего не делать"}], "multiple": false, "custom": false, "timeout": "10m" } ``` Подтверждение — без `options` и `custom`: ```json {"message": "Удалить ветку feature/x?", "accept": "Удалить", "decline": "Оставить", "comment": true} ``` | Поле | Тип | По умолчанию | Правило | |---|---|---|---| | `message` | строка | — | обязательное, до 600 символов | | `title` | строка | адрес интеграции | до 60 символов | | `options` | массив строк или `{id, label, detail?}` | — | до 9 вариантов; строка — сама себе и `id`, и `label` | | `multiple` | bool | `false` | выбор нескольких — нужно ≥ 2 вариантов | | `custom` | bool | `false` | добавляет поле свободного ответа | | `accept` / `decline` | строка | «Да» / «Нет» | подписи кнопок подтверждения, до 30 символов | | `comment` | bool | `false` | у отказа можно оставить комментарий | | `timeout` | число секунд или `"90s"` / `"10m"` / `"2h"` | `600` (10 мин) | от 5 секунд до суток | | `attachment` | абсолютный путь | — | файл, который человек может открыть из вопроса; должен существовать | С `options` или `custom` — вопрос с вариантами; иначе — подтверждение двумя кнопками. Указать одновременно `options`/`custom` и `accept`/`decline` нельзя — `accept`/`decline` относятся только к подтверждению. ### Ответ Всегда `200 OK`, форма тела зависит от исхода: | Ситуация | Тело | |---|---| | выбрал вариант(ы) | `{"answer": "chose", "options": ["yes"]}` | | свой ответ (`custom`) | `{"answer": "custom", "text": "…"}` | | подтвердил | `{"answer": "accepted"}` | | отказал | `{"answer": "declined"}` или `{"answer": "declined", "comment": "…"}` | | истёк `timeout` | `{"answer": "expired"}` | | вопрос снят | `{"answer": "withdrawn"}` | `withdrawn` приходит, если: клиент отключился до ответа (тогда, впрочем, отвечать уже некому — вопрос просто снимается с островка), SparkU закрывается, API выключили или интеграцию выключили/удалили, пока вопрос ждал ответа. Спрятать вопрос кнопкой «Скрыть» можно только у интеграции с карточкой — вернуть его на экран можно кнопкой «Ответить» в карточке; до этого он остаётся без ответа на сервере. ### Пример: curl ```bash curl --unix-socket ~/.sparku/run/api.sock \ -H "Authorization: Bearer $SPARKU_TOKEN_DEPLOY" \ -d '{"message": "Выкатить в прод?", "accept": "Да", "decline": "Нет"}' \ http://sparku/v1/integrations/deploy/dialogs ``` ```json {"answer": "accepted"} ``` ### `sparku ask` ``` sparku ask "вопрос" [-o id:подпись …] [--multi] [--custom] [--accept Да --decline Нет --comment] [--title …] [--timeout 10m] [--attach файл] ``` ```bash sparku ask deploy "Деплоить в прод?" -o yes:Да -o no:Нет sparku ask deploy "Куда катить?" -o prod:Прод -o stage:Стейджинг --custom sparku ask deploy "Удалить ветку?" --accept Удалить --decline Оставить --comment ``` Выбранные варианты печатаются в stdout по одному на строку (id, не подпись), свой ответ (`custom`) — текстом, комментарий отказа — тоже в stdout. #### Коды выхода `ask` Отдельные от ``, которыми пользуются `push`/`end`/`doctor` — чтобы в shell работало `if sparku ask deploy "Деплоить?"; then …`: | Код | Значение | |---|---| | `0` | ответ получен (выбор или свой текст — в stdout) | | `1` | отказ (`declined`) | | `2` | время вышло, вопрос снят или «Скрыть», SparkU закрылся | Ошибки транспорта (нет сокета, неверный токен, `409 dialog_pending`) возвращают обычные коды `sparku` — `69`, `77`, `69` соответственно, см. [CLI sparku](/docs/api/ru/cli.md#коды-выхода). ```bash if sparku ask deploy "Выкатить в прод?"; then ./deploy.sh else echo "деплой отменён" fi ``` ### Для ИИ-агентов Агент вроде Claude Code или Codex, который выполняет команды в shell, может спросить подтверждения у человека тем же путём, каким это делает CI-скрипт: через `sparku ask` в интеграцию, которую человек завёл заранее (например, «Агент» с адресом `agent`; если у неё есть карточка, вопрос можно спрятать и вернуться к нему). Это не отдельный протокол — агент такой же клиент API, как любой другой процесс. ```bash if sparku ask agent "Удалить ветку feature/old и её PR на GitHub?" \ --accept Удалить --decline "Не трогать" --timeout 90s; then git push origin --delete feature/old gh pr close 42 else echo "отменено человеком" >&2 exit 1 fi ``` Пока вопрос висит на островке, агент блокируется на вызове `sparku ask` — это ожидаемо: решение принимает человек, и до его ответа выполнять опасный шаг нельзя. Если человек не отвечает `--timeout`, по умолчанию 10 минут, `ask` вернёт `2`, и агент должен трактовать это как отказ, а не как разрешение продолжать. > [!TIP] > У инструмента, которым агент запускает команды, обычно свой таймаут (у Claude Code — > 2 минуты по умолчанию). Ставьте `--timeout` меньше него, иначе агент прервёт ожидание > раньше, чем человек успеет ответить, а вопрос снимется с островка. Строку-инструкцию для агента (в `CLAUDE.md`, `AGENTS.md` и т. п.) можно написать так: ```text Перед необратимыми действиями (удаление, force-push, деплой, миграции) спроси человека: sparku ask agent "<что и зачем>" --accept Да --decline Нет --timeout 90s Код 0 — можно, 1 — отказ, 2 — ответа нет: считай отказом. ``` ## Файлы .sparku и импорт > Формат .sparku, что в него не входит, POST /v1/imports и sparku import, согласие человека, риски. ### Формат `.sparku` Экспорт интеграции — это её схема без секретов: название, адрес, поля, оформление островка и карточки, элементы управления. Файл — обычный JSON: ```json { "format": "sparku.integration", "version": 1, "integration": { "slug": "deploy", "name": "Деплой", "icon": "shippingbox.fill", "accent": "blue", "fields": [ {"key": "progress", "type": "progress", "label": "Прогресс"}, {"key": "stage", "type": "state", "label": "Этап", "states": [ {"id": "running", "label": "Идёт", "icon": "arrow.triangle.2.circlepath", "color": "blue"}, {"id": "done", "label": "Готово", "icon": "checkmark.circle.fill", "color": "success"}, {"id": "failed", "label": "Ошибка", "icon": "xmark.octagon.fill", "color": "error"} ]}, {"key": "title", "type": "text", "label": "Сервис", "maxLength": 60} ], "valueLifetime": null, "island": {"...": "настройки индикатора и строки"}, "card": {"...": "шаблон, слоты, элементы управления"} } } ``` **Не входят в файл:** `id`, `createdAt`, `isEnabled` и, конечно, токен — у импортированной интеграции он всегда новый. Схему создаёт только человек: файл переносит то, что он уже настроил, а не выдаёт скрипту право создавать интерфейс самому (см. [Понятия](/docs/api/ru/concepts.md)). Лимит размера файла — 512 КБ: интеграция — это десятки полей и шаблонов, не мегабайты. Файл, сохранённый более новой версией SparkU (`version` больше поддерживаемой), отклоняется с просьбой обновить приложение. ### Экспорт Кнопка в шапке редактора интеграции (Настройки → Свои интеграции → «slug») сохраняет файл `<адрес>.sparku` (имя — по названию интеграции, транслитерация в адрес). ### Импорт в настройках «+» → «Импортировать из файла…» открывает `.sparku` с диска. SparkU показывает экран согласия: кто предлагает интеграцию, что появится на островке и в карточке, и отдельно — какие элементы управления открывают адреса, выполняют команды или запускают «Команды» (Shortcuts). Занятый адрес (`slug`) заменяется на свободный — `deploy` → `deploy-2`. Тот же экран открывается двойным кликом по файлу `.sparku` в Finder — SparkU зарегистрирован как его владелец. Если «Свои интеграции» выключены, SparkU откроет раздел «Интеграции» и попросит сначала включить их. ### `POST /v1/imports` Тот же путь, но инициированный скриптом или установщиком: тело запроса — содержимое файла `.sparku` как есть (не JSON-обёртка, а сам файл). Токен не нужен — интеграции пока нет. ```bash curl --unix-socket ~/.sparku/run/api.sock \ --data-binary @deploy.sparku \ http://sparku/v1/imports ``` Запрос **блокируется**, пока человек не откроет Настройки и не ответит на предложение — SparkU сам открывает нужный раздел. Согласие: ```json {"slug": "deploy", "name": "Деплой", "token": "spk_9f2c...a31b"} ``` Статус `201 Created`. Отказ — `403 import_declined`. Пока одно предложение уже ждёт ответа (из настроек или от другого запроса), новый `POST /v1/imports` получает `409 import_pending` немедленно, не дожидаясь ничего. Если SparkU закрывается или API выключают, пока запрос ждёт, — `503 withdrawn`. Файл, который не проходит проверку схемы (как в редакторе) — `422 invalid_package`. ### `sparku import` ```bash sparku import deploy.sparku ``` Ждёт согласия так же, как прямой `POST /v1/imports`, и печатает в stdout строку экспорта токена: ``` export SPARKU_TOKEN_DEPLOY=spk_9f2c...a31b ``` Поэтому установщик скрипта делает: ```bash eval "$(sparku import deploy.sparku)" sparku push deploy progress=0 ``` Отказ человека — код выхода `77` (нет прав, как и при неверном токене); во всех остальных случаях коды — обычные `sparku`, см. [CLI sparku](/docs/api/ru/cli.md#коды-выхода). ### Риски Импорт `.sparku` — это доверие к чужой конфигурации, не к чужому коду: сам файл не исполняется, но элементы управления в нём могут быть настроены на **выполнение команд** на этом Mac при нажатии в карточке (действие «Выполнить команду» или «Запустить „Команду“»). Экран импорта показывает такие элементы явно — с текстом команды или именем Shortcuts-команды — до того, как человек согласится. Прочитайте их так же внимательно, как командную строку из письма. Элементы с действием «Событие слушателю» безопасны сами по себе: они лишь сообщают о нажатии тому, кто запустил `sparku listen`, — исполнить что-либо без отдельного слушателя они не могут. > [!WARNING] > Не запускайте `sparku import` и не открывайте `.sparku`-файлы из источников, > которым вы не доверяете, даже если экран импорта не показал ни одного риска: > ссылки в элементах «Открыть» тоже стоит проверить прежде, чем нажимать кнопку на > островке. ### Коды ответов | Код | HTTP | Когда | |---|---|---| | — | 201 | человек согласился, интеграция добавлена | | `import_declined` | 403 | человек отклонил предложение | | `import_pending` | 409 | SparkU уже ждёт ответа на другое предложение | | `invalid_package` | 422 | файл не по формату `.sparku` или не проходит проверку схемы | | `withdrawn` | 503 | SparkU закрылось или API выключили, пока запрос ждал | ## Рецепты > Готовые примеры — прогресс деплоя, сборка Xcode, мониторинг сервера, таймер, очередь задач, подтверждение агента. Каждый рецепт — какая интеграция нужна (поля и шаблон карточки, см. [Типы полей](/docs/api/ru/fields.md)) и рабочий код. Токен везде читается из `SPARKU_TOKEN_<АДРЕС>` — см. [CLI sparku](/docs/api/ru/cli.md#токен). ### 1. Прогресс деплоя в CI с вспышкой при ошибке **Интеграция:** `deploy`, шаблон карточки «Прогресс». Поля: `progress` (`progress`), `stage` (`state`: `running`/`done`/`failed`), `title` (`text`). ```bash #!/bin/bash # deploy.sh — запускается в CI set -euo pipefail sparku push deploy title="api · prod" stage=running progress=0 total_steps=5 for step in $(seq 1 $total_steps); do ./run-step.sh "$step" || { sparku push deploy stage=failed sparku notify deploy "Деплой упал на шаге $step" --state failed --color red exit 1 } sparku push deploy progress="$(( step * 100 / total_steps ))%" done sparku push deploy stage=done progress=100% sparku notify deploy "Деплой готов" --state done ``` ### 2. Сборка Xcode / `swift build` **Интеграция:** `build`, шаблон «Статус». Поля: `phase` (`state`: `building`/`testing`/`passed`/`failed`), `title` (`text`). ```bash #!/bin/bash # build.sh sparku push build title="SparkU" phase=building if ! xcodebuild -scheme SparkU -destination 'platform=macOS' build 2>build.log; then sparku push build phase=failed sparku notify build "Сборка упала" --state failed --color red exit 1 fi sparku push build phase=testing if ! xcodebuild -scheme SparkU -destination 'platform=macOS' test 2>test.log; then sparku push build phase=failed sparku notify build "Тесты упали" --state failed --color red exit 1 fi sparku push build phase=passed sparku notify build "Сборка и тесты зелёные" --state passed ``` ### 3. Мониторинг сервера на Python раз в N секунд **Интеграция:** `server`, шаблон «Шкалы». Поля: `cpu` (`number`, 0–100, `%`), `memory` (`number`, 0–100, `%`), `latency` (`number`, мс). ```python #!/usr/bin/env python3 import http.client, json, os, socket, time TOKEN = os.environ["SPARKU_TOKEN_SERVER"] class SparkU(http.client.HTTPConnection): def __init__(self): super().__init__("sparku") def connect(self): self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(os.path.expanduser("~/.sparku/run/api.sock")) def push(fields): api = SparkU() api.request("POST", "/v1/integrations/server/fields", json.dumps(fields), { "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", }) response = api.getresponse() response.read() def read_metrics(): # замените на реальный сбор метрик (psutil, /proc, ssh на прод и т. д.) import random return {"cpu": random.uniform(10, 90), "memory": random.uniform(30, 80), "latency": random.uniform(40, 300)} while True: push(read_metrics()) time.sleep(5) ``` Запуск в фоне: `nohup python3 monitor.py &` или через `launchd`/systemd — сокет переживает перезапуск скрипта, следующий `push` просто восстановит значения. ### 4. Таймер / обратный отсчёт одним push **Интеграция:** `timer`, шаблон «Таймер». Поле: `until` (`time`). Таймер тикает внутри SparkU — повторять `push` раз в секунду не нужно, достаточно одного вызова с моментом окончания: ```bash # отсчёт 25 минут (Pomodoro) sparku push timer until=+25m # конкретный момент в ISO 8601 sparku push timer until=2026-09-28T15:00:00Z ``` Отменить отсчёт — `sparku end timer`. ### 5. Очередь задач (список) **Интеграция:** `queue`, шаблон «Список». Поле: `tasks` (`list`). ```bash #!/bin/bash # queue.sh — обновляет список задач сборки tasks_json() { cat <<'JSON' [ {"title": "Сборка iOS", "subtitle": "шаг 3 из 5", "value": "60%", "state": "running"}, {"title": "Тесты API", "subtitle": "412 из 420", "value": "98%", "state": "done"}, {"title": "Миграция БД", "subtitle": "staging", "state": "failed"}, {"title": "Деплой лендинга", "subtitle": "в очереди"} ] JSON } sparku push queue "tasks:=$(tasks_json)" ``` `tasks:=...` — двоеточие перед `=` в `sparku push` говорит CLI разобрать значение как JSON, а не как строку (см. [CLI sparku](/docs/api/ru/cli.md)). ### 6. Агент спрашивает перед опасной командой **Интеграция:** `agent`, диалоги включены в настройках, карточка не обязательна. ```bash #!/bin/bash # cleanup-branch.sh — вызывается ИИ-агентом (Claude Code, Codex и т. п.) из shell set -euo pipefail if sparku ask agent "Удалить ветку feature/old и закрыть PR #42?" \ --accept Удалить --decline "Не трогать" --timeout 5m; then git push origin --delete feature/old gh pr close 42 else echo "отменено человеком" >&2 exit 1 fi ``` Подробнее про коды выхода `ask` и раздел «Для ИИ-агентов» — [Вопросы человеку](/docs/api/ru/dialogs.md). ### 7. Обёртка `if sparku ask …` для любой команды **Интеграция:** та же `agent`, что и в рецепте 6, — одна интеграция для всех подтверждений в терминале. ```bash #!/bin/bash # confirm.sh — обёртка: спрашивает и выполняет любую команду # использование: ./confirm.sh "Удалить node_modules?" rm -rf node_modules question=$1; shift if sparku ask agent "$question"; then "$@" else echo "отменено" >&2 exit 1 fi ``` ```bash ./confirm.sh "Перезапустить прод-сервер?" ssh prod 'sudo systemctl restart api' ``` ## Если не работает > sparku doctor, типичные ошибки — от отсутствия сокета до 409, журнал запросов и консоль «Проверка». ### Первым делом — `sparku doctor` ```bash sparku doctor sparku doctor deploy # плюс проверка токена и схемы конкретной интеграции ``` Проходит по шагам и печатает `✓`/`✗` на каждом: сокет, `/v1/health`, версия CLI против версии приложения, а с адресом интеграции — источник токена и её поля. Останавливается на первой проблеме и возвращает соответствующий код выхода ([CLI sparku](/docs/api/ru/cli.md#коды-выхода)). ``` sparku 2.4.0 ✓ сокет: /Users/you/.sparku/run/api.sock ✓ SparkU 2.4.0, API v1 ✓ токен для «deploy»: $SPARKU_TOKEN_DEPLOY ✓ интеграция «Деплой»: progress (progress), stage (state), title (text) ``` ### Сокета нет ``` ✗ сокета нет: /Users/you/.sparku/run/api.sock ``` Значит одно из двух: - **SparkU не запущен.** Запустите приложение. - **«Свои интеграции» выключены.** Настройки → Интеграции → «Свои интеграции» → «Включить…». Функция экспериментальная и по умолчанию выключена (см. [Обзор](/docs/api/ru/overview.md)). Любой запрос к отсутствующему сокету — код `69` (`sparku`) или ошибка соединения (`curl: (7) Failed to connect`, `ConnectionRefusedError` у Python). Скрипт может надёжно отличить «SparkU не запущен» от прочих ошибок: недоступность проверяется первой, до токена и адреса. ### 401 — нет или не тот токен ```json {"error": {"code": "unauthorized", "message": "нужен токен: заголовок Authorization: Bearer <токен> (Настройки SparkU → Свои интеграции → Подключение)"}} {"error": {"code": "invalid_token", "message": "токен не подходит к интеграции «deploy» — возможно, его перевыпустили"}} ``` - `unauthorized` — заголовка `Authorization` нет вовсе. В `sparku` это значит, что токен не нашёлся ни в `--token`, ни в `--token-file`, ни в `SPARKU_TOKEN_<АДРЕС>`, ни в `SPARKU_TOKEN` (порядок поиска — [CLI sparku](/docs/api/ru/cli.md#токен)). - `invalid_token` — токен есть, но не подходит: опечатка, чужой токен или его перевыпустили в Настройках (например, после подозрения на утечку). Возьмите актуальный токен там же: «slug» → Подключение. `sparku` в обоих случаях завершается кодом `77`. ### 404 — нет такой интеграции ```json {"error": {"code": "unknown_integration", "message": "интеграции «deploy» нет; создайте её в Настройках SparkU → Свои интеграции"}} ``` Опечатка в адресе или интеграция ещё не создана человеком — API никогда не создаёт интеграции сам ([Понятия](/docs/api/ru/concepts.md)). `sparku` — код `67`. ### 422 — значение не прошло проверку ```json {"error": {"code": "invalid_fields", "message": "progress: ожидалось 0…1, пришло 70", "fields": [{"field": "progress", "message": "progress: ожидалось 0…1, пришло 70"}]}} ``` Текст ошибки рассчитан на то, чтобы сразу быть понятным в терминале — какое поле и что с ним не так. Запись **всё или ничего**: если хоть одно поле в запросе не прошло проверку, не применяется ни одно, даже если остальные были верными. Сверьте тип и границы поля со схемой — `GET /v1/integrations/{slug}` или раздел «Поля» в редакторе. `sparku` — код `65`. ### 429 — `notify` слишком часто ```json {"error": {"code": "too_many_notices", "message": "notify не чаще раза в 2 с"}} ``` Заголовок ответа `Retry-After` подсказывает, сколько ждать. Это не ошибка данных — лимит защищает от «стробоскопа», если `notify` вызывается в цикле. `sparku` — код `75`; скрипт может ловить именно `75`, чтобы не путать частый `notify` с прочими проблемами. ### 409 — конфликт состояния | Код | Причина | Что делать | |---|---|---| | `integration_disabled` | интеграция выключена в Настройках | включить её в разделе «Свои интеграции» | | `notify_disabled` | `notify` выключен в настройках именно этой интеграции | включить в разделе «Островок» редактора | | `dialog_pending` | у интеграции уже есть вопрос без ответа | дождаться ответа или снятия текущего вопроса | | `import_pending` | SparkU уже предлагает человеку другой импорт | дождаться его ответа | Все эти коды — временное состояние, не ошибка запроса: `sparku` возвращает `69` («SparkU недоступен») для первых двух и то же значение для остальных — см. таблицу кодов в [HTTP API](/docs/api/ru/http.md#коды-ошибок). ### Журнал запросов интеграции Настройки → Свои интеграции → «slug» → раздел «Журнал» — последние 50 запросов к этой интеграции: действие (`push`/`end`/`notify`/`ask`/`listen`/`describe`), код ответа, текст ошибки или ответ человека, и процесс-клиент (`python3`, `curl` и т. д. по PID на том конце сокета). Это первое место, куда стоит смотреть, когда скрипт «вроде бы что-то отправляет», но на островке ничего не меняется: журнал показывает, дошёл ли запрос и что ему ответил сервер, без необходимости включать отладку на стороне скрипта. ### Консоль «Проверка» в редакторе В редакторе интеграции (раздел «Проверка») можно отправить значения тем же путём, что и `push`, снять их, вызвать `notify` и задать тестовый вопрос — не выходя в терминал. Полезно, чтобы отделить «не так настроена карточка» от «скрипт шлёт не то»: если значения из консоли отображаются на островке правильно, а из скрипта — нет, проблема в скрипте (адрес, токен, имена полей, форма JSON), не в самой интеграции. ### Прочее - **Просроченные значения.** Если `GET /v1/integrations/{slug}` показывает пустой `values`, а вы точно писали значения, — проверьте `expires_at` в прошлом ответе: срок жизни (`valueLifetime` интеграции или `?ttl=`) истёк. `push` — это upsert, следующая запись всё восстановит. - **Версия CLI и приложения разошлись.** `sparku doctor` печатает предупреждение, если `sparku` в `/usr/local/bin` симлинкует на другую копию SparkU, чем запущенная. Переустановите симлинк: Настройки → Свои интеграции → «Командная строка». - **`sparku listen` не получает события.** Пока поток не открыт, элементы с действием «Событие слушателю» неактивны на самом островке — нажатие никуда не уходит, событие не генерируется вовсе. Откройте `listen` и лишь потом нажимайте кнопку; см. [Кнопки и события](/docs/api/ru/events.md).