mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 21:54:10 +00:00
171 lines
4.9 KiB
Go
171 lines
4.9 KiB
Go
|
|
package controller
|
||
|
|
|
||
|
|
import (
|
||
|
|
"os"
|
||
|
|
"strconv"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||
|
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
)
|
||
|
|
|
||
|
|
// NodeController handles node management API endpoints.
|
||
|
|
type NodeController struct {
|
||
|
|
BaseController
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewNodeController creates a new NodeController and initializes its routes.
|
||
|
|
func NewNodeController(g *gin.RouterGroup) *NodeController {
|
||
|
|
a := &NodeController{}
|
||
|
|
a.initRouter(g)
|
||
|
|
return a
|
||
|
|
}
|
||
|
|
|
||
|
|
// initRouter sets up the routes for node management.
|
||
|
|
func (a *NodeController) initRouter(g *gin.RouterGroup) {
|
||
|
|
g.GET("/list", a.list)
|
||
|
|
g.GET("/config", a.getConfig)
|
||
|
|
g.POST("/config", a.updateConfig)
|
||
|
|
}
|
||
|
|
|
||
|
|
// NodeView is the JSON shape returned to the frontend for each node.
|
||
|
|
type NodeView struct {
|
||
|
|
NodeID string `json:"nodeId"`
|
||
|
|
NodeRole string `json:"nodeRole"`
|
||
|
|
Online bool `json:"online"`
|
||
|
|
LastHeartbeatAt int64 `json:"lastHeartbeatAt"`
|
||
|
|
LastSyncAt int64 `json:"lastSyncAt"`
|
||
|
|
LastSeenVersion int64 `json:"lastSeenVersion"`
|
||
|
|
LastError string `json:"lastError"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// list returns connected nodes. Master sees all workers; worker sees the master.
|
||
|
|
func (a *NodeController) list(c *gin.Context) {
|
||
|
|
nodeCfg := config.GetNodeConfigFromJSON()
|
||
|
|
states, err := database.GetNodeStates()
|
||
|
|
if err != nil {
|
||
|
|
jsonMsg(c, "get node states", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
syncInterval := nodeCfg.SyncIntervalSeconds
|
||
|
|
if syncInterval <= 0 {
|
||
|
|
syncInterval = 30
|
||
|
|
}
|
||
|
|
offlineThreshold := int64(syncInterval * 2)
|
||
|
|
now := time.Now().Unix()
|
||
|
|
|
||
|
|
var nodes []NodeView
|
||
|
|
for _, s := range states {
|
||
|
|
// Master shows workers; worker shows master
|
||
|
|
if nodeCfg.Role == config.NodeRoleMaster && s.NodeRole != string(config.NodeRoleWorker) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if nodeCfg.Role == config.NodeRoleWorker && s.NodeRole != string(config.NodeRoleMaster) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
online := (now - s.LastHeartbeatAt) < offlineThreshold
|
||
|
|
nodes = append(nodes, NodeView{
|
||
|
|
NodeID: s.NodeID,
|
||
|
|
NodeRole: s.NodeRole,
|
||
|
|
Online: online,
|
||
|
|
LastHeartbeatAt: s.LastHeartbeatAt,
|
||
|
|
LastSyncAt: s.LastSyncAt,
|
||
|
|
LastSeenVersion: s.LastSeenVersion,
|
||
|
|
LastError: s.LastError,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
if nodes == nil {
|
||
|
|
nodes = []NodeView{}
|
||
|
|
}
|
||
|
|
|
||
|
|
jsonObj(c, nodes, nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
// NodeConfigView is the JSON shape for node configuration.
|
||
|
|
type NodeConfigView struct {
|
||
|
|
Role string `json:"role"`
|
||
|
|
NodeID string `json:"nodeId"`
|
||
|
|
SyncInterval int `json:"syncInterval"`
|
||
|
|
TrafficFlushInterval int `json:"trafficFlushInterval"`
|
||
|
|
DBType string `json:"dbType"`
|
||
|
|
DBHost string `json:"dbHost"`
|
||
|
|
DBPort string `json:"dbPort"`
|
||
|
|
DBUser string `json:"dbUser"`
|
||
|
|
DBPass string `json:"dbPass"`
|
||
|
|
DBName string `json:"dbName"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// getConfig returns the current node configuration.
|
||
|
|
func (a *NodeController) getConfig(c *gin.Context) {
|
||
|
|
nodeCfg := config.GetNodeConfigFromJSON()
|
||
|
|
dbCfg := config.GetDBConfigFromJSON()
|
||
|
|
|
||
|
|
jsonObj(c, NodeConfigView{
|
||
|
|
Role: string(nodeCfg.Role),
|
||
|
|
NodeID: nodeCfg.NodeID,
|
||
|
|
SyncInterval: nodeCfg.SyncIntervalSeconds,
|
||
|
|
TrafficFlushInterval: nodeCfg.TrafficFlushSeconds,
|
||
|
|
DBType: dbCfg.Type,
|
||
|
|
DBHost: dbCfg.Host,
|
||
|
|
DBPort: dbCfg.Port,
|
||
|
|
DBUser: dbCfg.User,
|
||
|
|
DBPass: dbCfg.Password,
|
||
|
|
DBName: dbCfg.Name,
|
||
|
|
}, nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
// updateConfigRequest is the JSON body for updating node config.
|
||
|
|
type updateConfigRequest struct {
|
||
|
|
SyncInterval int `json:"syncInterval"`
|
||
|
|
TrafficFlushInterval int `json:"trafficFlushInterval"`
|
||
|
|
DBType string `json:"dbType"`
|
||
|
|
DBHost string `json:"dbHost"`
|
||
|
|
DBPort string `json:"dbPort"`
|
||
|
|
DBUser string `json:"dbUser"`
|
||
|
|
DBPass string `json:"dbPass"`
|
||
|
|
DBName string `json:"dbName"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// updateConfig updates the node configuration in x-ui.json.
|
||
|
|
func (a *NodeController) updateConfig(c *gin.Context) {
|
||
|
|
var req updateConfigRequest
|
||
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||
|
|
jsonMsg(c, "invalid request", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate
|
||
|
|
if req.SyncInterval <= 0 {
|
||
|
|
jsonMsg(c, "syncInterval must be positive", os.ErrInvalid)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if req.TrafficFlushInterval <= 0 {
|
||
|
|
jsonMsg(c, "trafficFlushInterval must be positive", os.ErrInvalid)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Write each setting to JSON config
|
||
|
|
settings := map[string]string{
|
||
|
|
"syncInterval": strconv.Itoa(req.SyncInterval),
|
||
|
|
"trafficFlushInterval": strconv.Itoa(req.TrafficFlushInterval),
|
||
|
|
"dbType": req.DBType,
|
||
|
|
"dbHost": req.DBHost,
|
||
|
|
"dbPort": req.DBPort,
|
||
|
|
"dbUser": req.DBUser,
|
||
|
|
"dbPassword": req.DBPass,
|
||
|
|
"dbName": req.DBName,
|
||
|
|
}
|
||
|
|
|
||
|
|
for key, value := range settings {
|
||
|
|
if err := config.WriteSettingToJSON(key, value); err != nil {
|
||
|
|
jsonMsg(c, "save "+key, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.saveSuccess"), nil)
|
||
|
|
}
|