diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index 5a4c328..c767dd2 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -59,6 +59,8 @@ type nodeCfg struct { twinID uint64 status string healthy bool + online *bool + lastSeen *int64 Location Location Resources Resources Interfaces []Interface @@ -100,6 +102,18 @@ func ListNodesWithHealthy() ListNodeOpts { } } +func ListNodesWithOnline(online bool) ListNodeOpts { + return func(n *nodeCfg) { + n.online = &online + } +} + +func ListNodesWithLastSeen(minutes int64) ListNodeOpts { + return func(n *nodeCfg) { + n.lastSeen = &minutes + } +} + func ListNodesWithTwinID(id uint64) ListNodeOpts { return func(n *nodeCfg) { n.twinID = id @@ -495,13 +509,15 @@ func (c *RegistrarClient) parseUpdateNodeOpts(node Node, opts []UpdateNodeOpts) func parseListNodeOpts(opts []ListNodeOpts) map[string]any { cfg := nodeCfg{ - nodeID: 0, - twinID: 0, - farmID: 0, - status: "", - healthy: false, - size: 50, - page: 1, + nodeID: 0, + twinID: 0, + farmID: 0, + status: "", + healthy: false, + online: nil, + lastSeen: nil, + size: 50, + page: 1, } for _, opt := range opts { @@ -530,6 +546,14 @@ func parseListNodeOpts(opts []ListNodeOpts) map[string]any { data["healthy"] = cfg.healthy } + if cfg.online != nil { + data["online"] = *cfg.online + } + + if cfg.lastSeen != nil { + data["last_seen"] = *cfg.lastSeen + } + data["size"] = cfg.size data["page"] = cfg.page diff --git a/node-registrar/client/types.go b/node-registrar/client/types.go index 2007195..bbc0bfd 100644 --- a/node-registrar/client/types.go +++ b/node-registrar/client/types.go @@ -30,6 +30,8 @@ type Node struct { Virtualized bool `json:"virtualized"` SerialNumber string `json:"serial_number"` UptimeReports []UptimeReport `json:"uptime"` + LastSeen *time.Time `json:"last_seen"` + Online bool `json:"online"` Approved bool } diff --git a/node-registrar/docs/docs.go b/node-registrar/docs/docs.go index c46e734..40122cc 100644 --- a/node-registrar/docs/docs.go +++ b/node-registrar/docs/docs.go @@ -470,6 +470,18 @@ const docTemplate = `{ "name": "healthy", "in": "query" }, + { + "type": "boolean", + "description": "Filter by online status (true = online, false = offline)", + "name": "online", + "in": "query" + }, + { + "type": "integer", + "description": "Filter nodes last seen within this many minutes", + "name": "last_seen", + "in": "query" + }, { "type": "integer", "default": 1, @@ -487,7 +499,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "List of nodes", + "description": "List of nodes with online status", "schema": { "type": "array", "items": { @@ -592,7 +604,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Node details", + "description": "Node details with online status and last_seen information", "schema": { "$ref": "#/definitions/db.Node" } @@ -894,6 +906,11 @@ const docTemplate = `{ }, "db.Farm": { "type": "object", + "required": [ + "farm_name", + "stellar_address", + "twin_id" + ], "properties": { "created_at": { "type": "string" @@ -930,7 +947,10 @@ const docTemplate = `{ "type": "object", "properties": { "ips": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "mac": { "type": "string" @@ -976,12 +996,20 @@ const docTemplate = `{ "$ref": "#/definitions/db.Interface" } }, + "last_seen": { + "description": "Last time the node sent an uptime report", + "type": "string" + }, "location": { "$ref": "#/definitions/db.Location" }, "node_id": { "type": "integer" }, + "online": { + "description": "Computed field, not stored in database", + "type": "boolean" + }, "resources": { "description": "PublicConfig PublicConfig ` + "`" + `json:\"public_config\" gorm:\"type:json\"` + "`" + `", "allOf": [ @@ -1139,8 +1167,7 @@ const docTemplate = `{ "maxLength": 40 }, "stellar_address": { - "type": "string", - "maxLength": 56 + "type": "string" } } }, @@ -1200,12 +1227,12 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "", + Version: "1.0", Host: "", - BasePath: "", + BasePath: "/v1", Schemes: []string{}, - Title: "", - Description: "", + Title: "Node Registrar API", + Description: "API for managing TFGrid node registration", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/node-registrar/docs/swagger.json b/node-registrar/docs/swagger.json index d05ca7f..b0e3f4e 100644 --- a/node-registrar/docs/swagger.json +++ b/node-registrar/docs/swagger.json @@ -1,8 +1,12 @@ { "swagger": "2.0", "info": { - "contact": {} + "description": "API for managing TFGrid node registration", + "title": "Node Registrar API", + "contact": {}, + "version": "1.0" }, + "basePath": "/v1", "paths": { "/accounts": { "get": { @@ -459,6 +463,18 @@ "name": "healthy", "in": "query" }, + { + "type": "boolean", + "description": "Filter by online status (true = online, false = offline)", + "name": "online", + "in": "query" + }, + { + "type": "integer", + "description": "Filter nodes last seen within this many minutes", + "name": "last_seen", + "in": "query" + }, { "type": "integer", "default": 1, @@ -476,7 +492,7 @@ ], "responses": { "200": { - "description": "List of nodes", + "description": "List of nodes with online status", "schema": { "type": "array", "items": { @@ -581,7 +597,7 @@ ], "responses": { "200": { - "description": "Node details", + "description": "Node details with online status and last_seen information", "schema": { "$ref": "#/definitions/db.Node" } @@ -883,6 +899,11 @@ }, "db.Farm": { "type": "object", + "required": [ + "farm_name", + "stellar_address", + "twin_id" + ], "properties": { "created_at": { "type": "string" @@ -919,7 +940,10 @@ "type": "object", "properties": { "ips": { - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "mac": { "type": "string" @@ -965,12 +989,20 @@ "$ref": "#/definitions/db.Interface" } }, + "last_seen": { + "description": "Last time the node sent an uptime report", + "type": "string" + }, "location": { "$ref": "#/definitions/db.Location" }, "node_id": { "type": "integer" }, + "online": { + "description": "Computed field, not stored in database", + "type": "boolean" + }, "resources": { "description": "PublicConfig PublicConfig `json:\"public_config\" gorm:\"type:json\"`", "allOf": [ @@ -1128,8 +1160,7 @@ "maxLength": 40 }, "stellar_address": { - "type": "string", - "maxLength": 56 + "type": "string" } } }, diff --git a/node-registrar/docs/swagger.yaml b/node-registrar/docs/swagger.yaml index d2b3482..c431314 100644 --- a/node-registrar/docs/swagger.yaml +++ b/node-registrar/docs/swagger.yaml @@ -1,3 +1,4 @@ +basePath: /v1 definitions: db.Account: properties: @@ -50,11 +51,17 @@ definitions: type: integer updated_at: type: string + required: + - farm_name + - stellar_address + - twin_id type: object db.Interface: properties: ips: - type: string + items: + type: string + type: array mac: type: string name: @@ -85,10 +92,16 @@ definitions: items: $ref: '#/definitions/db.Interface' type: array + last_seen: + description: Last time the node sent an uptime report + type: string location: $ref: '#/definitions/db.Location' node_id: type: integer + online: + description: Computed field, not stored in database + type: boolean resources: allOf: - $ref: '#/definitions/db.Resources' @@ -198,7 +211,6 @@ definitions: maxLength: 40 type: string stellar_address: - maxLength: 56 type: string type: object server.UpdateNodeRequest: @@ -238,6 +250,9 @@ definitions: type: object info: contact: {} + description: API for managing TFGrid node registration + title: Node Registrar API + version: "1.0" paths: /accounts: get: @@ -544,6 +559,14 @@ paths: in: query name: healthy type: boolean + - description: Filter by online status (true = online, false = offline) + in: query + name: online + type: boolean + - description: Filter nodes last seen within this many minutes + in: query + name: last_seen + type: integer - default: 1 description: Page number in: query @@ -558,7 +581,7 @@ paths: - application/json responses: "200": - description: List of nodes + description: List of nodes with online status schema: items: $ref: '#/definitions/db.Node' @@ -629,7 +652,7 @@ paths: - application/json responses: "200": - description: Node details + description: Node details with online status and last_seen information schema: $ref: '#/definitions/db.Node' "400": diff --git a/node-registrar/pkg/db/models.go b/node-registrar/pkg/db/models.go index 7427dfe..3524ab1 100644 --- a/node-registrar/pkg/db/models.go +++ b/node-registrar/pkg/db/models.go @@ -48,6 +48,8 @@ type Node struct { SerialNumber string `json:"serial_number"` UptimeReports []UptimeReport `json:"uptime" gorm:"foreignKey:NodeID;references:NodeID;constraint:OnDelete:CASCADE"` + LastSeen *time.Time `json:"last_seen" gorm:"index"` // Last time the node sent an uptime report + Online bool `json:"online" gorm:"-"` // Computed field, not stored in database CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -89,11 +91,13 @@ type Location struct { } type NodeFilter struct { - NodeID *uint64 `form:"node_id"` - FarmID *uint64 `form:"farm_id"` - TwinID *uint64 `form:"twin_id"` - Status string `form:"status"` - Healthy bool `form:"healthy"` + NodeID *uint64 `form:"node_id"` + FarmID *uint64 `form:"farm_id"` + TwinID *uint64 `form:"twin_id"` + Status string `form:"status"` + Healthy bool `form:"healthy"` + Online *bool `form:"online"` // Filter by online status (true = online, false = offline, nil = both) + LastSeen *int64 `form:"last_seen"` // Filter nodes last seen within this many minutes } type FarmFilter struct { diff --git a/node-registrar/pkg/db/nodes.go b/node-registrar/pkg/db/nodes.go index aa52f5d..8077312 100644 --- a/node-registrar/pkg/db/nodes.go +++ b/node-registrar/pkg/db/nodes.go @@ -23,6 +23,28 @@ func (db *Database) ListNodes(filter NodeFilter, limit Limit) (nodes []Node, err query = query.Where("twin_id = ?", *filter.TwinID) } + // Filter by online status (node sent an uptime report in the last 30 minutes) + if filter.Online != nil { + // Calculate the cutoff time (30 minutes ago by default) + cutoffMinutes := int64(30) // Default to 30 minutes + if filter.LastSeen != nil { + cutoffMinutes = *filter.LastSeen + } + cutoffTime := time.Now().Add(-time.Duration(cutoffMinutes) * time.Minute) + + if *filter.Online { + // Online nodes: last_seen is not null and more recent than cutoff time + query = query.Where("last_seen IS NOT NULL AND last_seen > ?", cutoffTime) + } else { + // Offline nodes: last_seen is null or older than cutoff time + query = query.Where("last_seen IS NULL OR last_seen <= ?", cutoffTime) + } + } else if filter.LastSeen != nil { + // If only LastSeen is provided without Online flag, show nodes active within that period + cutoffTime := time.Now().Add(-time.Duration(*filter.LastSeen) * time.Minute) + query = query.Where("last_seen IS NOT NULL AND last_seen > ?", cutoffTime) + } + offset := (limit.Page - 1) * limit.Size query = query.Offset(int(offset)).Limit(int(limit.Size)) @@ -78,7 +100,27 @@ func (db *Database) GetUptimeReports(nodeID uint64, start, end time.Time) ([]Upt } func (db *Database) CreateUptimeReport(report *UptimeReport) error { - return db.gormDB.Create(report).Error + // Start a transaction + tx := db.gormDB.Begin() + if tx.Error != nil { + return tx.Error + } + + // Create the uptime report + if err := tx.Create(report).Error; err != nil { + tx.Rollback() + return err + } + + // Update the node's LastSeen field + now := time.Now() + if err := tx.Model(&Node{}).Where("node_id = ?", report.NodeID).Update("last_seen", now).Error; err != nil { + tx.Rollback() + return err + } + + // Commit the transaction + return tx.Commit().Error } func (db *Database) SetZOSVersion(version string) error { diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 0e2534e..b134ac2 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -211,9 +211,11 @@ func (s Server) updateFarmHandler(c *gin.Context) { // @Param twin_id query int false "Filter by twin ID" // @Param status query string false "Filter by status" // @Param healthy query bool false "Filter by health status" +// @Param online query bool false "Filter by online status (true = online, false = offline)" +// @Param last_seen query int false "Filter nodes last seen within this many minutes" // @Param page query int false "Page number" default(1) // @Param size query int false "Results per page" default(10) -// @Success 200 {object} []db.Node "List of nodes" +// @Success 200 {object} []db.Node "List of nodes with online status" // @Failure 400 {object} map[string]any "Bad request" // @Router /nodes [get] func (s Server) listNodesHandler(c *gin.Context) { @@ -232,6 +234,14 @@ func (s Server) listNodesHandler(c *gin.Context) { return } + // Set online status for each node + cutoffTime := time.Now().Add(-30 * time.Minute) + for i := range nodes { + if nodes[i].LastSeen != nil { + nodes[i].Online = nodes[i].LastSeen.After(cutoffTime) + } + } + c.JSON(http.StatusOK, nodes) } @@ -241,7 +251,7 @@ func (s Server) listNodesHandler(c *gin.Context) { // @Accept json // @Produce json // @Param node_id path int true "Node ID" -// @Success 200 {object} db.Node "Node details" +// @Success 200 {object} db.Node "Node details with online status and last_seen information" // @Failure 400 {object} map[string]any "Invalid node ID" // @Failure 404 {object} map[string]any "Node not found" // @Router /nodes/{node_id} [get] @@ -265,6 +275,13 @@ func (s Server) getNodeHandler(c *gin.Context) { return } + // Determine if the node is online (has sent an uptime report in the last 30 minutes) + if node.LastSeen != nil { + // Calculate if the node is online (last seen within the last 30 minutes) + cutoffTime := time.Now().Add(-30 * time.Minute) + node.Online = node.LastSeen.After(cutoffTime) + } + c.JSON(http.StatusOK, node) }