feat: auto-trunk creation, SIP gateway in metrics, gateway WS broadcast

- SIPGateway field in NodeMetric model
- HasNode() engine method
- OnNewNode callback in NATS subscriber
- Auto-create balance trunk from new node metric
- BroadcastGatewayEvent on WS hub
- Gateway status indicators in trunks UI (green/red/gray)
- SPA fallback for frontend routing
This commit is contained in:
Maksim Totmin
2026-06-25 16:56:49 +07:00
parent 533a1fba49
commit f25f8e4d01
6 changed files with 141 additions and 10 deletions
+68 -1
View File
@@ -5,6 +5,8 @@ package main
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"net/http"
@@ -20,6 +22,7 @@ import (
"github.com/pulse-lets-go/internal/engine"
"github.com/pulse-lets-go/internal/esl"
filelog "github.com/pulse-lets-go/internal/log"
"github.com/pulse-lets-go/internal/models"
"github.com/pulse-lets-go/internal/nats"
)
@@ -73,6 +76,42 @@ func main() {
}
defer sub.Close()
// Авто-создание balance-транков при первой метрике от новой ноды
sub.SetOnNewNode(func(nodeID, sipGateway string) {
trunks, err := cfgMgr.ReadTrunks()
if err != nil {
log.Printf("[main] ошибка чтения транков для авто-создания: %v", err)
return
}
// Проверяем идемпотентность: транк уже существует?
for _, t := range trunks {
if t.Type == "balance" && t.NodeID == nodeID {
return
}
}
// Создаём новый balance-транк
now := time.Now().UTC()
trunkID := "trk-" + randomHex(6)
trunk := models.Trunk{
ID: trunkID,
Name: fmt.Sprintf("%s баланс", nodeID),
Type: "balance",
NodeID: nodeID,
Gateway: sipGateway,
Codecs: []string{"PCMU", "PCMA"},
Context: "default",
Enabled: true,
CreatedAt: now,
UpdatedAt: now,
}
trunks = append(trunks, trunk)
if err := cfgMgr.SaveTrunks(trunks); err != nil {
log.Printf("[main] ошибка сохранения авто-созданного транка: %v", err)
return
}
log.Printf("[nats] авто-создан balance-транк: %s → %s (узел: %s)", trunkID, sipGateway, nodeID)
})
// 6. HTTP API
apiHandler := api.NewAPI(eng, cfgMgr, cfg.JWTSecret, cfg.MonitoringAPIKey, sub.IsConnected, cfg)
handler := apiHandler.Handler()
@@ -105,7 +144,26 @@ func main() {
eslClient.GatewaySyncAll(cfg.ESL.SofiaProfile, specs)
},
func(eventName string, headers map[string]string, body string) {
log.Printf("[esl] событие: %s", eventName)
gwName := headers["Gateway-Name"]
if gwName == "" {
return
}
// Извлекаем trunk_id из имени gateway: "pulse-ingress-trk-X" → "trk-X"
prefix := cfg.ESL.GatewayPrefix + "-ingress-"
trunkID := strings.TrimPrefix(gwName, prefix)
if trunkID == gwName {
return // префикс не найден, не наш gateway
}
status := "unknown"
switch eventName {
case "SOFIA::gateway_register":
status = "up"
case "SOFIA::gateway_unregister", "SOFIA::gateway_expire":
status = "down"
}
apiHandler.BroadcastGatewayEvent(gwName, trunkID, status)
},
)
cancel()
@@ -236,6 +294,15 @@ func withSPA(webDir string, apiHandler http.Handler) http.Handler {
})
}
// randomHex генерирует случайную hex-строку заданной длины.
func randomHex(n int) string {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("%x", time.Now().UnixNano())[:n]
}
return hex.EncodeToString(b)[:n]
}
// extractHostFromGateway извлекает хост из SIP URI.
// "sip:mts-gw.lan:5060" → "mts-gw.lan"
func extractHostFromGateway(gateway string) string {