# pulse-lets-go — Telephone Load Balancer Балансировщик телефонной нагрузки на базе FreeSWITCH + NATS. Принимает метрики от нижестоящих АТС, вычисляет score готовности, возвращает лучшую ноду для следующего вызова через `/api/route`. ## Стек | Компонент | Технология | |-----------|-----------| | Backend | Go (stdlib net/http или Chi, nats.go, golang-jwt, bcrypt) | | Frontend | SvelteKit + Tailwind + shadcn-svelte | | Auth | JWT access/refresh, bcrypt, роли: admin / viewer | | Шина | NATS — `pulse.metrics.` | | Хранилище | JSON (`data/config.json`, `trunks.json`, `users.json`) + ASCII-лог метрик | | Деплой | systemd + единый бинарник, без Docker | ## Модели данных ### NATS message (node → balancer, каждые 5 сек) ```json { "node_id": "pbx-01", "ts": 1719000000, "status": "ok", "active_calls": 42, "max_calls": 250, "idle_cpu": 35, "load_avg": 0.85, "call_failure_rate": 0.5 } ``` `call_failure_rate`: 0.0..100.0 (проценты). ### In-memory node state (engine) Все поля метрики + дополнительные: - `disabled` (bool) — ручное исключение админом - `disabled_reason` (string) ## Scoring Engine ### Lethal (score → -100, нода исключается) ``` status != "ok" stale > stale_threshold_sec (по умолчанию 20 сек) active_calls >= max_calls idle_cpu < idle_cpu_min (по умолчанию 5) disabled == true call_failure_rate > call_failure_rate_lethal (по умолчанию 15.0) ``` ### Weighted score ``` call_score = min(100, max(0, 100 - (active_calls / max_calls * 100))) load_score = min(100, max(0, 100 - (load_avg * 50))) idle_score = min(100, max(0, idle_cpu)) fail_score = min(100, max(0, 100 - call_failure_rate)) score = call_score * 0.40 + load_score * 0.30 + idle_score * 0.20 + fail_score * 0.10 ``` Веса и пороги конфигурируются в `config.json`. ### Оптимизация `best_node_id` + `best_score` + `fallback_active` кэшируются, обновляются при каждой метрике. `/api/route` читает их за RLock, O(1). ## HTTP API ### Auth - `POST /api/auth/login` → JWT - Все эндпоинты (кроме login и `/api/monitoring/*`) требуют `Authorization: Bearer ` - `/api/monitoring/*` — API-key в заголовке `X-API-Key` ### Core | Метод | Путь | Роль | Описание | |-------|------|------|----------| | `GET` | `/api/route?caller_id=X&dest=Y&ingress_trunk=Z` | – | Выбор ноды для звонка (без JWT) | Response (успех): ```json { "node_id": "pbx-03", "score": 88, "sip_gateway": "sip:pbx03.lan:5060" } ``` Response (fallback — все ноды unhealthy): ```json { "fallback": true, "sip_gateway": "sip:operator.lan:5060", "reason": "all_nodes_unhealthy", "nodes": [...] } ``` Response (нет нод вообще): ```json { "error": "no_nodes_registered" } ``` ### Nodes | Метод | Путь | Роль | Описание | |-------|------|------|----------| | `GET` | `/api/nodes` | admin/viewer | Список нод со score | | `GET` | `/api/nodes/:id/metrics` | admin/viewer | История метрик (ring buffer) | | `PUT` | `/api/nodes/:id/toggle` | admin | `{"disabled": true/false, "reason": "..."}` | ### Trunks | Метод | Путь | Роль | Описание | |-------|------|------|----------| | `GET` | `/api/trunks` | admin | Все транки | | `GET` | `/api/trunks?type=ingress\|balance\|fallback` | admin | Фильтр по типу | | `POST` | `/api/trunks` | admin | Создать | | `PUT` | `/api/trunks/:id` | admin | Обновить | | `DELETE` | `/api/trunks/:id` | admin | Удалить | ### Users (admin only) | Метод | Путь | Описание | |-------|------|----------| | `GET` | `/api/users` | Список | | `POST` | `/api/users` | Создать | | `PUT` | `/api/users/:id` | Обновить | | `DELETE` | `/api/users/:id` | Удалить | ### Monitoring (API-key, не JWT) | Метод | Путь | Описание | |-------|------|----------| | `GET` | `/api/monitoring/zabbix` | JSON для Zabbix LLD + items | | `GET` | `/api/monitoring/prometheus` | text/plain метрики | ### WebSocket | Метод | Путь | Описание | |-------|------|----------| | `WS` | `/ws/metrics` | Real-time метрики на фронт (JWT в query param) | ## Терминология транков | Тип | Назначение | Сколько | |-----|-----------|---------| | `ingress` | Входящий транк (откуда звонки приходят) | 1..N | | `balance` | Транк назначения (куда балансятся, привязан к Node) | 1..N | | `fallback` | Резервный транк оператора (когда все balance score < 0) | ровно 1 | Модель транка: ```json { "id": "trk-001", "name": "pbx-03 balance", "type": "ingress|balance|fallback", "node_id": "pbx-03", "gateway": "sip:pbx03.lan:5060", "codecs": ["PCMU", "PCMA"], "context": "default", "enabled": true, "description": "", "created_at": "...", "updated_at": "..." } ``` ## FreeSWITCH интеграция FS использует Lua скрипт в dialplan → `mod_curl` дёргает `/api/route` → получает JSON → парсит `sip_gateway` → bridge. ``` ``` route.lua: GET `/api/route?...` → если `fallback: true` идёт на fallback gateway, иначе на `sip_gateway`. ## config.json ```json { "nats_url": "nats://localhost:4222", "listen_addr": ":8080", "jwt_secret": "auto-generated-on-first-run", "monitoring_api_key": "auto-generated-on-first-run", "stale_threshold_sec": 20, "scoring": { "idle_cpu_min": 5, "call_failure_rate_lethal": 15.0, "weights": { "call_score": 0.40, "load_score": 0.30, "idle_score": 0.20, "fail_score": 0.10 } } } ``` ## Структура проекта ``` pulse-lets-go/ ├── cmd/pulse-lets-go/main.go ├── internal/ │ ├── api/ — handlers: auth, nodes, trunks, users, monitoring, ws │ ├── engine/ — scorer + router (sync.RWMutex, best_node кэш) │ ├── nats/ — subscriber, in-memory node store │ ├── config/ — manager: чтение/атомарная запись JSON │ ├── models/ — все общие типы │ └── log/ — ASCII-metrics логгер + ротация ├── data/ — JSON конфиги (gitignored) ├── web/ — SvelteKit + shadcn-svelte + Tailwind │ ├── src/routes/ │ │ ├── +layout.svelte │ │ ├── +page.svelte # дашборд │ │ ├── login/+page.svelte │ │ ├── trunks/+page.svelte │ │ └── nodes/+page.svelte │ ├── src/lib/ │ │ ├── components/ │ │ └── api.ts │ ├── svelte.config.js │ ├── tailwind.config.js │ └── package.json ├── go.mod ├── Makefile └── AGENTS.md ``` ## Порядок реализации 1. `cmd/pulse-lets-go/main.go` + Go module + пустая структура internal 2. `internal/models/` — все типы данных 3. `internal/config/` — чтение/запись JSON, атомарное сохранение 4. `internal/nats/` — подписка, in-memory store 5. `internal/engine/` — scorer + router 6. `internal/api/` — все хендлеры 7. `internal/log/` — ASCII-логгер + ring buffer 8. Web-фронт (SvelteKit) 9. Makefile + systemd unit 10. Тесты + graceful shutdown