Compare commits

..

16 Commits

Author SHA1 Message Date
Maksim Totmin
6e9c5f20d4 docs: add routing.md — balancing modes and routing rules 2026-06-25 22:54:37 +07:00
Maksim Totmin
0fa9278eef docs: remove img.shields.io badges (blocked in RF) 2026-06-25 22:45:34 +07:00
Maksim Totmin
f20d5aa0e2 docs: restructure documentation into docs/ directory
- Create docs/ with 7 focused files: architecture, api, scoring, agent,
  deployment, freeswitch, development
- Rewrite README.md as concise overview with links to docs/
- Remove AGENTS.md (content merged into docs/ and README)
2026-06-25 22:35:19 +07:00
Maksim Totmin
110289a093 feat: production-grade routing cache + stale detection fixes
Three-tier routing cache (ESL global -> file -> HTTP) eliminates HTTP
from call path for 2500-5000 concurrent calls. Lua reads cached route
in ~0.1us instead of blocking on api:execute('curl', ...).

Engine fixes:
- recalcBestLocked now skips stale nodes (was 5 min bug -> now ~10-15s)
- PickNodeForCall action_type='node' checks staleness for consistency
- onMetric callback pushes cache on every metric (no ticker delay)

Config:
- stale_threshold_sec default 20 -> 10 (industry standard)
- contrib/ included in Makefile deploy target
- route.lua paths updated for /opt/pulse-lets-go
2026-06-25 22:23:16 +07:00
Maksim Totmin
54259e51b9 feat: CID-based routing rules engine with flexible match conditions
Routing rules:
- RouteRule model: 3 match fields (caller_id, dest, ingress_trunk regex),
  3 action types (node/pool/auto), time-based (days of week, time range)
- Engine router.go: CompiledRule with pre-compiled regex,
  PickNodeForCall with cascade (dead node → next rule), pickNodeFromPool
- Separate sync.RWMutex for Router — zero contention admin vs traffic
- Validate-before-save pattern: engine validates before JSON save
- Graceful degradation: corrupted rules skipped, no rules → PickNode()

Persistence:
- data/routing.json with atomic save (tmp→rename), config.Manager
- Auto-created empty on first start

API (admin only):
- GET/POST/PUT/DELETE /api/routing/rules
- PUT /api/routing/rules/:id/toggle
- PUT /api/routing/rules/reorder

Integration:
- /api/route now uses PickNodeForCall with caller_id/dest/ingress_trunk
  query params (already sent by route.lua)
- RouteResponse includes matched_rule field for debugging
- Main.go loads routing rules on startup with graceful fallback

Web UI:
- /routing page: table with priority arrows, inline edit, create form,
  regex test tool, toggle, delete, action badges, hits counter

Tests:
- 28 tests: matchRule, time matching, PickNodeForCall cascade,
  pool scoring, validation, hit counting, compilation
2026-06-25 21:51:37 +07:00
Maksim Totmin
54d56690c6 feat: eviction trunk cleanup + gateway auto-update on re-registration 2026-06-25 21:01:58 +07:00
Maksim Totmin
938c9e9362 feat: weighted random routing + mode switching + EWMA smoothing + stale eviction
- Weighted random routing (math/rand/v2 per-goroutine ChaCha8)
- PUT /api/route/mode for runtime mode switch (best / weighted_random)
- EWMA score smoothing (configurable smoothing_factor, default 0.3)
- Periodic stale node eviction (5 min threshold, 60s interval)
- Non-blocking WS broadcast (per-client buffered channel + writePump)
- Background rate limiter cleanup goroutine
- In-memory trunk gateway cache (zero disk I/O in hot path)
- Configurable load_avg multiplier (default 50.0, backward compat)
- UI mode indicator + admin Switch button in dashboard
- 42 tests: weighted random, mode switching, EWMA, eviction, API integration
2026-06-25 20:42:46 +07:00
Maksim Totmin
ff037f9c1e feat: balancer dashboard metrics + fix WebSocket Hijacker 2026-06-25 19:59:26 +07:00
Maksim Totmin
273e316af9 refactor: deploy model — all files under /opt/pulse-lets-go
Single prefix deployment: /opt/pulse-lets-go/ contains
bin/, data/, web/, log/ — no more split across
/usr/local/bin, /etc, /usr/local/share, /var/log.

