From 8198b65f29f83e2ff51d4c0f89a7b7dcbe9c2fc1 Mon Sep 17 00:00:00 2001
From: Eugene Bujak <hmage@hmage.net>
Date: Fri, 7 Sep 2018 17:49:33 +0300
Subject: [PATCH 1/5] API /stats_top -- show only top entries for last 3
 minutes

---
 control.go |  8 ++++++++
 helpers.go | 17 +++++++++++++++++
 2 files changed, 25 insertions(+)

diff --git a/control.go b/control.go
index 70cb9349..2552df53 100644
--- a/control.go
+++ b/control.go
@@ -409,6 +409,9 @@ func handleStatsTop(w http.ResponseWriter, r *http.Request) {
 	domains := map[string]int{}
 	blocked := map[string]int{}
 	clients := map[string]int{}
+	now := time.Now()
+	timeWindow := time.Minute * 3
+	notBefore := now.Add(timeWindow * -1)
 
 	for _, value := range values {
 		entry, ok := value.(map[string]interface{})
@@ -419,6 +422,11 @@ func handleStatsTop(w http.ResponseWriter, r *http.Request) {
 		host := getHost(entry)
 		reason := getReason(entry)
 		client := getClient(entry)
+		time := getTime(entry)
+		if time.Before(notBefore) {
+			// skip if the entry is before specified cutoff
+			continue
+		}
 		if len(host) > 0 {
 			domains[host]++
 		}
diff --git a/helpers.go b/helpers.go
index a460f98f..d39c5e93 100644
--- a/helpers.go
+++ b/helpers.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"sort"
 	"strings"
+	"time"
 )
 
 func clamp(value, low, high int) int {
@@ -167,6 +168,22 @@ func getClient(entry map[string]interface{}) string {
 	return client
 }
 
+func getTime(entry map[string]interface{}) time.Time {
+	t, ok := entry["time"]
+	if !ok {
+		return time.Time{}
+	}
+	tstr, ok := t.(string)
+	if !ok {
+		return time.Time{}
+	}
+	value, err := time.Parse(time.RFC3339, tstr)
+	if err != nil {
+		return time.Time{}
+	}
+	return value
+}
+
 // -------------------------------------------------
 // helper functions for parsing parameters from body
 // -------------------------------------------------

From f623c3d9093d5a953336ee6a2e74507070a7def8 Mon Sep 17 00:00:00 2001
From: Eugene Bujak <hmage@hmage.net>
Date: Fri, 7 Sep 2018 17:50:03 +0300
Subject: [PATCH 2/5] API /stats_top -- sort top entries by value

---
 control.go | 39 +++++++++++++++++++++++++++------------
 1 file changed, 27 insertions(+), 12 deletions(-)

diff --git a/control.go b/control.go
index 2552df53..8161c954 100644
--- a/control.go
+++ b/control.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
@@ -438,21 +439,35 @@ func handleStatsTop(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	toMarshal := map[string]interface{}{
-		"top_queried_domains": produceTop(domains, 50),
-		"top_blocked_domains": produceTop(blocked, 50),
-		"top_clients":         produceTop(clients, 50),
-	}
-	json, err := json.Marshal(toMarshal)
-	if err != nil {
-		errortext := fmt.Sprintf("Couldn't marshal into JSON: %s", err)
-		log.Println(errortext)
-		http.Error(w, errortext, http.StatusBadGateway)
-		return
+	// use manual json marshalling because we want maps to be sorted by value
+	json := bytes.Buffer{}
+	json.WriteString("{\n")
+
+	gen := func(json *bytes.Buffer, name string, top map[string]int, addComma bool) {
+		json.WriteString("  \"")
+		json.WriteString(name)
+		json.WriteString("\": {\n")
+		sorted := sortByValue(top)
+		for i, key := range sorted {
+			fmt.Fprintf(json, "    \"%s\": %d", key, top[key]))
+			if i+1 != len(sorted) {
+				json.WriteByte(',')
+			}
+			json.WriteByte('\n')
+		}
+		json.WriteString("  }")
+		if addComma {
+			json.WriteByte(',')
+		}
+		json.WriteByte('\n')
 	}
+	gen(&json, "top_queried_domains", domains, true)
+	gen(&json, "top_blocked_domains", blocked, true)
+	gen(&json, "top_clients", clients, false)
+	json.WriteString("}\n")
 
 	w.Header().Set("Content-Type", "application/json")
-	_, err = w.Write(json)
+	_, err = w.Write(json.Bytes())
 	if err != nil {
 		errortext := fmt.Sprintf("Couldn't write body: %s", err)
 		log.Println(errortext)

From 7094ed4f28e25d0610cbe0f68dd721b80e737442 Mon Sep 17 00:00:00 2001
From: Eugene Bujak <hmage@hmage.net>
Date: Fri, 7 Sep 2018 17:59:24 +0300
Subject: [PATCH 3/5] Fixup of previous commit -- errand keystroke crept in

---
 control.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/control.go b/control.go
index 8161c954..643458ba 100644
--- a/control.go
+++ b/control.go
@@ -449,7 +449,7 @@ func handleStatsTop(w http.ResponseWriter, r *http.Request) {
 		json.WriteString("\": {\n")
 		sorted := sortByValue(top)
 		for i, key := range sorted {
-			fmt.Fprintf(json, "    \"%s\": %d", key, top[key]))
+			fmt.Fprintf(json, "    \"%s\": %d", key, top[key])
 			if i+1 != len(sorted) {
 				json.WriteByte(',')
 			}

From 4ba8293c06891fae66e487d170cd7aa01eb45e93 Mon Sep 17 00:00:00 2001
From: Eugene Bujak <hmage@hmage.net>
Date: Fri, 7 Sep 2018 18:04:18 +0300
Subject: [PATCH 4/5] web interface -- change text from 'general counters' to
 'general statistics'

---
 client/src/components/Dashboard/Counters.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/components/Dashboard/Counters.js b/client/src/components/Dashboard/Counters.js
index c9fba7c2..094b78ed 100644
--- a/client/src/components/Dashboard/Counters.js
+++ b/client/src/components/Dashboard/Counters.js
@@ -5,7 +5,7 @@ import Card from '../ui/Card';
 import Tooltip from '../ui/Tooltip';
 
 const Counters = props => (
-    <Card title="General counters" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
+    <Card title="General statistics" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
         <table className="table card-table">
             <tbody>
                 <tr>

From 31893410892bd047c9f6ea8f602717e6996c9491 Mon Sep 17 00:00:00 2001
From: Eugene Bujak <hmage@hmage.net>
Date: Fri, 7 Sep 2018 18:04:31 +0300
Subject: [PATCH 5/5] web interface -- Make refresh buttons reload all data,
 not just counters

---
 client/src/components/Dashboard/index.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js
index 885b72f4..035ff9bc 100644
--- a/client/src/components/Dashboard/index.js
+++ b/client/src/components/Dashboard/index.js
@@ -27,8 +27,8 @@ class Dashboard extends Component {
             dashboard.processingTopStats;
 
         const disableButton = <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={() => this.props.disableDns()}>Disable DNS</button>;
-        const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.props.getStats()}>Refresh statistics</button>;
-        const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.props.getStats()}></button>;
+        const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.componentDidMount()}>Refresh statistics</button>;
+        const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.componentDidMount()}></button>;
 
         return (
             <Fragment>