mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-24 21:46:07 +00:00
feat(nodes): bulk panel self-update with live online indicator
Adds the ability to update node panels to the latest release from the Nodes page: select online, enabled nodes (checkboxes) and trigger their official self-updater, or use the per-row Update action. A node whose reported panel version trails the latest GitHub release is flagged with an 'update available' tag (compared via lib/panel-version, mirroring the Go isNewerVersion). Backend: Remote.UpdatePanel calls the node's existing POST /panel/api/server/updatePanel; NodeService.UpdatePanels fans out over the selected ids, skipping disabled/offline nodes with a per-node reason; exposed as POST /panel/api/nodes/updatePanel (documented in endpoints.ts + openapi.json). The bulk request sends a JSON body, so it sets Content-Type: application/json explicitly — axios defaults POST to form-urlencoded, which made ShouldBindJSON fail with 'invalid character i'. Also reuses the clients-page online cue on the Nodes page: a pulsing green dot plus green label for an online node. The .online-dot style moved to the shared styles/utils.css so both pages load it. Translations for all new node keys added across every language file.
This commit is contained in:
@@ -246,6 +246,56 @@ func (s *NodeService) SetEnable(id int, enable bool) error {
|
||||
return db.Model(model.Node{}).Where("id = ?", id).Update("enable", enable).Error
|
||||
}
|
||||
|
||||
// NodeUpdateResult reports the outcome of triggering a panel self-update on one
|
||||
// node so the UI can show per-node success/failure for a bulk request.
|
||||
type NodeUpdateResult struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// UpdatePanels triggers the official self-updater on each given node. Only
|
||||
// enabled, online nodes are eligible — an offline node can't be reached, so it
|
||||
// is reported as skipped rather than silently dropped.
|
||||
func (s *NodeService) UpdatePanels(ids []int) ([]NodeUpdateResult, error) {
|
||||
mgr := runtime.GetManager()
|
||||
if mgr == nil {
|
||||
return nil, fmt.Errorf("runtime manager unavailable")
|
||||
}
|
||||
results := make([]NodeUpdateResult, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
n, err := s.GetById(id)
|
||||
if err != nil || n == nil {
|
||||
results = append(results, NodeUpdateResult{Id: id, OK: false, Error: "node not found"})
|
||||
continue
|
||||
}
|
||||
res := NodeUpdateResult{Id: id, Name: n.Name}
|
||||
switch {
|
||||
case !n.Enable:
|
||||
res.Error = "node is disabled"
|
||||
case n.Status != "online":
|
||||
res.Error = "node is offline"
|
||||
default:
|
||||
remote, remoteErr := mgr.RemoteFor(n)
|
||||
if remoteErr != nil {
|
||||
res.Error = remoteErr.Error()
|
||||
break
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
updErr := remote.UpdatePanel(ctx)
|
||||
cancel()
|
||||
if updErr != nil {
|
||||
res.Error = updErr.Error()
|
||||
} else {
|
||||
res.OK = true
|
||||
}
|
||||
}
|
||||
results = append(results, res)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *NodeService) UpdateHeartbeat(id int, p HeartbeatPatch) error {
|
||||
db := database.GetDB()
|
||||
updates := map[string]any{
|
||||
|
||||
Reference in New Issue
Block a user