* Makefile: install → deploy target with PREFIX=/opt/pulse-lets-go
* deploy/pulse-lets-go.service: all paths under /opt/pulse-lets-go,
  removed stale nats.service dependency
* main.go: default dataDir = parent of exeDir (PREFIX/bin → PREFIX/data)
* README: updated all paths, step 1 uses make deploy
2026-06-25 19:35:54 +07:00
Maksim Totmin
0d8a56e73f fix: build system — findWebDir order, auto-copy web static to bin
* findWebDir: CWD and project root checked before binary-adjacent path,
  so dev always serves fresh web/build/ over stale bin/web/build/
* build-go: auto-copies web/build/ to bin/web/ before Go compile
* build-prod: same auto-copy for production builds
* Proper refresh of frontend static on every make build
2026-06-25 19:30:49 +07:00
Maksim Totmin
78e53f42bf fix: SvelteKit UI reactivity with Svelte 5 runes
* / replaces legacy $: syntax
* Active menu highlight via .url.pathname (/stores)
* Logout navigates to /login via goto()
* Token/user footer updates reactively via
2026-06-25 19:30:42 +07:00
Maksim Totmin
cc9da3ad7d refactor: common util package, ESL/AMI/security fixes, Prometheus metrics
Backend stability and security improvements:

* internal/util/ — common RandomHex helper, removed 3 duplicates
* ESL: deduplicated readMessage (locked/unlocked), net.JoinHostPort for IPv6
* AMI: synchronous reconnect() in readEventsLoop, net.JoinHostPort for IPv6
* Auth: /api/auth/refresh accepts Authorization header only (no ?token=)
* decodeJSON: http.MaxBytesReader(1<<20) body limit
* Trunks: gatewayParams() uses configured ESL.GatewayPrefix
* Config: jwt_secret_env env-var fallback
* FSCollector: time.After → time.NewTimer with defer Stop
* Monitoring: Prometheus counters (route_requests, nodes_total/healthy, uptime)
* go fmt pass across all internal/ packages
2026-06-25 19:30:36 +07:00
Maksim Totmin
e66ac27dd9 docs: fix 12 documentation inaccuracies in README.md and AGENTS.md
README.md fixes:
- Architecture diagram now shows pulse-lets-go-agent layer
- Features list: added agent, auto-trunk, WS gateway, user-01 protection
- route.lua example: replaced fake curl.fetch() with real api:execute()
- systemd unit: Wants=nats.service removed (NATS on separate host)
- Gateway mapping: removed grep 'pulse-' filter (misses existing gateways)
- Production deployment: added agent deployment step (between NATS and route.lua)
- Makefile commands: added build-agent, build-siptest, siptest-*, emulate-*

AGENTS.md fixes:
- Structure: added missing cmd/emulator, cmd/siptest, cmd/pulse-lets-go-agent, esl/, ami/, contrib/
- NATS model: added sip_gateway field
- config.json: added nats_user, nats_password, tls, rate_limit, log, esl blocks
- Internal packages: added esl/ and ami/
- route.lua name: route_call → pulse_route (matches production)
2026-06-25 17:16:03 +07:00
Maksim Totmin
13c88c1586 fix: agent systemd unit remove nats dependency
Agent connects to NATS remotely, not locally. After=network only.
2026-06-25 16:59:39 +07:00
Maksim Totmin
95ac4164dd chore: emulator sip_gateway, Makefile targets, full README documentation
- Emulator: add sip_gateway field to all scenarios (auto-trunk creation)
- Makefile: build-agent, build-siptest, emulate-*, siptest-* targets
- README: full documentation with agent section, production deployment
- Project structure update with all components
2026-06-25 16:59:17 +07:00
Maksim Totmin
1e6f432230 feat: metrics agent for FreeSWITCH and Asterisk nodes
- pulse-lets-go-agent: collects metrics from PBX nodes via ESL (FreeSWITCH) or AMI (Asterisk)
- AMI client (raw TCP) for Asterisk Manager Interface
- Collector interface: collect, connect, subscribe hangup events, close
- FreeSWITCH collector: show channels, max_sessions, idle_cpu
- Asterisk collector: CoreShowChannels, CoreSettings
- System collector: /proc/loadavg, /proc/stat (CPU idle)
- Failure tracker: sliding window for call_failure_rate
- NATS publisher: pulse.metrics.<node_id> every 5 seconds
- Graceful shutdown, retry logic (5 failures → status=down)
- Template configs for UC nodes (Asterisk) and ses-sip (FreeSWITCH)
2026-06-25 16:59:10 +07:00
6 changed files with 30 additions and 30 deletions

