Быстрый старт по виджетам
Виджет — это компактная плитка, закреплённая под шапкой сервера в боковой панели. Внешний агент отправляет POST со строками предопределённых типов — счётчиками, статусами, текстом и прогресс-барами, — а все участники, открывшие сервер, видят обновление плитки в реальном времени через SSE.
Типичные сценарии: онлайн стрима, статус сборки, дежурный, аптайм, лаг бэкапов.
Создание виджета
- Откройте Dev Portal → Серверы → выберите сервер.
- В панели Виджет укажите название и (необязательно) описание, отправьте форму.
- Скопируйте URL приёма из одноразового блока. Регенерация токена мгновенно инвалидирует старый URL.
URL приёма выглядит так:
https://chattr.example.com/widgets/:appId/:token
Отправка снапшота
POST /widgets/:appId/:token
Content-Type: application/json
{
"rows": [
{ "kind": "counter", "label": "Онлайн", "value": 1247 },
{ "kind": "status", "label": "API", "state": "ok", "text": "200 OK" },
{ "kind": "progress", "label": "Диск", "current": 42, "max": 128 },
{ "kind": "text", "label": "Версия", "value": "1.4.2-beta" }
]
}
Каждый POST полностью заменяет предыдущий снапшот. Истории нет — хранится и отображается только последняя отправка.
Ответ при успехе:
{
"snapshot": {
"updatedAt": "2026-01-15T13:42:07.012Z",
"rows": [ /* повторяются входные строки */ ]
}
}
Типы строк
| Kind | Поля | Отображение |
|---|---|---|
counter |
{ label, value: number } |
label слева, число справа (с разделителями тысяч при ` |
status |
{ label, state: "ok" | "warn" | "err" | "idle", text?: string } |
Цветная точка + чип статуса + опциональный текст |
text |
{ label, value: string } |
label и короткое текстовое значение (до 120 символов) |
progress |
{ label, current: number, max: number } |
Подпись, current / max и полоса, заполненная до current / max |
Ограничения
| Ограничение | Значение |
|---|---|
| Строк в одном снапшоте | 1 – 6 |
Длина label |
1 – 40 символов |
Длина text/value |
≤ 120 символов |
progress.max |
Конечное число, ≥ 0 |
| Размер тела запроса | ≤ 32 KB |
| Rate limit | 30 POST/мин/IP |
Неизвестный тип строки, превышение длины или отсутствие обязательного поля → 400 с полем error (например, row_2:label_too_long). При ошибке валидации снапшот в БД не меняется.
Закрепление и видимость
- Закрепление: администратор выбирает один виджет из модалки Виджеты (кнопка в шапке сервера) — этот виджет отображается под мета-информацией сервера. Закреплённый виджет один на сервер.
- Видимость: администраторы могут скрыть виджет. Скрытый виджет не виден обычным участникам и отклоняет приём с
403 widget_invisible. Скрытие закреплённого виджета автоматически снимает закрепление. - Удаление виджета автоматически снимает закрепление и удаляет снапшот.
Realtime-обновления
Каждый успешный POST публикует SSE-событие всем участникам, открывшим сервер. Клиенты с открытой плиткой обновляют её на месте — без полной перерисовки боковой панели. Модалка со списком виджетов у открывших её пользователей также обновляется.
Примеры кода
cURL
curl -X POST "https://chattr.example.com/widgets/APP_ID/WIDGET_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{ "kind": "counter", "label": "Онлайн", "value": 42 },
{ "kind": "status", "label": "API", "state": "ok", "text": "200 OK" }
]
}'
Node.js
const WIDGET_URL = "https://chattr.example.com/widgets/APP_ID/WIDGET_TOKEN";
async function push(rows) {
const res = await fetch(WIDGET_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rows }),
});
if (!res.ok) throw new Error("widget ingest failed: " + res.status);
}
// Вызывайте из цикла мониторинга, например раз в 30 секунд.
await push([
{ kind: "counter", label: "Онлайн", value: await countOnline() },
{ kind: "status", label: "Сборка", state: "warn", text: "в очереди 3" },
]);
Python
import requests
WIDGET_URL = "https://chattr.example.com/widgets/APP_ID/WIDGET_TOKEN"
def push(rows):
r = requests.post(WIDGET_URL, json={"rows": rows}, timeout=5)
r.raise_for_status()
push([
{"kind": "counter", "label": "Онлайн", "value": 42},
{"kind": "progress", "label": "Диск", "current": 42, "max": 128},
])
Go
package main
import (
"bytes"
"encoding/json"
"net/http"
)
func PushWidget(url string, rows []map[string]any) error {
body, err := json.Marshal(map[string]any{"rows": rows})
if err != nil {
return err
}
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
resp.Body.Close()
return nil
}
PHP
$widgetUrl = "https://chattr.example.com/widgets/APP_ID/WIDGET_TOKEN";
$ch = curl_init($widgetUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"rows" => [
["kind" => "counter", "label" => "Зрители", "value" => 1247],
["kind" => "status", "label" => "Стрим", "state" => "ok", "text" => "в эфире"],
],
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
Справочник ошибок
| Статус | error |
Что означает |
|---|---|---|
400 |
rows_required / too_many_rows / row_N:<причина> |
Валидатор отклонил payload |
403 |
widget_invisible |
Виджет скрыт; админ может снова сделать его видимым в приложении |
403 |
suspended |
Приложение было приостановлено |
404 |
widget_not_found |
Неизвестная пара app/token — либо токен был перевыпущен |
429 |
rate_limited |
Превышен лимит 30 запросов/мин/IP; в ответе есть заголовок Retry-After |
Права
Создание, ротация, закрепление и скрытие виджетов требуют права manage_server. Владелец и администраторы сервера имеют его по умолчанию. Обычные участники видят закреплённые и видимые виджеты, но не могут менять их состояние.
Примечания
- Ротация токена одноразовая — портал показывает новый URL единожды; скопируйте его или запустите ротацию снова.
- Снапшоты переживают рестарт API (хранятся в Postgres); in-memory кэши прогреваются при первом чтении.
- Строки рендерятся в том порядке, в котором вы их отправили в payload.