feat: add device traffic metrics with gateway telemetry reporting and admin UI display
Add rx_bytes and tx_bytes fields to Device type and API responses. Add formatDataSize helper for human-readable byte formatting with units from B to TB. Add Received and Sent columns to devices table in admin UI with formatted traffic totals. Add traffic metrics display to device action panel. Add TelemetrySnapshot and PeerTelemetry types for gateway runtime stats. Add gateway telemetry endpoint at POST /gateway
This commit is contained in:
@@ -2,6 +2,7 @@ package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"time"
|
||||
@@ -198,7 +199,10 @@ func (r *PGRepository) ListByUser(ctx context.Context, userID uuid.UUID) ([]Devi
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.applyRuntimeStats(ctx, items)
|
||||
}
|
||||
|
||||
func (r *PGRepository) ListAll(ctx context.Context) ([]Device, error) {
|
||||
@@ -222,7 +226,10 @@ func (r *PGRepository) ListAll(ctx context.Context) ([]Device, error) {
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.applyRuntimeStats(ctx, items)
|
||||
}
|
||||
|
||||
func (r *PGRepository) Revoke(ctx context.Context, deviceID uuid.UUID) error {
|
||||
@@ -258,6 +265,69 @@ type enrollmentRowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
type runtimeSnapshot struct {
|
||||
Peers []runtimePeer `json:"peers"`
|
||||
}
|
||||
|
||||
type runtimePeer struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
RXBytes uint64 `json:"rx_bytes"`
|
||||
TXBytes uint64 `json:"tx_bytes"`
|
||||
}
|
||||
|
||||
func (r *PGRepository) applyRuntimeStats(ctx context.Context, items []Device) ([]Device, error) {
|
||||
if len(items) == 0 {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
rows, err := r.db.Query(ctx, `
|
||||
select value
|
||||
from settings
|
||||
where category = 'gateway_runtime'
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
statsByDevice := make(map[uuid.UUID]runtimePeer)
|
||||
for rows.Next() {
|
||||
var raw []byte
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var snapshot runtimeSnapshot
|
||||
if err := json.Unmarshal(raw, &snapshot); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, peer := range snapshot.Peers {
|
||||
deviceID, err := uuid.Parse(peer.DeviceID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
existing := statsByDevice[deviceID]
|
||||
existing.DeviceID = peer.DeviceID
|
||||
existing.RXBytes += peer.RXBytes
|
||||
existing.TXBytes += peer.TXBytes
|
||||
statsByDevice[deviceID] = existing
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for index := range items {
|
||||
if stats, ok := statsByDevice[items[index].ID]; ok {
|
||||
items[index].RXBytes = stats.RXBytes
|
||||
items[index].TXBytes = stats.TXBytes
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func scanEnrollmentRow(row enrollmentRowScanner) (EnrollmentResponse, error) {
|
||||
var response EnrollmentResponse
|
||||
var profileRevision int
|
||||
|
||||
@@ -19,6 +19,8 @@ type Device struct {
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
AssignedIP string `json:"assigned_ip,omitempty"`
|
||||
RXBytes uint64 `json:"rx_bytes"`
|
||||
TXBytes uint64 `json:"tx_bytes"`
|
||||
}
|
||||
|
||||
type ConnectionStatus struct {
|
||||
|
||||
Reference in New Issue
Block a user