Документация

Виджеты

Быстрый старт по виджетам

Виджет — это компактная плитка, закреплённая под шапкой сервера в боковой панели. Внешний агент отправляет POST со строками предопределённых типов — счётчиками, статусами, текстом и прогресс-барами, — а все участники, открывшие сервер, видят обновление плитки в реальном времени через SSE.

Типичные сценарии: онлайн стрима, статус сборки, дежурный, аптайм, лаг бэкапов.

Создание виджета

  1. Откройте Dev Portal → Серверы → выберите сервер.
  2. В панели Виджет укажите название и (необязательно) описание, отправьте форму.
  3. Скопируйте 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.