View File

@ -11,11 +11,11 @@
// { // {
// "node_id": "uc06", // "node_id": "uc06",
// "type": "asterisk", // "type": "asterisk",
// "nats_url": "nats://balancer.lan:4222", // "nats_url": "nats://10.101.60.81:4222",
// "sip_gateway": "sip:uc-pbx.lan:5060", // "sip_gateway": "sip:10.101.60.115:5060",
// "interval_sec": 5, // "interval_sec": 5,
// "max_calls": 150, // "max_calls": 150,
// "ami": { "host": "127.0.0.1", "port": 6154, "username": "admin", "password": "changeme" } // "ami": { "host": "127.0.0.1", "port": 6154, "username": "ctt", "password": "cttpass" }
// } // }
package agent package agent

View File

@ -11,11 +11,11 @@
// { // {
// "node_id": "uc06", // "node_id": "uc06",
// "type": "asterisk", // "type": "asterisk",
// "nats_url": "nats://balancer.lan:4222", // "nats_url": "nats://10.101.60.81:4222",
// "sip_gateway": "sip:uc-pbx.lan:5060", // "sip_gateway": "sip:10.101.60.115:5060",
// "interval_sec": 5, // "interval_sec": 5,
// "max_calls": 150, // "max_calls": 150,
// "ami": { "host": "127.0.0.1", "port": 6154, "username": "admin", "password": "changeme" } // "ami": { "host": "127.0.0.1", "port": 6154, "username": "ctt", "password": "cttpass" }
// } // }
package main package main

View File

