mirror of
				https://github.com/komari-monitor/komari.git
				synced 2025-11-04 05:53:15 +00:00 
			
		
		
		
	移除 远程配置下发。
修改 客户端基本属性逻辑。 修改 最新上报key改为uuid 新增 客户端命名 新增 历史记录数据库压缩。 新增 在线节点信息。 新增 公开API /api/nodes 显示客户端基础属性
This commit is contained in:
		@@ -4,21 +4,16 @@ import (
 | 
				
			|||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/akizon77/komari/common"
 | 
					 | 
				
			||||||
	"github.com/akizon77/komari/database/clients"
 | 
						"github.com/akizon77/komari/database/clients"
 | 
				
			||||||
 | 
						"github.com/akizon77/komari/database/dbcore"
 | 
				
			||||||
	"github.com/akizon77/komari/database/history"
 | 
						"github.com/akizon77/komari/database/history"
 | 
				
			||||||
 | 
						"github.com/akizon77/komari/database/models"
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
	"gorm.io/gorm"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func AddClient(c *gin.Context) {
 | 
					func AddClient(c *gin.Context) {
 | 
				
			||||||
	var config common.ClientConfig
 | 
					 | 
				
			||||||
	if err := c.ShouldBindJSON(&config); err != nil {
 | 
					 | 
				
			||||||
		c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": err.Error()})
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	uuid, token, err := clients.CreateClient(config)
 | 
						uuid, token, err := clients.CreateClient()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
							c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -30,9 +25,8 @@ func AddClient(c *gin.Context) {
 | 
				
			|||||||
func EditClient(c *gin.Context) {
 | 
					func EditClient(c *gin.Context) {
 | 
				
			||||||
	var req struct {
 | 
						var req struct {
 | 
				
			||||||
		UUID       string `json:"uuid" binding:"required"`
 | 
							UUID       string `json:"uuid" binding:"required"`
 | 
				
			||||||
		ClientName string              `json:"client_name,omitempty"`
 | 
							ClientName string `json:"name,omitempty"`
 | 
				
			||||||
		Token      string `json:"token,omitempty"`
 | 
							Token      string `json:"token,omitempty"`
 | 
				
			||||||
		Config     common.ClientConfig `json:"config,omitempty"`
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := c.ShouldBindJSON(&req); err != nil {
 | 
						if err := c.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
@@ -40,49 +34,19 @@ func EditClient(c *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 验证配置
 | 
						updates := make(map[string]interface{})
 | 
				
			||||||
	if req.Config.Interval <= 0 {
 | 
						updates["updated_at"] = time.Now()
 | 
				
			||||||
		c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Interval must be greater than 0"})
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 获取原始配置
 | 
					 | 
				
			||||||
	rawConfig, err := clients.GetClientConfig(req.UUID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 更新配置
 | 
					 | 
				
			||||||
	if req.Config.ClientUUID != "" {
 | 
					 | 
				
			||||||
		// 确保创建时间不变
 | 
					 | 
				
			||||||
		req.Config.CreatedAt = rawConfig.CreatedAt
 | 
					 | 
				
			||||||
		req.Config.UpdatedAt = time.Now()
 | 
					 | 
				
			||||||
		err = clients.UpdateClientConfig(req.Config)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 更新客户端名称
 | 
					 | 
				
			||||||
	if req.ClientName != "" {
 | 
						if req.ClientName != "" {
 | 
				
			||||||
		err = clients.EditClientName(req.UUID, req.ClientName)
 | 
							updates["client_name"] = req.ClientName
 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 更新token
 | 
					 | 
				
			||||||
	if req.Token != "" {
 | 
						if req.Token != "" {
 | 
				
			||||||
		err = clients.EditClientToken(req.UUID, req.Token)
 | 
							updates["token"] = req.Token
 | 
				
			||||||
		if err != nil {
 | 
						}
 | 
				
			||||||
 | 
						db := dbcore.GetDBInstance()
 | 
				
			||||||
 | 
						if err := db.Model(&models.Client{}).Where("uuid = ?", req.UUID).Updates(updates).Error; err != nil {
 | 
				
			||||||
		c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
							c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.JSON(http.StatusOK, gin.H{"status": "success"})
 | 
						c.JSON(http.StatusOK, gin.H{"status": "success"})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -128,72 +92,21 @@ func GetClient(c *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	result := getClientByUUID(uuid)
 | 
						result, err := clients.GetClientByUUID(uuid)
 | 
				
			||||||
	if result == nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "Failed to get client"})
 | 
							c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.JSON(http.StatusOK, result)
 | 
						c.JSON(http.StatusOK, result)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func getClientByUUID(uuid string) map[string]interface{} {
 | 
					 | 
				
			||||||
	clientBasicInfo, err := clients.GetClientBasicInfo(uuid)
 | 
					 | 
				
			||||||
	if err == gorm.ErrRecordNotFound {
 | 
					 | 
				
			||||||
		clientBasicInfo = clients.ClientBasicInfo{}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	config, err := clients.GetClientConfig(uuid)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	client, err := clients.GetClientByUUID(uuid)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	result := map[string]interface{}{
 | 
					 | 
				
			||||||
		"uuid":   uuid,
 | 
					 | 
				
			||||||
		"token":  client.Token,
 | 
					 | 
				
			||||||
		"info":   clientBasicInfo,
 | 
					 | 
				
			||||||
		"config": config,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return result
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func getClientInfo(uuid string) (map[string]interface{}, error) {
 | 
					 | 
				
			||||||
	clientBasicInfo, err := clients.GetClientBasicInfo(uuid)
 | 
					 | 
				
			||||||
	if err == gorm.ErrRecordNotFound {
 | 
					 | 
				
			||||||
		clientBasicInfo = clients.ClientBasicInfo{}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	config, err := clients.GetClientConfig(uuid)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	client, err := clients.GetClientByUUID(uuid)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return map[string]interface{}{
 | 
					 | 
				
			||||||
		"uuid":   uuid,
 | 
					 | 
				
			||||||
		"token":  client.Token,
 | 
					 | 
				
			||||||
		"name":   client.ClientName,
 | 
					 | 
				
			||||||
		"info":   clientBasicInfo,
 | 
					 | 
				
			||||||
		"config": config,
 | 
					 | 
				
			||||||
	}, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func ListClients(c *gin.Context) {
 | 
					func ListClients(c *gin.Context) {
 | 
				
			||||||
	cls, err := clients.GetAllClients()
 | 
						cls, err := clients.GetAllClientBasicInfo()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
							c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	result := []map[string]interface{}{}
 | 
					
 | 
				
			||||||
	for i := range cls {
 | 
						c.JSON(http.StatusOK, cls)
 | 
				
			||||||
		clientInfo, err := getClientInfo(cls[i].UUID)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		result = append(result, clientInfo)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	c.JSON(http.StatusOK, result)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,36 +0,0 @@
 | 
				
			|||||||
package client
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/akizon77/komari/database/clients"
 | 
					 | 
				
			||||||
	"gorm.io/gorm"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func GetRemoteConfig(c *gin.Context) {
 | 
					 | 
				
			||||||
	token := c.Query("token")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	clientUUID, err := clients.GetClientUUIDByToken(token)
 | 
					 | 
				
			||||||
	if err == gorm.ErrRecordNotFound {
 | 
					 | 
				
			||||||
		c.JSON(http.StatusNotFound, gin.H{"status": "error", "error": "No data found"})
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	} else if err != nil {
 | 
					 | 
				
			||||||
		c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("%v", err)})
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	config, err := clients.GetClientConfig(clientUUID)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err == gorm.ErrRecordNotFound {
 | 
					 | 
				
			||||||
		c.JSON(http.StatusNotFound, gin.H{"status": "error", "error": "No data found"})
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	} else if err != nil {
 | 
					 | 
				
			||||||
		c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": fmt.Errorf("error querying client: %v", err)})
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.JSON(http.StatusOK, config)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -44,13 +44,14 @@ func UploadReport(c *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	// Update report with method and token
 | 
						// Update report with method and token
 | 
				
			||||||
	report.Token = ""
 | 
						report.Token = ""
 | 
				
			||||||
	ws.LatestReport[report.UUID] = report
 | 
						ws.LatestReport[report.UUID] = &report
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Restore the body for further use
 | 
						c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Restore the body for further use
 | 
				
			||||||
	c.JSON(200, gin.H{"status": "success"})
 | 
						c.JSON(200, gin.H{"status": "success"})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func WebSocketReport(c *gin.Context) {
 | 
					func WebSocketReport(c *gin.Context) {
 | 
				
			||||||
 | 
						// 升级ws
 | 
				
			||||||
	if !websocket.IsWebSocketUpgrade(c.Request) {
 | 
						if !websocket.IsWebSocketUpgrade(c.Request) {
 | 
				
			||||||
		c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Require WebSocket upgrade"})
 | 
							c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Require WebSocket upgrade"})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -74,6 +75,7 @@ func WebSocketReport(c *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 第一次数据拿token
 | 
				
			||||||
	data := map[string]interface{}{}
 | 
						data := map[string]interface{}{}
 | 
				
			||||||
	err = json.Unmarshal(message, &data)
 | 
						err = json.Unmarshal(message, &data)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -103,21 +105,22 @@ func WebSocketReport(c *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if a connection with the same token already exists
 | 
						uuid, err := clients.GetClientUUIDByToken(token)
 | 
				
			||||||
	if _, exists := ws.ConnectedClients[token]; exists {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							conn.WriteJSON(gin.H{"status": "error", "error": errMsg})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 只允许一个客户端的连接
 | 
				
			||||||
 | 
						if _, exists := ws.ConnectedClients[uuid]; exists {
 | 
				
			||||||
		conn.WriteJSON(gin.H{"status": "error", "error": "Token already in use"})
 | 
							conn.WriteJSON(gin.H{"status": "error", "error": "Token already in use"})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	ws.ConnectedClients[token] = conn
 | 
					 | 
				
			||||||
	defer func() {
 | 
					 | 
				
			||||||
		delete(ws.ConnectedClients, token)
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	clientUUID, err := clients.GetClientUUIDByToken(token)
 | 
						ws.ConnectedClients[uuid] = conn
 | 
				
			||||||
	if err != nil {
 | 
						defer func() {
 | 
				
			||||||
		conn.WriteJSON(gin.H{"status": "error", "error": fmt.Sprintf("%v", err)})
 | 
							delete(ws.ConnectedClients, uuid)
 | 
				
			||||||
		return
 | 
						}()
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for {
 | 
						for {
 | 
				
			||||||
		_, message, err := conn.ReadMessage()
 | 
							_, message, err := conn.ReadMessage()
 | 
				
			||||||
@@ -131,17 +134,12 @@ func WebSocketReport(c *gin.Context) {
 | 
				
			|||||||
			break
 | 
								break
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err = clients.SaveClientReport(clientUUID, report)
 | 
							err = clients.SaveClientReport(uuid, report)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			conn.WriteJSON(gin.H{"status": "error", "error": fmt.Sprintf("%v", err)})
 | 
								conn.WriteJSON(gin.H{"status": "error", "error": fmt.Sprintf("%v", err)})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		uuid, err := clients.GetClientUUIDByToken(token)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			conn.WriteJSON(gin.H{"status": "error", "error": fmt.Sprintf("%v", err)})
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		report.Token = ""
 | 
							report.Token = ""
 | 
				
			||||||
		ws.LatestReport[uuid] = report
 | 
							ws.LatestReport[uuid] = &report
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,14 @@
 | 
				
			|||||||
package client
 | 
					package client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/akizon77/komari/common"
 | 
				
			||||||
	"github.com/akizon77/komari/database/clients"
 | 
						"github.com/akizon77/komari/database/clients"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func UploadBasicInfo(c *gin.Context) {
 | 
					func UploadBasicInfo(c *gin.Context) {
 | 
				
			||||||
	var cbi = &clients.ClientBasicInfo{}
 | 
						var cbi = &common.ClientInfo{}
 | 
				
			||||||
	err := c.ShouldBindJSON(&cbi)
 | 
						err := c.ShouldBindJSON(&cbi)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		c.JSON(400, gin.H{"status": "error", "error": err.Error()})
 | 
							c.JSON(400, gin.H{"status": "error", "error": err.Error()})
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								api/nodes.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/nodes.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					package api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/akizon77/komari/database/clients"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetNodesInformation(c *gin.Context) {
 | 
				
			||||||
 | 
						clientList, err := clients.GetAllClientBasicInfo()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(500, gin.H{"status": "error", "error": err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						count := len(clientList)
 | 
				
			||||||
 | 
						// 公开信息不展示IP地址
 | 
				
			||||||
 | 
						for i := 0; i < count; i++ {
 | 
				
			||||||
 | 
							clientList[i].IPv4 = ""
 | 
				
			||||||
 | 
							clientList[i].IPv6 = ""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.JSON(200, gin.H{"status": "success", "data": clientList})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -25,10 +25,10 @@ var ServerCmd = &cobra.Command{
 | 
				
			|||||||
		r.POST("/api/login", api.Login)
 | 
							r.POST("/api/login", api.Login)
 | 
				
			||||||
		r.GET("/api/me", api.GetMe)
 | 
							r.GET("/api/me", api.GetMe)
 | 
				
			||||||
		r.GET("/api/clients", ws.GetClients)
 | 
							r.GET("/api/clients", ws.GetClients)
 | 
				
			||||||
 | 
							r.GET("/api/nodes", api.GetNodesInformation)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		tokenAuthrized := r.Group("/api/clients", api.TokenAuthMiddleware())
 | 
							tokenAuthrized := r.Group("/api/clients", api.TokenAuthMiddleware())
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			tokenAuthrized.GET("/getRemoteConfig", client.GetRemoteConfig)
 | 
					 | 
				
			||||||
			tokenAuthrized.GET("/report", client.WebSocketReport) // websocket
 | 
								tokenAuthrized.GET("/report", client.WebSocketReport) // websocket
 | 
				
			||||||
			tokenAuthrized.POST("/uploadBasicInfo", client.UploadBasicInfo)
 | 
								tokenAuthrized.POST("/uploadBasicInfo", client.UploadBasicInfo)
 | 
				
			||||||
			tokenAuthrized.POST("/report", client.UploadReport)
 | 
								tokenAuthrized.POST("/report", client.UploadReport)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,14 +30,16 @@ type ClientConfig struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// ClientInfo stores static client information
 | 
					// ClientInfo stores static client information
 | 
				
			||||||
type ClientInfo struct {
 | 
					type ClientInfo struct {
 | 
				
			||||||
	ClientUUID string `gorm:"type:uuid;primaryKey;foreignKey:ClientUUID;references:UUID;constraint:OnDelete:CASCADE"`
 | 
						ClientUUID string `json:"uuid" gorm:"type:uuid;primaryKey;foreignKey:ClientUUID;references:UUID;constraint:OnDelete:CASCADE"`
 | 
				
			||||||
	CPUNAME    string `gorm:"type:varchar(100)"`
 | 
						ClientName string `json:"name" gorm:"type:varchar(100);not null"`
 | 
				
			||||||
	CPUARCH    string `gorm:"type:varchar(50)"`
 | 
						CPUNAME    string `json:"cpu_name" gorm:"type:varchar(100)"`
 | 
				
			||||||
	CPUCORES   int
 | 
						CPUARCH    string `json:"arch" gorm:"type:varchar(50)"`
 | 
				
			||||||
	OS         string `gorm:"type:varchar(100)"`
 | 
						CPUCORES   int    `json:"cpu_cores" gorm:"type:int"`
 | 
				
			||||||
	GPUNAME    string `gorm:"type:varchar(100)"`
 | 
						OS         string `json:"os" gorm:"type:varchar(100)"`
 | 
				
			||||||
	IPv4       string `gorm:"type:varchar(100)"`
 | 
						GPUNAME    string `json:"gpu_name" gorm:"type:varchar(100)"`
 | 
				
			||||||
	IPv6       string `gorm:"type:varchar(100)"`
 | 
						IPv4       string `json:"ipv4" gorm:"type:varchar(100)"`
 | 
				
			||||||
 | 
						IPv6       string `json:"ipv6" gorm:"type:varchar(100)"`
 | 
				
			||||||
 | 
						Country    string `json:"country" gorm:"type:varchar(100)"`
 | 
				
			||||||
	CreatedAt  time.Time
 | 
						CreatedAt  time.Time
 | 
				
			||||||
	UpdatedAt  time.Time
 | 
						UpdatedAt  time.Time
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ func DeleteClientConfig(clientUuid string) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 更新或插入客户端基本信息
 | 
					// 更新或插入客户端基本信息
 | 
				
			||||||
func UpdateOrInsertBasicInfo(cbi ClientBasicInfo) error {
 | 
					func UpdateOrInsertBasicInfo(cbi common.ClientInfo) error {
 | 
				
			||||||
	db := dbcore.GetDBInstance()
 | 
						db := dbcore.GetDBInstance()
 | 
				
			||||||
	err := db.Save(&cbi).Error
 | 
						err := db.Save(&cbi).Error
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -74,7 +74,7 @@ func EditClientToken(clientUUID, token string) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateClient 创建新客户端
 | 
					// CreateClient 创建新客户端
 | 
				
			||||||
func CreateClient(config common.ClientConfig) (clientUUID, token string, err error) {
 | 
					func CreateClient() (clientUUID, token string, err error) {
 | 
				
			||||||
	db := dbcore.GetDBInstance()
 | 
						db := dbcore.GetDBInstance()
 | 
				
			||||||
	token = utils.GenerateToken()
 | 
						token = utils.GenerateToken()
 | 
				
			||||||
	clientUUID = uuid.New().String()
 | 
						clientUUID = uuid.New().String()
 | 
				
			||||||
@@ -86,13 +86,15 @@ func CreateClient(config common.ClientConfig) (clientUUID, token string, err err
 | 
				
			|||||||
		UpdatedAt: time.Now(),
 | 
							UpdatedAt: time.Now(),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	config.ClientUUID = clientUUID
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = db.Create(&client).Error
 | 
						err = db.Create(&client).Error
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", "", err
 | 
							return "", "", err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	err = db.Create(&config).Error
 | 
						clientInfo := common.ClientInfo{
 | 
				
			||||||
 | 
							ClientUUID: clientUUID,
 | 
				
			||||||
 | 
							ClientName: "client_" + clientUUID[0:8],
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = db.Create(&clientInfo).Error
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", "", err
 | 
							return "", "", err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -100,7 +102,7 @@ func CreateClient(config common.ClientConfig) (clientUUID, token string, err err
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetAllClients 获取所有客户端配置
 | 
					// GetAllClients 获取所有客户端配置
 | 
				
			||||||
func GetAllClients() (clients []models.Client, err error) {
 | 
					func getAllClients() (clients []models.Client, err error) {
 | 
				
			||||||
	db := dbcore.GetDBInstance()
 | 
						db := dbcore.GetDBInstance()
 | 
				
			||||||
	err = db.Find(&clients).Error
 | 
						err = db.Find(&clients).Error
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -118,45 +120,22 @@ func GetClientByUUID(uuid string) (client models.Client, err error) {
 | 
				
			|||||||
	return client, nil
 | 
						return client, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetClientConfig 获取指定 UUID 的客户端配置
 | 
					// GetClientBasicInfo 获取指定 UUID 的客户端基本信息
 | 
				
			||||||
func GetClientConfig(uuid string) (client common.ClientConfig, err error) {
 | 
					func GetClientBasicInfo(uuid string) (client common.ClientInfo, err error) {
 | 
				
			||||||
	db := dbcore.GetDBInstance()
 | 
						db := dbcore.GetDBInstance()
 | 
				
			||||||
	err = db.Where("client_uuid = ?", uuid).First(&client).Error
 | 
						err = db.Where("client_uuid = ?", uuid).First(&client).Error
 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return common.ClientConfig{}, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return client, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ClientBasicInfo 客户端基本信息(假设的结构体,需根据实际定义调整)
 | 
					 | 
				
			||||||
type ClientBasicInfo struct {
 | 
					 | 
				
			||||||
	CPU       common.CPUReport `json:"cpu"`
 | 
					 | 
				
			||||||
	GPU       common.GPUReport `json:"gpu"`
 | 
					 | 
				
			||||||
	IpAddress common.IPAddress `json:"ip"`
 | 
					 | 
				
			||||||
	OS        string           `json:"os"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetClientBasicInfo 获取指定 UUID 的客户端基本信息
 | 
					 | 
				
			||||||
func GetClientBasicInfo(uuid string) (client ClientBasicInfo, err error) {
 | 
					 | 
				
			||||||
	db := dbcore.GetDBInstance()
 | 
					 | 
				
			||||||
	var clientInfo common.ClientInfo
 | 
					 | 
				
			||||||
	err = db.Where("client_uuid = ?", uuid).First(&clientInfo).Error
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return client, err
 | 
							return client, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	client = ClientBasicInfo{
 | 
					 | 
				
			||||||
		CPU: common.CPUReport{
 | 
					 | 
				
			||||||
			Name:  clientInfo.CPUNAME,
 | 
					 | 
				
			||||||
			Arch:  clientInfo.CPUARCH,
 | 
					 | 
				
			||||||
			Cores: clientInfo.CPUCORES,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		GPU: common.GPUReport{
 | 
					 | 
				
			||||||
			Name: clientInfo.GPUNAME,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		OS: clientInfo.OS,
 | 
					 | 
				
			||||||
		// IpAddress: 未在数据库中找到对应字段,需确认
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return client, nil
 | 
						return client, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetAllClientBasicInfo() (clients []common.ClientInfo, err error) {
 | 
				
			||||||
 | 
						db := dbcore.GetDBInstance()
 | 
				
			||||||
 | 
						err = db.Find(&clients).Error
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return clients, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,7 +52,6 @@ func GetDBInstance() *gorm.DB {
 | 
				
			|||||||
			log.Fatalf("Failed to connect to SQLite3 database: %v", err)
 | 
								log.Fatalf("Failed to connect to SQLite3 database: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		err = instance.AutoMigrate(
 | 
							err = instance.AutoMigrate(
 | 
				
			||||||
			&common.ClientConfig{},
 | 
					 | 
				
			||||||
			&models.User{},
 | 
								&models.User{},
 | 
				
			||||||
			&models.Client{},
 | 
								&models.Client{},
 | 
				
			||||||
			&models.Session{},
 | 
								&models.Session{},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ package history
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/akizon77/komari/database/dbcore"
 | 
						"github.com/akizon77/komari/database/dbcore"
 | 
				
			||||||
	"github.com/akizon77/komari/database/models"
 | 
						"github.com/akizon77/komari/database/models"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -27,3 +29,222 @@ func DeleteRecordBefore(before time.Time) error {
 | 
				
			|||||||
	db := dbcore.GetDBInstance()
 | 
						db := dbcore.GetDBInstance()
 | 
				
			||||||
	return db.Where("time < ?", before).Delete(&models.History{}).Error
 | 
						return db.Where("time < ?", before).Delete(&models.History{}).Error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 计算区间 [0.02, 0.98] 的平均值
 | 
				
			||||||
 | 
					func QuantileMean(values []float32) float32 {
 | 
				
			||||||
 | 
						if len(values) == 0 {
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 排序数据
 | 
				
			||||||
 | 
						sorted := make([]float32, len(values))
 | 
				
			||||||
 | 
						copy(sorted, values)
 | 
				
			||||||
 | 
						for i := 0; i < len(sorted)-1; i++ {
 | 
				
			||||||
 | 
							for j := i + 1; j < len(sorted); j++ {
 | 
				
			||||||
 | 
								if sorted[i] > sorted[j] {
 | 
				
			||||||
 | 
									sorted[i], sorted[j] = sorted[j], sorted[i]
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 区间边缘index
 | 
				
			||||||
 | 
						lower := int(0.02 * float64(len(sorted)))
 | 
				
			||||||
 | 
						upper := int(0.98 * float64(len(sorted)))
 | 
				
			||||||
 | 
						if lower >= upper || lower >= len(sorted) {
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sum := float32(0)
 | 
				
			||||||
 | 
						count := 0
 | 
				
			||||||
 | 
						for i := lower; i < upper && i < len(sorted); i++ {
 | 
				
			||||||
 | 
							sum += sorted[i]
 | 
				
			||||||
 | 
							count++
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count == 0 {
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return sum / float32(count)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 压缩数据库,针对每个ClientUUID,3小时内的数据不动,24小时内的数据精简为每分钟一条,3天为每15分钟一条,7天为每1小时一条,30天为每12小时一条。所有精简的数据是取 [0.02,0.98] 区间内的平均值
 | 
				
			||||||
 | 
					func CompactHistory() error {
 | 
				
			||||||
 | 
						db := dbcore.GetDBInstance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var clientUUIDs []string
 | 
				
			||||||
 | 
						if err := db.Model(&models.History{}).Distinct("ClientUUID").Pluck("ClientUUID", &clientUUIDs).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						now := time.Now()
 | 
				
			||||||
 | 
						threeHoursAgo := now.Add(-3 * time.Hour)
 | 
				
			||||||
 | 
						oneDayAgo := now.Add(-24 * time.Hour)
 | 
				
			||||||
 | 
						threeDaysAgo := now.Add(-3 * 24 * time.Hour)
 | 
				
			||||||
 | 
						sevenDaysAgo := now.Add(-7 * 24 * time.Hour)
 | 
				
			||||||
 | 
						thirtyDaysAgo := now.Add(-30 * 24 * time.Hour)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, clientUUID := range clientUUIDs {
 | 
				
			||||||
 | 
							// Process each time window
 | 
				
			||||||
 | 
							if err := db.Transaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
								// 24 hours to 3 hours: compact to 1-minute intervals
 | 
				
			||||||
 | 
								if err := compactTimeWindow(tx, clientUUID, oneDayAgo, threeHoursAgo, time.Minute, func(records []models.History) models.History {
 | 
				
			||||||
 | 
									return aggregateRecords(records, time.Minute)
 | 
				
			||||||
 | 
								}); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 3 days to 24 hours: compact to 15-minute intervals
 | 
				
			||||||
 | 
								if err := compactTimeWindow(tx, clientUUID, threeDaysAgo, oneDayAgo, 15*time.Minute, func(records []models.History) models.History {
 | 
				
			||||||
 | 
									return aggregateRecords(records, 15*time.Minute)
 | 
				
			||||||
 | 
								}); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 7 days to 3 days: compact to 1-hour intervals
 | 
				
			||||||
 | 
								if err := compactTimeWindow(tx, clientUUID, sevenDaysAgo, threeDaysAgo, time.Hour, func(records []models.History) models.History {
 | 
				
			||||||
 | 
									return aggregateRecords(records, time.Hour)
 | 
				
			||||||
 | 
								}); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 30 days to 7 days: compact to 12-hour intervals
 | 
				
			||||||
 | 
								if err := compactTimeWindow(tx, clientUUID, thirtyDaysAgo, sevenDaysAgo, 12*time.Hour, func(records []models.History) models.History {
 | 
				
			||||||
 | 
									return aggregateRecords(records, 12*time.Hour)
 | 
				
			||||||
 | 
								}); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// compactTimeWindow compacts records within a specific time window
 | 
				
			||||||
 | 
					func compactTimeWindow(db *gorm.DB, clientUUID string, startTime, endTime time.Time, interval time.Duration, aggregator func([]models.History) models.History) error {
 | 
				
			||||||
 | 
						var records []models.History
 | 
				
			||||||
 | 
						if err := db.Where("ClientUUID = ? AND Time >= ? AND Time < ?", clientUUID, startTime, endTime).
 | 
				
			||||||
 | 
							Order("Time ASC").Find(&records).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(records) == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Group records by interval
 | 
				
			||||||
 | 
						groups := make(map[int64][]models.History)
 | 
				
			||||||
 | 
						for _, record := range records {
 | 
				
			||||||
 | 
							// Truncate time to the start of the interval
 | 
				
			||||||
 | 
							intervalStart := record.Time.Truncate(interval).Unix()
 | 
				
			||||||
 | 
							groups[intervalStart] = append(groups[intervalStart], record)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Process each group
 | 
				
			||||||
 | 
						for intervalStart, group := range groups {
 | 
				
			||||||
 | 
							if len(group) <= 1 {
 | 
				
			||||||
 | 
								continue // Skip groups with single record
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Aggregate records
 | 
				
			||||||
 | 
							aggregated := aggregator(group)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Delete original records
 | 
				
			||||||
 | 
							if err := db.Where("ClientUUID = ? AND Time >= ? AND Time < ?",
 | 
				
			||||||
 | 
								clientUUID, time.Unix(intervalStart, 0), time.Unix(intervalStart, 0).Add(interval)).
 | 
				
			||||||
 | 
								Delete(&models.History{}).Error; err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Insert aggregated record
 | 
				
			||||||
 | 
							if err := db.Create(&aggregated).Error; err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// aggregateRecords aggregates a group of records into a single record
 | 
				
			||||||
 | 
					func aggregateRecords(records []models.History, interval time.Duration) models.History {
 | 
				
			||||||
 | 
						if len(records) == 0 {
 | 
				
			||||||
 | 
							return models.History{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize result with first record's metadata
 | 
				
			||||||
 | 
						result := models.History{
 | 
				
			||||||
 | 
							ClientUUID: records[0].ClientUUID,
 | 
				
			||||||
 | 
							Time:       records[0].Time.Truncate(interval),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Collect values for quantile mean calculation
 | 
				
			||||||
 | 
						var cpuValues, gpuValues, loadValues, tempValues []float32
 | 
				
			||||||
 | 
						var ram, ramTotal, swap, swapTotal, disk, diskTotal, netIn, netOut, netTotalUp, netTotalDown []int64
 | 
				
			||||||
 | 
						var process, connections, connectionsUDP []int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, r := range records {
 | 
				
			||||||
 | 
							cpuValues = append(cpuValues, r.CPU)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							gpuValues = append(gpuValues, r.GPU)
 | 
				
			||||||
 | 
							loadValues = append(loadValues, r.LOAD)
 | 
				
			||||||
 | 
							tempValues = append(tempValues, r.TEMP)
 | 
				
			||||||
 | 
							ram = append(ram, r.RAM)
 | 
				
			||||||
 | 
							ramTotal = append(ramTotal, r.RAMTotal)
 | 
				
			||||||
 | 
							swap = append(swap, r.SWAP)
 | 
				
			||||||
 | 
							swapTotal = append(swapTotal, r.SWAPTotal)
 | 
				
			||||||
 | 
							disk = append(disk, r.DISK)
 | 
				
			||||||
 | 
							diskTotal = append(diskTotal, r.DISKTotal)
 | 
				
			||||||
 | 
							netIn = append(netIn, r.NETIn)
 | 
				
			||||||
 | 
							netOut = append(netOut, r.NETOut)
 | 
				
			||||||
 | 
							netTotalUp = append(netTotalUp, r.NETTotalUp)
 | 
				
			||||||
 | 
							netTotalDown = append(netTotalDown, r.NETTotalDown)
 | 
				
			||||||
 | 
							process = append(process, r.PROCESS)
 | 
				
			||||||
 | 
							connections = append(connections, r.Connections)
 | 
				
			||||||
 | 
							connectionsUDP = append(connectionsUDP, r.ConnectionsUDP)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Apply quantile mean for float32 fields
 | 
				
			||||||
 | 
						result.CPU = QuantileMean(cpuValues)
 | 
				
			||||||
 | 
						result.GPU = QuantileMean(gpuValues)
 | 
				
			||||||
 | 
						result.LOAD = QuantileMean(loadValues)
 | 
				
			||||||
 | 
						result.TEMP = QuantileMean(tempValues)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Simple mean for integer fields
 | 
				
			||||||
 | 
						sumInt64 := func(values []int64) int64 {
 | 
				
			||||||
 | 
							if len(values) == 0 {
 | 
				
			||||||
 | 
								return 0
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							sum := int64(0)
 | 
				
			||||||
 | 
							for _, v := range values {
 | 
				
			||||||
 | 
								sum += v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return sum / int64(len(values))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sumInt := func(values []int) int {
 | 
				
			||||||
 | 
							if len(values) == 0 {
 | 
				
			||||||
 | 
								return 0
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							sum := 0
 | 
				
			||||||
 | 
							for _, v := range values {
 | 
				
			||||||
 | 
								sum += v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return sum / len(values)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						result.RAM = sumInt64(ram)
 | 
				
			||||||
 | 
						result.RAMTotal = sumInt64(ramTotal)
 | 
				
			||||||
 | 
						result.SWAP = sumInt64(swap)
 | 
				
			||||||
 | 
						result.SWAPTotal = sumInt64(swapTotal)
 | 
				
			||||||
 | 
						result.DISK = sumInt64(disk)
 | 
				
			||||||
 | 
						result.DISKTotal = sumInt64(diskTotal)
 | 
				
			||||||
 | 
						result.NETIn = sumInt64(netIn)
 | 
				
			||||||
 | 
						result.NETOut = sumInt64(netOut)
 | 
				
			||||||
 | 
						result.NETTotalUp = sumInt64(netTotalUp)
 | 
				
			||||||
 | 
						result.NETTotalDown = sumInt64(netTotalDown)
 | 
				
			||||||
 | 
						result.PROCESS = sumInt(process)
 | 
				
			||||||
 | 
						result.Connections = sumInt(connections)
 | 
				
			||||||
 | 
						result.ConnectionsUDP = sumInt(connectionsUDP)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return result
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,22 +6,21 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Client represents a registered client device
 | 
					// Client represents a registered client device
 | 
				
			||||||
type Client struct {
 | 
					type Client struct {
 | 
				
			||||||
	UUID       string `gorm:"type:uuid;primaryKey"`
 | 
						UUID      string    `json:"uuid,omitempty" gorm:"type:uuid;primaryKey"`
 | 
				
			||||||
	Token      string `gorm:"type:varchar(255);unique;not null"`
 | 
						Token     string    `json:"token,omitempty" gorm:"type:varchar(255);unique;not null"`
 | 
				
			||||||
	ClientName string `gorm:"type:varchar(100);not null"`
 | 
						CreatedAt time.Time `json:"created_at"`
 | 
				
			||||||
	CreatedAt  time.Time
 | 
						UpdatedAt time.Time `json:"updated_at"`
 | 
				
			||||||
	UpdatedAt  time.Time
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// User represents an authenticated user
 | 
					// User represents an authenticated user
 | 
				
			||||||
type User struct {
 | 
					type User struct {
 | 
				
			||||||
	UUID      string `gorm:"type:uuid;primaryKey"`
 | 
						UUID      string    `json:"uuid,omitempty" gorm:"type:uuid;primaryKey"`
 | 
				
			||||||
	Username  string `gorm:"type:varchar(50);unique;not null"`
 | 
						Username  string    `json:"username" gorm:"type:varchar(50);unique;not null"`
 | 
				
			||||||
	Passwd    string `gorm:"type:varchar(255);not null"` // Hashed password
 | 
						Passwd    string    `json:"passwd,omitempty" gorm:"type:varchar(255);not null"` // Hashed password
 | 
				
			||||||
	SSOType   string `gorm:"type:varchar(20)"`           // e.g., "github", "google"
 | 
						SSOType   string    `json:"sso_type" gorm:"type:varchar(20)"`                   // e.g., "github", "google"
 | 
				
			||||||
	SSOID     string `gorm:"type:varchar(100)"`          // OAuth provider's user ID
 | 
						SSOID     string    `json:"sso_id" gorm:"type:varchar(100)"`                    // OAuth provider's user ID
 | 
				
			||||||
	CreatedAt time.Time
 | 
						CreatedAt time.Time `json:"created_at"`
 | 
				
			||||||
	UpdatedAt time.Time
 | 
						UpdatedAt time.Time `json:"updated_at"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Session manages user sessions
 | 
					// Session manages user sessions
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								main.go
									
									
									
									
									
								
							@@ -1,12 +1,13 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/akizon77/komari/cmd"
 | 
						"github.com/akizon77/komari/cmd"
 | 
				
			||||||
	"github.com/akizon77/komari/database/accounts"
 | 
						"github.com/akizon77/komari/database/accounts"
 | 
				
			||||||
	"github.com/akizon77/komari/database/dbcore"
 | 
						"github.com/akizon77/komari/database/dbcore"
 | 
				
			||||||
	"github.com/akizon77/komari/database/history"
 | 
						"github.com/akizon77/komari/database/history"
 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
@@ -26,6 +27,7 @@ func main() {
 | 
				
			|||||||
		select {
 | 
							select {
 | 
				
			||||||
		case <-ticker.C:
 | 
							case <-ticker.C:
 | 
				
			||||||
			history.DeleteRecordBefore(time.Now().Add(-time.Hour * 24 * 7))
 | 
								history.DeleteRecordBefore(time.Now().Add(-time.Hour * 24 * 7))
 | 
				
			||||||
 | 
								history.CompactHistory()
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,12 +3,14 @@ package ws
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/akizon77/komari/common"
 | 
				
			||||||
	"github.com/gorilla/websocket"
 | 
						"github.com/gorilla/websocket"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetClients(c *gin.Context) {
 | 
					func GetClients(c *gin.Context) {
 | 
				
			||||||
 | 
						// 升级到ws
 | 
				
			||||||
	if !websocket.IsWebSocketUpgrade(c.Request) {
 | 
						if !websocket.IsWebSocketUpgrade(c.Request) {
 | 
				
			||||||
		c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Require WebSocket upgrade"})
 | 
							c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Require WebSocket upgrade"})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -26,7 +28,13 @@ func GetClients(c *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	defer conn.Close()
 | 
						defer conn.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 请求
 | 
				
			||||||
	for {
 | 
						for {
 | 
				
			||||||
 | 
							var resp struct {
 | 
				
			||||||
 | 
								Online []string                  `json:"online"` // 已建立连接的客户端uuid列表
 | 
				
			||||||
 | 
								Data   map[string]*common.Report `json:"data"`   // 最后上报的数据
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		_, data, err := conn.ReadMessage()
 | 
							_, data, err := conn.ReadMessage()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			//log.Println("Error reading message:", err)
 | 
								//log.Println("Error reading message:", err)
 | 
				
			||||||
@@ -38,7 +46,18 @@ func GetClients(c *gin.Context) {
 | 
				
			|||||||
			conn.WriteJSON(gin.H{"status": "error", "error": "Invalid message"})
 | 
								conn.WriteJSON(gin.H{"status": "error", "error": "Invalid message"})
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		err = conn.WriteJSON(gin.H{"status": "success", "data": LatestReport})
 | 
							// 已建立连接的客户端uuid列表
 | 
				
			||||||
 | 
							for key := range ConnectedClients {
 | 
				
			||||||
 | 
								resp.Online = append(resp.Online, key)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// 清除UUID和Token,简化报告单
 | 
				
			||||||
 | 
							for _, report := range LatestReport {
 | 
				
			||||||
 | 
								report.Token = ""
 | 
				
			||||||
 | 
								report.UUID = ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							resp.Data = LatestReport
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err = conn.WriteJSON(gin.H{"status": "success", "data": resp})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,12 @@
 | 
				
			|||||||
package ws
 | 
					package ws
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "github.com/gorilla/websocket"
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/akizon77/komari/common"
 | 
				
			||||||
 | 
						"github.com/gorilla/websocket"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	ConnectedClients = make(map[string]*websocket.Conn)
 | 
						ConnectedClients = make(map[string]*websocket.Conn)
 | 
				
			||||||
	ConnectedUsers   = []*websocket.Conn{}
 | 
						ConnectedUsers   = []*websocket.Conn{}
 | 
				
			||||||
	LatestReport     = make(map[string]interface{})
 | 
						LatestReport     = make(map[string]*common.Report)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user