feat: add device connection status and owner username to device listings with handshake-based connectivity detection

Add connection_status and owner_username fields to Device model. Join users table in ListByUser and ListAll queries to populate owner_username from username column.

Parse latest_handshake_at from gateway runtime snapshots and aggregate across peers. Add isPeerConnected helper with 3-minute handshake timeout threshold. Set connection_status to "connected" or "disconnected" based on handsh
This commit is contained in:
2026-03-24 18:07:46 +01:00
parent 6e68b13c06
commit e3da49ec23
4 changed files with 47 additions and 17 deletions

View File

@@ -221,8 +221,9 @@ func (r *PGRepository) SetSelectedProfileID(ctx context.Context, deviceID uuid.U
func (r *PGRepository) ListByUser(ctx context.Context, userID uuid.UUID) ([]Device, error) {
rows, err := r.db.Query(ctx, `
select d.id, d.user_id, d.gateway_id, d.name, d.platform, d.status, coalesce(host(wp.assigned_ip), '')
select d.id, d.user_id, coalesce(u.username, ''), d.gateway_id, d.name, d.platform, d.status, coalesce(host(wp.assigned_ip), '')
from devices d
left join users u on u.id = d.user_id
left join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
where d.user_id = $1 and d.deleted_at is null
order by d.created_at desc
@@ -235,7 +236,7 @@ func (r *PGRepository) ListByUser(ctx context.Context, userID uuid.UUID) ([]Devi
var items []Device
for rows.Next() {
var item Device
if err := rows.Scan(&item.ID, &item.UserID, &item.GatewayID, &item.Name, &item.Platform, &item.Status, &item.AssignedIP); err != nil {
if err := rows.Scan(&item.ID, &item.UserID, &item.OwnerUsername, &item.GatewayID, &item.Name, &item.Platform, &item.Status, &item.AssignedIP); err != nil {
return nil, err
}
items = append(items, item)
@@ -248,8 +249,9 @@ func (r *PGRepository) ListByUser(ctx context.Context, userID uuid.UUID) ([]Devi
func (r *PGRepository) ListAll(ctx context.Context) ([]Device, error) {
rows, err := r.db.Query(ctx, `
select d.id, d.user_id, d.gateway_id, d.name, d.platform, d.status, coalesce(host(wp.assigned_ip), '')
select d.id, d.user_id, coalesce(u.username, ''), d.gateway_id, d.name, d.platform, d.status, coalesce(host(wp.assigned_ip), '')
from devices d
left join users u on u.id = d.user_id
left join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
where d.deleted_at is null
order by d.created_at desc
@@ -262,7 +264,7 @@ func (r *PGRepository) ListAll(ctx context.Context) ([]Device, error) {
var items []Device
for rows.Next() {
var item Device
if err := rows.Scan(&item.ID, &item.UserID, &item.GatewayID, &item.Name, &item.Platform, &item.Status, &item.AssignedIP); err != nil {
if err := rows.Scan(&item.ID, &item.UserID, &item.OwnerUsername, &item.GatewayID, &item.Name, &item.Platform, &item.Status, &item.AssignedIP); err != nil {
return nil, err
}
items = append(items, item)
@@ -349,9 +351,10 @@ type runtimeSnapshot struct {
}
type runtimePeer struct {
DeviceID string `json:"device_id"`
RXBytes uint64 `json:"rx_bytes"`
TXBytes uint64 `json:"tx_bytes"`
DeviceID string `json:"device_id"`
RXBytes uint64 `json:"rx_bytes"`
TXBytes uint64 `json:"tx_bytes"`
LatestHandshakeAt *int64 `json:"latest_handshake_at,omitempty"`
}
func (r *PGRepository) applyRuntimeStats(ctx context.Context, items []Device) ([]Device, error) {
@@ -390,6 +393,11 @@ func (r *PGRepository) applyRuntimeStats(ctx context.Context, items []Device) ([
existing.DeviceID = peer.DeviceID
existing.RXBytes += peer.RXBytes
existing.TXBytes += peer.TXBytes
if peer.LatestHandshakeAt != nil {
if existing.LatestHandshakeAt == nil || *peer.LatestHandshakeAt > *existing.LatestHandshakeAt {
existing.LatestHandshakeAt = peer.LatestHandshakeAt
}
}
statsByDevice[deviceID] = existing
}
}
@@ -401,12 +409,28 @@ func (r *PGRepository) applyRuntimeStats(ctx context.Context, items []Device) ([
if stats, ok := statsByDevice[items[index].ID]; ok {
items[index].RXBytes = stats.RXBytes
items[index].TXBytes = stats.TXBytes
if isPeerConnected(stats.LatestHandshakeAt) {
items[index].ConnectionStatus = "connected"
} else {
items[index].ConnectionStatus = "disconnected"
}
} else {
items[index].ConnectionStatus = "disconnected"
}
}
return items, nil
}
func isPeerConnected(latestHandshakeAt *int64) bool {
if latestHandshakeAt == nil || *latestHandshakeAt <= 0 {
return false
}
handshakeTime := time.Unix(*latestHandshakeAt, 0)
return time.Since(handshakeTime) <= 3*time.Minute
}
func scanEnrollmentRow(row enrollmentRowScanner) (EnrollmentResponse, error) {
var response EnrollmentResponse
var profileRevision int

View File

@@ -12,15 +12,17 @@ type EnrollRequest struct {
}
type Device struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id,omitempty"`
GatewayID uuid.UUID `json:"gateway_id,omitempty"`
Name string `json:"name"`
Platform string `json:"platform"`
Status string `json:"status"`
AssignedIP string `json:"assigned_ip,omitempty"`
RXBytes uint64 `json:"rx_bytes"`
TXBytes uint64 `json:"tx_bytes"`
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id,omitempty"`
OwnerUsername string `json:"owner_username,omitempty"`
GatewayID uuid.UUID `json:"gateway_id,omitempty"`
Name string `json:"name"`
Platform string `json:"platform"`
Status string `json:"status"`
ConnectionStatus string `json:"connection_status"`
AssignedIP string `json:"assigned_ip,omitempty"`
RXBytes uint64 `json:"rx_bytes"`
TXBytes uint64 `json:"tx_bytes"`
}
type ConnectionStatus struct {