# Кнопки и события

> Поток /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 <slug> [--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
  <имя>`.

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