@@ -703,3 +703,289 @@ func (h *Handlers) GetDNSNameservers(c *gin.Context) {
703703
704704 c .JSON (http .StatusOK , nameservers )
705705}
706+
707+ // parseTimeRange extracts and validates start/end query params.
708+ // Defaults: start = 1 hour ago, end = now.
709+ func (h * Handlers ) parseTimeRange (c * gin.Context ) (time.Time , time.Time , error ) {
710+ var startTime , endTime time.Time
711+ var err error
712+
713+ if s := c .Query ("start" ); s != "" {
714+ startTime , err = time .Parse (time .RFC3339 , s )
715+ if err != nil {
716+ return time.Time {}, time.Time {}, fmt .Errorf ("invalid start time: %w" , err )
717+ }
718+ } else {
719+ startTime = time .Now ().Add (- 1 * time .Hour )
720+ }
721+
722+ if e := c .Query ("end" ); e != "" {
723+ endTime , err = time .Parse (time .RFC3339 , e )
724+ if err != nil {
725+ return time.Time {}, time.Time {}, fmt .Errorf ("invalid end time: %w" , err )
726+ }
727+ } else {
728+ endTime = time .Now ()
729+ }
730+
731+ if endTime .Before (startTime ) {
732+ return time.Time {}, time.Time {}, fmt .Errorf ("end time before start time" )
733+ }
734+
735+ return startTime , endTime , nil
736+ }
737+
738+ // GetStatsOverview returns network-wide statistics for a time range
739+ func (h * Handlers ) GetStatsOverview (c * gin.Context ) {
740+ if h .store == nil {
741+ c .JSON (http .StatusServiceUnavailable , gin.H {"error" : "Database not configured" })
742+ return
743+ }
744+
745+ startTime , endTime , err := h .parseTimeRange (c )
746+ if err != nil {
747+ c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
748+ return
749+ }
750+
751+ var buckets []database.TrafficStats
752+ source := "database"
753+
754+ // Try rolling cache first for recent data
755+ duration := endTime .Sub (startTime )
756+ if h .poller != nil && duration <= time .Hour {
757+ cache := h .poller .GetRollingCache ()
758+ if cache .HasDataFor (startTime , endTime ) {
759+ buckets = cache .GetTrafficStats (startTime , endTime )
760+ source = "cache"
761+ }
762+ }
763+
764+ // Fall back to database
765+ if len (buckets ) == 0 {
766+ ctx , cancel := context .WithTimeout (c .Request .Context (), AggregationQueryTimeout )
767+ defer cancel ()
768+
769+ buckets , err = h .store .GetTrafficStats (ctx , startTime , endTime )
770+ if err != nil {
771+ log .Printf ("ERROR GetStatsOverview: %v" , err )
772+ c .JSON (http .StatusInternalServerError , gin.H {
773+ "error" : "Failed to fetch traffic stats" ,
774+ "message" : err .Error (),
775+ })
776+ return
777+ }
778+ source = "database"
779+ }
780+
781+ // Aggregate buckets into summary
782+ var tcpBytes , udpBytes , otherProtoBytes int64
783+ var virtualBytes , subnetBytes , physicalBytes int64
784+ var totalFlows , maxUniquePairs int64
785+ for _ , b := range buckets {
786+ tcpBytes += b .TCPBytes
787+ udpBytes += b .UDPBytes
788+ otherProtoBytes += b .OtherProtoBytes
789+ virtualBytes += b .VirtualBytes
790+ subnetBytes += b .SubnetBytes
791+ physicalBytes += b .PhysicalBytes
792+ totalFlows += b .TotalFlows
793+ if b .UniquePairs > maxUniquePairs {
794+ maxUniquePairs = b .UniquePairs
795+ }
796+ }
797+
798+ c .JSON (http .StatusOK , gin.H {
799+ "summary" : gin.H {
800+ "tcpBytes" : tcpBytes ,
801+ "udpBytes" : udpBytes ,
802+ "otherProtoBytes" : otherProtoBytes ,
803+ "virtualBytes" : virtualBytes ,
804+ "subnetBytes" : subnetBytes ,
805+ "physicalBytes" : physicalBytes ,
806+ "totalFlows" : totalFlows ,
807+ "uniquePairs" : maxUniquePairs ,
808+ },
809+ "buckets" : buckets ,
810+ "metadata" : gin.H {
811+ "start" : startTime ,
812+ "end" : endTime ,
813+ "bucketCount" : len (buckets ),
814+ "source" : source ,
815+ },
816+ })
817+ }
818+
819+ // resolveNodeName returns a human-readable name for a node ID using the device cache.
820+ func (h * Handlers ) resolveNodeName (nodeID string ) string {
821+ if h .poller == nil {
822+ return ""
823+ }
824+ cache := h .poller .GetDeviceCache ()
825+ if entry := cache .GetDevice (nodeID ); entry != nil {
826+ // Prefer hostname, fall back to full name
827+ if entry .Hostname != "" {
828+ return entry .Hostname
829+ }
830+ return entry .Name
831+ }
832+ return ""
833+ }
834+
835+ // GetTopTalkers returns the top N nodes by total traffic
836+ func (h * Handlers ) GetTopTalkers (c * gin.Context ) {
837+ if h .store == nil {
838+ c .JSON (http .StatusServiceUnavailable , gin.H {"error" : "Database not configured" })
839+ return
840+ }
841+
842+ startTime , endTime , err := h .parseTimeRange (c )
843+ if err != nil {
844+ c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
845+ return
846+ }
847+
848+ limit := 10
849+ if l := c .Query ("limit" ); l != "" {
850+ if _ , err := fmt .Sscanf (l , "%d" , & limit ); err != nil || limit <= 0 {
851+ c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid limit" })
852+ return
853+ }
854+ }
855+ if limit > 100 {
856+ limit = 100
857+ }
858+
859+ ctx , cancel := context .WithTimeout (c .Request .Context (), DefaultQueryTimeout )
860+ defer cancel ()
861+
862+ talkers , err := h .store .GetTopTalkers (ctx , startTime , endTime , limit )
863+ if err != nil {
864+ log .Printf ("ERROR GetTopTalkers: %v" , err )
865+ c .JSON (http .StatusInternalServerError , gin.H {
866+ "error" : "Failed to fetch top talkers" ,
867+ "message" : err .Error (),
868+ })
869+ return
870+ }
871+
872+ // Enrich with device names
873+ enriched := make ([]gin.H , 0 , len (talkers ))
874+ for _ , t := range talkers {
875+ enriched = append (enriched , gin.H {
876+ "nodeId" : t .NodeID ,
877+ "displayName" : h .resolveNodeName (t .NodeID ),
878+ "txBytes" : t .TxBytes ,
879+ "rxBytes" : t .RxBytes ,
880+ "totalBytes" : t .TotalBytes ,
881+ })
882+ }
883+
884+ c .JSON (http .StatusOK , gin.H {
885+ "talkers" : enriched ,
886+ "metadata" : gin.H {
887+ "start" : startTime ,
888+ "end" : endTime ,
889+ "limit" : limit ,
890+ "count" : len (enriched ),
891+ },
892+ })
893+ }
894+
895+ // GetTopPairs returns the top N node pairs by total traffic
896+ func (h * Handlers ) GetTopPairs (c * gin.Context ) {
897+ if h .store == nil {
898+ c .JSON (http .StatusServiceUnavailable , gin.H {"error" : "Database not configured" })
899+ return
900+ }
901+
902+ startTime , endTime , err := h .parseTimeRange (c )
903+ if err != nil {
904+ c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
905+ return
906+ }
907+
908+ limit := 10
909+ if l := c .Query ("limit" ); l != "" {
910+ if _ , err := fmt .Sscanf (l , "%d" , & limit ); err != nil || limit <= 0 {
911+ c .JSON (http .StatusBadRequest , gin.H {"error" : "invalid limit" })
912+ return
913+ }
914+ }
915+ if limit > 100 {
916+ limit = 100
917+ }
918+
919+ ctx , cancel := context .WithTimeout (c .Request .Context (), DefaultQueryTimeout )
920+ defer cancel ()
921+
922+ pairs , err := h .store .GetTopPairs (ctx , startTime , endTime , limit )
923+ if err != nil {
924+ log .Printf ("ERROR GetTopPairs: %v" , err )
925+ c .JSON (http .StatusInternalServerError , gin.H {
926+ "error" : "Failed to fetch top pairs" ,
927+ "message" : err .Error (),
928+ })
929+ return
930+ }
931+
932+ // Enrich with device names
933+ enriched := make ([]gin.H , 0 , len (pairs ))
934+ for _ , p := range pairs {
935+ enriched = append (enriched , gin.H {
936+ "srcNodeId" : p .SrcNodeID ,
937+ "srcDisplayName" : h .resolveNodeName (p .SrcNodeID ),
938+ "dstNodeId" : p .DstNodeID ,
939+ "dstDisplayName" : h .resolveNodeName (p .DstNodeID ),
940+ "txBytes" : p .TxBytes ,
941+ "rxBytes" : p .RxBytes ,
942+ "totalBytes" : p .TotalBytes ,
943+ "flowCount" : p .FlowCount ,
944+ })
945+ }
946+
947+ c .JSON (http .StatusOK , gin.H {
948+ "pairs" : enriched ,
949+ "metadata" : gin.H {
950+ "start" : startTime ,
951+ "end" : endTime ,
952+ "limit" : limit ,
953+ "count" : len (enriched ),
954+ },
955+ })
956+ }
957+
958+ // GetNodeDetailStats returns detailed stats for a specific node
959+ func (h * Handlers ) GetNodeDetailStats (c * gin.Context ) {
960+ if h .store == nil {
961+ c .JSON (http .StatusServiceUnavailable , gin.H {"error" : "Database not configured" })
962+ return
963+ }
964+
965+ nodeID := c .Param ("id" )
966+ if nodeID == "" {
967+ c .JSON (http .StatusBadRequest , gin.H {"error" : "node ID required" })
968+ return
969+ }
970+
971+ startTime , endTime , err := h .parseTimeRange (c )
972+ if err != nil {
973+ c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
974+ return
975+ }
976+
977+ ctx , cancel := context .WithTimeout (c .Request .Context (), DefaultQueryTimeout )
978+ defer cancel ()
979+
980+ stats , err := h .store .GetNodeStats (ctx , nodeID , startTime , endTime )
981+ if err != nil {
982+ log .Printf ("ERROR GetNodeDetailStats: %v" , err )
983+ c .JSON (http .StatusInternalServerError , gin.H {
984+ "error" : "Failed to fetch node stats" ,
985+ "message" : err .Error (),
986+ })
987+ return
988+ }
989+
990+ c .JSON (http .StatusOK , stats )
991+ }
0 commit comments