移除 远程配置下发。

修改 客户端基本属性逻辑。
修改 最新上报key改为uuid
新增 客户端命名
新增 历史记录数据库压缩。
新增 在线节点信息。
新增 公开API /api/nodes 显示客户端基础属性
This commit is contained in:
Akizon77
2025-04-27 18:57:37 +08:00
parent 07ceb1b036
commit 2c2c81953e
14 changed files with 360 additions and 237 deletions

View File

@@ -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
@@ -29,10 +24,9 @@ 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"}) if req.ClientName != "" {
return updates["client_name"] = req.ClientName
} }
if req.Token != "" {
// 获取原始配置 updates["token"] = req.Token
rawConfig, err := clients.GetClientConfig(req.UUID) }
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
} }
// 更新配置
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 != "" {
err = clients.EditClientName(req.UUID, req.ClientName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
return
}
}
// 更新token
if req.Token != "" {
err = clients.EditClientToken(req.UUID, req.Token)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
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 {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "Failed to get client"})
return
}
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) {
cls, err := clients.GetAllClients()
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 {
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) c.JSON(http.StatusOK, result)
} }
func ListClients(c *gin.Context) {
cls, err := clients.GetAllClientBasicInfo()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
return
}
c.JSON(http.StatusOK, cls)
}

View File

@@ -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)
}

View File

@@ -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
} }
} }

View File

@@ -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
View 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})
}

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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
}

View File

@@ -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{},

View File

@@ -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)
}
// 压缩数据库针对每个ClientUUID3小时内的数据不动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
}

View File

@@ -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

View File

@@ -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()
} }
}() }()

View File

@@ -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
} }

View File

@@ -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)
) )