diff --git a/database/db.go b/database/db.go index 2e468587..80f9f655 100644 --- a/database/db.go +++ b/database/db.go @@ -39,6 +39,7 @@ func initModels() error { &xray.ClientTraffic{}, &model.HistoryOfSeeders{}, &model.CustomGeoResource{}, + &model.Node{}, } for _, model := range models { if err := db.AutoMigrate(model); err != nil { diff --git a/database/model/model.go b/database/model/model.go index 047780e5..10036df4 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -66,6 +66,12 @@ type Inbound struct { StreamSettings string `json:"streamSettings" form:"streamSettings"` Tag string `json:"tag" form:"tag" gorm:"unique"` Sniffing string `json:"sniffing" form:"sniffing"` + + // NodeID points at the remote panel (Node) where this inbound's xray + // actually runs. NULL means the inbound runs on the local xray (the + // pre-multi-node behaviour). Existing rows migrate to NULL with no + // backfill. + NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"` } // OutboundTraffics tracks traffic statistics for Xray outbound connections. @@ -117,6 +123,37 @@ type Setting struct { Value string `json:"value" form:"value"` } +// Node represents a remote 3x-ui panel registered with the central panel. +// The central panel polls each node's existing /panel/api/server/status +// endpoint over HTTP using the per-node ApiToken to populate the runtime +// status fields below. +type Node struct { + Id int `json:"id" gorm:"primaryKey;autoIncrement"` + Name string `json:"name" gorm:"uniqueIndex"` + Remark string `json:"remark"` + Scheme string `json:"scheme"` // "https" | "http" + Address string `json:"address"` // host or IP + Port int `json:"port"` + BasePath string `json:"basePath"` // "/" or "/myprefix/" + ApiToken string `json:"apiToken"` // plaintext, matches existing tg/ldap pattern + Enable bool `json:"enable" gorm:"default:true"` + + // Heartbeat-updated fields. UpdatedAt advances on every probe even when + // the row is otherwise unchanged so the UI's "last seen" tooltip is + // truthful without us having to read LastHeartbeat separately. + Status string `json:"status" gorm:"default:unknown"` // online|offline|unknown + LastHeartbeat int64 `json:"lastHeartbeat"` // unix seconds, 0 = never + LatencyMs int `json:"latencyMs"` + XrayVersion string `json:"xrayVersion"` + CpuPct float64 `json:"cpuPct"` + MemPct float64 `json:"memPct"` + UptimeSecs uint64 `json:"uptimeSecs"` + LastError string `json:"lastError"` + + CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime"` + UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime"` +} + type CustomGeoResource struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` Type string `json:"type" gorm:"not null;uniqueIndex:idx_custom_geo_type_alias;column:geo_type"` diff --git a/frontend/nodes.html b/frontend/nodes.html new file mode 100644 index 00000000..fb607b19 --- /dev/null +++ b/frontend/nodes.html @@ -0,0 +1,13 @@ + + +
+ + +