diff --git a/pkg/rest/stats.go b/pkg/rest/stats.go index 229b5d4..ba19f3a 100644 --- a/pkg/rest/stats.go +++ b/pkg/rest/stats.go @@ -35,6 +35,13 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { case "GB": unitAdjustment = 1024 * 1024 * 1024 } + offset := 0 + if r.FormValue("offset") != "" { + i, err := strconv.Atoi(r.FormValue("offset")) + if err == nil { + offset = i + } + } // get all users users := c.UserStore.ListUsers() userMap := make(map[string]string) @@ -43,22 +50,24 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { } // calculate stats var userStatsResponse UserStatsResponse - statsFile := c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.Format("2006-01-02")+".log")) - if !c.Storage.Client.FileExists(statsFile) { // file does not exist so just return empty response - out, err := json.Marshal(userStatsResponse) - if err != nil { - c.returnError(w, fmt.Errorf("user stats response marshal error: %s", err), http.StatusBadRequest) - return - } - c.write(w, out) - return + statsFiles := []string{ + c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.AddDate(0, 0, -1).Format("2006-01-02")+".log")), + c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.Format("2006-01-02")+".log")), + c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.AddDate(0, 0, 1).Format("2006-01-02")+".log")), } - logData, err := c.Storage.Client.ReadFile(statsFile) - if err != nil { - c.returnError(w, fmt.Errorf("readfile error: %s", err), http.StatusBadRequest) - return + logData := bytes.NewBuffer([]byte{}) + for _, statsFile := range statsFiles { + if c.Storage.Client.FileExists(statsFile) { + fileLogData, err := c.Storage.Client.ReadFile(statsFile) + if err != nil { + c.returnError(w, fmt.Errorf("readfile error: %s", err), http.StatusBadRequest) + return + } + logData.Write(fileLogData) + } } - scanner := bufio.NewScanner(bytes.NewReader(logData)) + + scanner := bufio.NewScanner(logData) receiveBytesLast := make(map[string]int64) transmitBytesLast := make(map[string]int64) @@ -89,7 +98,13 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { receiveBytesData[userID] = []UserStatsDataPoint{} } value := math.Round(float64((receiveBytes-receiveBytesLast[userID])/unitAdjustment*100)) / 100 - receiveBytesData[userID] = append(receiveBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: value}) + timestamp, err := time.Parse(wireguard.TIMESTAMP_FORMAT, inputSplit[0]) + if err == nil { + timestamp = timestamp.Add(time.Duration(offset) * time.Minute) + if dateEqual(timestamp, date) { + receiveBytesData[userID] = append(receiveBytesData[userID], UserStatsDataPoint{X: timestamp.Format(wireguard.TIMESTAMP_FORMAT), Y: value}) + } + } } transmitBytes, err := strconv.ParseInt(inputSplit[4], 10, 64) if err == nil { @@ -97,7 +112,13 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { transmitBytesData[userID] = []UserStatsDataPoint{} } value := math.Round(float64((transmitBytes-transmitBytesLast[userID])/unitAdjustment*100)) / 100 - transmitBytesData[userID] = append(transmitBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: value}) + timestamp, err := time.Parse(wireguard.TIMESTAMP_FORMAT, inputSplit[0]) + if err == nil { + timestamp = timestamp.Add(time.Duration(offset) * time.Minute) + if dateEqual(timestamp, date) { + transmitBytesData[userID] = append(transmitBytesData[userID], UserStatsDataPoint{X: timestamp.Format(wireguard.TIMESTAMP_FORMAT), Y: value}) + } + } } receiveBytesLast[userID] = receiveBytes transmitBytesLast[userID] = transmitBytes @@ -161,3 +182,9 @@ func getColor(i int) string { } return colors[i%len(colors)] } + +func dateEqual(date1, date2 time.Time) bool { + y1, m1, d1 := date1.Date() + y2, m2, d2 := date2.Date() + return y1 == y2 && m1 == m2 && d1 == d2 +} diff --git a/pkg/wireguard/constants.go b/pkg/wireguard/constants.go index 8b82074..e2bce0e 100644 --- a/pkg/wireguard/constants.go +++ b/pkg/wireguard/constants.go @@ -44,3 +44,6 @@ PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACC const ACTION_ADD = "add" const ACTION_DELETE = "delete" const ACTION_CLEANUP = "cleanup" + +// stats +const TIMESTAMP_FORMAT = "2006-01-02T15:04:05" diff --git a/pkg/wireguard/stats_linux.go b/pkg/wireguard/stats_linux.go index 93d57de..e82495f 100644 --- a/pkg/wireguard/stats_linux.go +++ b/pkg/wireguard/stats_linux.go @@ -17,7 +17,6 @@ import ( ) const RUN_STATS_INTERVAL = 5 -const TIMESTAMP_FORMAT = "2006-01-02T15:04:05" func RunStats(storage storage.Iface) { err := storage.EnsurePath(storage.ConfigPath(VPN_STATS_DIR)) diff --git a/webapp/src/Routes/Home/UserStats.tsx b/webapp/src/Routes/Home/UserStats.tsx index 60b44e0..c7431fd 100644 --- a/webapp/src/Routes/Home/UserStats.tsx +++ b/webapp/src/Routes/Home/UserStats.tsx @@ -1,4 +1,4 @@ -import { Card, Center, Divider, Grid, Select, Text } from "@mantine/core"; +import { Card, Center, Grid, Select, Text } from "@mantine/core"; import { DatePickerInput } from '@mantine/dates'; import { useQuery } from "@tanstack/react-query"; import { useAuthContext } from "../../Auth/Auth"; @@ -8,18 +8,17 @@ import { Chart } from 'react-chartjs-2'; import 'chartjs-adapter-date-fns'; import { Chart as ChartJS, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, ChartOptions, Legend } from 'chart.js'; import { useState } from "react"; - export function UserStats() { ChartJS.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, Legend); - + const timezoneOffset = new Date().getTimezoneOffset() * -1 const {authInfo} = useAuthContext() const [statsDate, setStatsDate] = useState(new Date()); const [unit, setUnit] = useState("MB") const { isPending, error, data } = useQuery({ queryKey: ['userstats', statsDate, unit], queryFn: () => - fetch(AppSettings.url + '/stats/user/' + format(statsDate === null ? new Date() : statsDate, "yyyy-MM-dd") + "?unit=" +unit, { + fetch(AppSettings.url + '/stats/user/' + format(statsDate === null ? new Date() : statsDate, "yyyy-MM-dd") + "?offset="+timezoneOffset+"&unit=" +unit, { headers: { "Content-Type": "application/json", "Authorization": "Bearer " + authInfo.token @@ -43,12 +42,6 @@ export function UserStats() { scales: { x: { type: 'time', - min: '00:00:00', - /*time: { - displayFormats: { - quarter: 'HHHH MM' - } - }*/ }, y: { min: 0 @@ -58,7 +51,13 @@ export function UserStats() { if (isPending) return '' if (error) return 'cannot retrieve licensed users' - + + if(data.receivedBytes.datasets === null) { + data.receivedBytes.datasets = [{ data: [0], label: "no data"}] + } + if(data.transmitBytes.datasets === null) { + data.transmitBytes.datasets = [{ data: [0], label: "no data"}] + } return ( <>