@ -1,10 +1,10 @@
{ {
"node_id": "ses-pbx", "node_id": "ses-sip",
"type": "freeswitch", "type": "freeswitch",
"nats_url": "nats://balancer.lan:4222", "nats_url": "nats://10.101.60.81:4222",
"interval_sec": 5, "interval_sec": 5,
"max_calls": 250, "max_calls": 250,
"sip_gateway": "sip:ses-pbx.lan:5060", "sip_gateway": "sip:10.3.0.44:5060",
"sip_gateway_auto": false, "sip_gateway_auto": false,
"failure_window": 1000, "failure_window": 1000,
"esl": { "esl": {

View File

@ -1,16 +1,16 @@
{ {
"node_id": "uc-pbx", "node_id": "uc06",
"type": "asterisk", "type": "asterisk",
"nats_url": "nats://balancer.lan:4222", "nats_url": "nats://10.101.60.81:4222",
"interval_sec": 5, "interval_sec": 5,
"max_calls": 150, "max_calls": 150,
"sip_gateway": "sip:uc-pbx.lan:5060", "sip_gateway": "sip:10.101.60.115:5060",
"sip_gateway_auto": false, "sip_gateway_auto": false,
"failure_window": 1000, "failure_window": 1000,
"ami": { "ami": {
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 6154, "port": 6154,
"username": "admin", "username": "ctt",
"password": "changeme" "password": "cttpass"
} }
} }

View File

@ -48,24 +48,24 @@ make build-agent
```json ```json
{ {
"node_id": "pbx-01", "node_id": "uc06",
"type": "asterisk", "type": "asterisk",
"nats_url": "nats://balancer.host:4222", "nats_url": "nats://balancer.host:4222",
"nats_user": "", "nats_user": "",
"nats_password": "", "nats_password": "",
"interval_sec": 5, "interval_sec": 5,
"max_calls": 150, "max_calls": 150,
"sip_gateway": "sip:pbx-01.lan:5060", "sip_gateway": "sip:10.101.60.115:5060",
"sip_gateway_auto": false, "sip_gateway_auto": false,
"failure_window": 1000, "failure_window": 1000,
"esl": { "host": "127.0.0.1", "port": 8021, "password": "ClueCon" }, "esl": { "host": "127.0.0.1", "port": 8021, "password": "ClueCon" },
"ami": { "host": "127.0.0.1", "port": 6154, "username": "admin", "password": "changeme" } "ami": { "host": "127.0.0.1", "port": 6154, "username": "ctt", "password": "cttpass" }
} }
``` ```
| Field | Type | Default | Description | | Field | Type | Default | Description |
|-------|------|---------|-------------| |-------|------|---------|-------------|
| `node_id` | string | — | Unique node ID (pbx-01, pbx-02) | | `node_id` | string | — | Unique node ID (uc06, ses-sip) |
| `type` | string | — | `freeswitch` or `asterisk` | | `type` | string | — | `freeswitch` or `asterisk` |
| `nats_url` | string | — | NATS server URL | | `nats_url` | string | — | NATS server URL |
| `nats_user` | string | `""` | NATS username | | `nats_user` | string | `""` | NATS username |
@ -113,8 +113,8 @@ New PBX type = new `collector_<type>.go` implementing 5 interface methods. No ex
| File | Target | Type | | File | Target | Type |
|------|--------|------| |------|--------|------|
| `contrib/agent-uc.json` | Asterisk UC nodes | asterisk | | `contrib/agent-uc.json` | Asterisk UC nodes (05, 06, 66-69) | asterisk |
| `contrib/agent-sessip.json` | FreeSWITCH ses-sip | freeswitch | | `contrib/agent-sessip.json` | FreeSWITCH ses-sip (10.3.0.44) | freeswitch |
## Deployment ## Deployment
@ -123,13 +123,13 @@ New PBX type = new `collector_<type>.go` implementing 5 interface methods. No ex
make build-agent make build-agent
# 2. Copy to node # 2. Copy to node
scp bin/pulse-lets-go-agent admin@pbx-node:/usr/local/bin/ scp bin/pulse-lets-go-agent devadmin@NODE_IP:/usr/local/bin/
# 3. Create config # 3. Create config
scp contrib/agent-uc.json admin@pbx-node:/etc/pulse-lets-go-agent/agent.json scp contrib/agent-uc.json devadmin@NODE_IP:/etc/pulse-lets-go-agent/agent.json
# 4. Start # 4. Start
ssh admin@pbx-node "pulse-lets-go-agent -config /etc/pulse-lets-go-agent/agent.json" ssh devadmin@NODE_IP "pulse-lets-go-agent -config /etc/pulse-lets-go-agent/agent.json"
``` ```
## Systemd Unit ## Systemd Unit

View File

@ -60,14 +60,14 @@ WantedBy=multi-user.target
```bash ```bash
make deploy make deploy
scp -r /opt/pulse-lets-go admin@balancer.lan:/opt/ scp -r /opt/pulse-lets-go devadmin@10.101.60.81:/opt/
scp contrib/route.lua admin@balancer.lan:/tmp/ scp contrib/route.lua devadmin@10.101.60.81:/tmp/
``` ```
### Step 2 — NATS Server (if not installed) ### Step 2 — NATS Server (if not installed)
```bash ```bash
ssh admin@balancer.lan ssh devadmin@10.101.60.81
sudo mkdir -p /opt/nats sudo mkdir -p /opt/nats
# Install nats-server from repository or copy binary # Install nats-server from repository or copy binary
# Run: nats-server -p 4222 -D & # Run: nats-server -p 4222 -D &
@ -79,11 +79,11 @@ Repeat for each UC/Asterisk node:
```bash ```bash
# Copy binary and config to each PBX: # Copy binary and config to each PBX:
scp bin/pulse-lets-go-agent admin@pbx-node:/usr/local/bin/ scp bin/pulse-lets-go-agent devadmin@PBX_IP:/usr/local/bin/
scp contrib/agent-uc.json admin@pbx-node:/etc/pulse-lets-go-agent/agent.json scp contrib/agent-uc.json devadmin@PBX_IP:/etc/pulse-lets-go-agent/agent.json
# Start: # Start:
ssh admin@pbx-node "systemctl start pulse-lets-go-agent" ssh devadmin@PBX_IP "systemctl start pulse-lets-go-agent"
# Check log: # Check log:
# journalctl -u pulse-lets-go-agent -f # journalctl -u pulse-lets-go-agent -f
@ -121,7 +121,7 @@ Trunks are created automatically when an agent sends its first metric with `sip_
```bash ```bash
curl -X POST .../api/trunks \ curl -X POST .../api/trunks \
-d '{"name":"pbx-01","type":"balance","node_id":"pbx-01","gateway":"sip:pbx-01.lan:5060","enabled":true}' -d '{"name":"uc06","type":"balance","node_id":"uc06","gateway":"sip:10.101.60.115:5060","enabled":true}'
``` ```
### Step 7 — Start pulse-lets-go ### Step 7 — Start pulse-lets-go