diff --git a/README.md b/README.md
index 0d1e1be..4a19ef8 100644
--- a/README.md
+++ b/README.md
@@ -45,3 +45,5 @@ Counters also has support for Docker Compose, a extended version of docker that
- [] Add API Integration
- [] Add Logging (similar to Votes "Actions")
- [] Time Zone support
+
+
\ No newline at end of file
diff --git a/internal/database/counter.go b/internal/database/counter.go
index 941f2fd..5bce14c 100644
--- a/internal/database/counter.go
+++ b/internal/database/counter.go
@@ -2,12 +2,16 @@ package database
import (
"context"
+ "fmt"
+ "strings"
"time"
"github.com/ComputerScienceHouse/counters/internal/models"
"github.com/jackc/pgx/v5"
)
+const MAX_COUNTERS_IN_QUERY = 10
+
func CreateCounterWithReset(ctx context.Context, userId string, username string, cReq *models.CreateCounterInput, rReq *models.CreateResetInput) (string, error) {
tx, err := db.Begin(ctx)
if err != nil {
@@ -45,42 +49,61 @@ func CreateCounterWithReset(ctx context.Context, userId string, username string,
return counterId, nil
}
-func GetCounters(ctx context.Context) ([]*models.CounterListPart, error) {
- rows, err := db.Query(ctx, `
- SELECT c.counter_id, c.user_id, c.title, c.description, r.description, r.username, r.occured_at FROM counters c
+func GetCounters(ctx context.Context, filter *models.GetCountersFilter) ([]*models.GetCountersCounterPart, error) {
+ args := make([]any, 0)
+
+ postQuery := `
+ SELECT c.counter_id, c.title, r.description, r.username, r.occured_at FROM counters c
JOIN resets r ON c.last_reset = r.reset_id
- ORDER BY r.occured_at DESC
- `)
+ `
+ if filter.Timestamp != nil {
+ args = append(args, time.Unix(*filter.Timestamp, 0))
+
+ if filter.SortDescending {
+ postQuery = fmt.Sprintf("%s AND r.occured_at < $%d", postQuery, len(args))
+ } else {
+ postQuery = fmt.Sprintf("%s AND r.occured_at > $%d", postQuery, len(args))
+ }
+ }
+
+ if filter.Name != nil && strings.TrimSpace(*filter.Name) != "" {
+ args = append(args, "%"+*filter.Name+"%")
+ postQuery = fmt.Sprintf("%s AND c.title ILIKE $%d", postQuery, len(args))
+ }
+
+ if filter.SortDescending {
+ postQuery = fmt.Sprintf("%s ORDER BY r.occured_at DESC LIMIT %d", postQuery, MAX_COUNTERS_IN_QUERY)
+ } else {
+ postQuery = fmt.Sprintf("%s ORDER BY r.occured_at ASC LIMIT %d", postQuery, MAX_COUNTERS_IN_QUERY)
+ }
+
+ rows, err := db.Query(ctx, postQuery, args...)
if err != nil {
return nil, err
}
defer rows.Close()
- counters := make([]*models.CounterListPart, 0)
+ counters := make([]*models.GetCountersCounterPart, 0)
for rows.Next() {
var (
- counterId string
- counterOwnerID string
- counterTitle string
- counterDescription string
- resetDescription string
- resetUsername string
- resetCreatedAt time.Time
+ counterId string
+ counterTitle string
+ resetDescription string
+ resetUsername string
+ resetCreatedAt time.Time
)
- if err := rows.Scan(&counterId, &counterOwnerID, &counterTitle, &counterDescription, &resetDescription, &resetUsername, &resetCreatedAt); err != nil {
+ if err := rows.Scan(&counterId, &counterTitle, &resetDescription, &resetUsername, &resetCreatedAt); err != nil {
return nil, err
}
- counters = append(counters, &models.CounterListPart{
- CounterID: counterId,
- CounterOwner: counterOwnerID,
- CounterTitle: counterTitle,
- CounterDescription: counterDescription,
- ResetDescription: resetDescription,
- ResetUsername: resetUsername,
- ResetOccuredAt: resetCreatedAt.Unix(),
+ counters = append(counters, &models.GetCountersCounterPart{
+ CounterID: counterId,
+ CounterTitle: counterTitle,
+ ResetDescription: resetDescription,
+ ResetUsername: resetUsername,
+ ResetTimestamp: resetCreatedAt.Unix(),
})
}
diff --git a/internal/database/reset.go b/internal/database/reset.go
index afff888..06ee4db 100644
--- a/internal/database/reset.go
+++ b/internal/database/reset.go
@@ -2,6 +2,7 @@ package database
import (
"context"
+ "fmt"
"time"
"github.com/ComputerScienceHouse/counters/internal/models"
@@ -9,6 +10,8 @@ import (
"github.com/jackc/pgx/v5"
)
+const MAX_RESETS_IN_QUERY = 10
+
func CreateReset(ctx context.Context, userID string, username string, req *models.CreateResetInput) (string, error) {
resetID, err := GenerateUUID()
if err != nil {
@@ -73,12 +76,31 @@ func GetCounterFromReset(ctx context.Context, resetID string) (string, error) {
return counterID, nil
}
-func GetResetsFromCounterId(ctx context.Context, counterID string) ([]*models.ResetListPart, error) {
- rows, err := db.Query(ctx, `
+func GetResetsFromCounterId(ctx context.Context, counterID string, filter *models.GetResetsFilter) ([]*models.ResetListPart, error) {
+ args := make([]any, 0)
+ args = append(args, counterID)
+
+ postQuery := `
SELECT r.reset_id, r.description, r.user_id, r.username, r.occured_at FROM resets r
WHERE r.counter_id = $1
- ORDER BY r.occured_at DESC
- `, counterID)
+ `
+ if filter.Timestamp != nil {
+ args = append(args, time.Unix(*filter.Timestamp, 0))
+
+ if filter.SortDescending {
+ postQuery = fmt.Sprintf("%s AND r.occured_at < $%d", postQuery, len(args))
+ } else {
+ postQuery = fmt.Sprintf("%s AND r.occured_at > $%d", postQuery, len(args))
+ }
+ }
+
+ if filter.SortDescending {
+ postQuery = fmt.Sprintf("%s ORDER BY r.occured_at DESC LIMIT %d", postQuery, MAX_RESETS_IN_QUERY)
+ } else {
+ postQuery = fmt.Sprintf("%s ORDER BY r.occured_at ASC LIMIT %d", postQuery, MAX_RESETS_IN_QUERY)
+ }
+
+ rows, err := db.Query(ctx, postQuery, args...)
if err != nil {
return nil, err
}
@@ -92,10 +114,10 @@ func GetResetsFromCounterId(ctx context.Context, counterID string) ([]*models.Re
resetDescription string
resetOwner string
resetUsername string
- resetOccuredAt time.Time
+ resetOccurredAt time.Time
)
- if err := rows.Scan(&resetId, &resetDescription, &resetOwner, &resetUsername, &resetOccuredAt); err != nil {
+ if err := rows.Scan(&resetId, &resetDescription, &resetOwner, &resetUsername, &resetOccurredAt); err != nil {
return nil, err
}
@@ -104,7 +126,7 @@ func GetResetsFromCounterId(ctx context.Context, counterID string) ([]*models.Re
ResetDescription: resetDescription,
ResetOwner: resetOwner,
ResetUsername: resetUsername,
- ResetOccuredAt: resetOccuredAt.Unix(),
+ ResetOccurredAt: resetOccurredAt.Unix(),
})
}
diff --git a/internal/models/api_counter.go b/internal/models/api_counter.go
index f729074..66e83e9 100644
--- a/internal/models/api_counter.go
+++ b/internal/models/api_counter.go
@@ -1,5 +1,30 @@
package models
+type GetCountersFilter struct {
+ Timestamp *int64 `json:"timestamp"`
+ SortDescending bool `json:"sort_descending"`
+ Name *string `json:"name"`
+}
+
+type GetCountersCounterPart struct {
+ CounterID string `json:"counter_id"`
+ CounterTitle string `json:"counter_title"`
+ ResetDescription string `json:"reset_description"`
+ ResetUsername string `json:"reset_username"`
+ ResetTimestamp int64 `json:"reset_timestamp"`
+}
+
+type GetCountersInput struct {
+ CounterID string `json:"counter_id"`
+ Filter GetCountersFilter `json:"filter"`
+}
+
+type GetCountersOutput struct {
+ Counters []*GetCountersCounterPart `json:"counters"`
+ HasMore bool `json:"has_more"`
+ Timestamp *int64 `json:"timestamp"`
+}
+
type CreateCounterWithResetInput struct {
Counter CreateCounterInput `json:"counter"`
Reset CreateResetInput `json:"reset"`
diff --git a/internal/models/api_reset.go b/internal/models/api_reset.go
index 56d014e..5cd04ec 100644
--- a/internal/models/api_reset.go
+++ b/internal/models/api_reset.go
@@ -1,5 +1,29 @@
package models
+type GetResetsFilter struct {
+ Timestamp *int64 `json:"timestamp"`
+ SortDescending bool `json:"sort_descending"`
+}
+
+type GetResetsResetPart struct {
+ Id string `json:"id"`
+ Username string `json:"username"`
+ Description string `json:"description"`
+ Timestamp int64 `json:"timestamp"`
+ CanDelete bool `json:"can_delete"`
+}
+
+type GetResetsInput struct {
+ CounterID string `json:"counter_id"`
+ Filter GetResetsFilter `json:"filter"`
+}
+
+type GetResetsOutput struct {
+ Resets []*GetResetsResetPart `json:"resets"`
+ HasMore bool `json:"has_more"`
+ Timestamp *int64 `json:"timestamp"`
+}
+
type CreateResetInput struct {
CounterID string `json:"counter_id"`
Description string `json:"description"`
diff --git a/internal/models/reset.go b/internal/models/reset.go
index cf90581..b6b67f8 100644
--- a/internal/models/reset.go
+++ b/internal/models/reset.go
@@ -13,5 +13,5 @@ type ResetListPart struct {
ResetDescription string `json:"reset_description"`
ResetOwner string `json:"reset_owner"`
ResetUsername string `json:"reset_username"`
- ResetOccuredAt int64 `json:"reset_occured_at"`
+ ResetOccurredAt int64 `json:"reset_occurred_at"`
}
diff --git a/internal/routes/api/v1/counters/counter.go b/internal/routes/api/v1/counters/counter.go
index fc88b0f..6a8f21d 100644
--- a/internal/routes/api/v1/counters/counter.go
+++ b/internal/routes/api/v1/counters/counter.go
@@ -1,7 +1,9 @@
package counters
import (
+ "context"
"net/http"
+ "time"
"github.com/ComputerScienceHouse/counters/internal/database"
"github.com/ComputerScienceHouse/counters/internal/logging"
@@ -12,38 +14,42 @@ import (
"github.com/sirupsen/logrus"
)
-func GetCounter(c *gin.Context) {
- user, err := users.GetCSHAuth(c)
+func GetCounters(c *gin.Context) {
+ _, err := users.GetCSHAuth(c)
if err != nil {
c.Status(http.StatusUnauthorized)
return
}
- idToUse := c.Param("id")
- counter, err := database.GetCounterFromId(c.Request.Context(), idToUse)
- if err != nil {
+ ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15)
+ defer cancel()
+
+ var req models.GetCountersInput
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error binding req")
c.Status(http.StatusBadRequest)
return
}
- resets, err := database.GetResetsFromCounterId(c.Request.Context(), idToUse)
+ counters, err := database.GetCounters(ctx, &req.Filter)
if err != nil {
+ logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("error getting counters!")
c.Status(http.StatusBadRequest)
return
}
- c.HTML(200, "counter.tmpl", gin.H{
- "Id": counter.CounterID,
- "CounterID": counter.CounterOwner,
- "Title": counter.CounterTitle,
- "Description": counter.CounterDescription,
- "Timestamp": counter.ResetOccuredAt,
- "History": resets,
- "Username": user.Username,
- "FullName": user.FullName,
- "UserID": user.Uuid,
- "EBoard": users.IsEboard(user),
- "RTP": users.IsActiveRTP(user),
+ var hasMore bool = false
+ var timestamp int64 = 0
+ if len(counters) > 1 {
+ hasMore = (len(counters) > (database.MAX_COUNTERS_IN_QUERY)-1)
+ timestamp = counters[len(counters)-1].ResetTimestamp
+ }
+
+ c.JSON(http.StatusOK, models.GetCountersOutput{
+ Counters: counters,
+ HasMore: hasMore,
+ Timestamp: ×tamp,
})
}
@@ -55,6 +61,9 @@ func DeleteCounter(c *gin.Context) {
return
}
+ ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15)
+ defer cancel()
+
var req models.DeleteCounterInput
if err := c.ShouldBindJSON(&req); err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error casting request")
@@ -62,7 +71,7 @@ func DeleteCounter(c *gin.Context) {
return
}
- counterOwner, err := database.GetCounterOwner(c.Request.Context(), req.RowID)
+ counterOwner, err := database.GetCounterOwner(ctx, req.RowID)
if err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error getting counter owner")
c.Status(http.StatusBadRequest)
@@ -76,11 +85,12 @@ func DeleteCounter(c *gin.Context) {
return
}
- if err := database.DeleteCounter(c.Request.Context(), req.RowID); err != nil {
+ if err := database.DeleteCounter(ctx, req.RowID); err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error deleting counter")
c.Status(http.StatusInternalServerError)
return
}
+
c.Status(http.StatusNoContent)
}
@@ -91,6 +101,9 @@ func CreateCounter(c *gin.Context) {
return
}
+ ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15)
+ defer cancel()
+
var req models.CreateCounterWithResetInput
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
@@ -105,7 +118,7 @@ func CreateCounter(c *gin.Context) {
return
}
- counterID, err := database.CreateCounterWithReset(c.Request.Context(), user.Uuid, user.Username, &req.Counter, &req.Reset)
+ counterID, err := database.CreateCounterWithReset(ctx, user.Uuid, user.Username, &req.Counter, &req.Reset)
if err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "counters", "error": err}).Warning("Error creating new database")
c.Status(http.StatusBadRequest)
@@ -121,4 +134,6 @@ func Routes(r *gin.RouterGroup) {
cGroup.GET("/:id")
cGroup.POST("", CreateCounter)
cGroup.DELETE("", DeleteCounter)
+
+ cGroup.Handle("QUERY", "", GetCounters)
}
diff --git a/internal/routes/api/v1/reset/reset.go b/internal/routes/api/v1/reset/reset.go
index beb59b3..b4ea626 100644
--- a/internal/routes/api/v1/reset/reset.go
+++ b/internal/routes/api/v1/reset/reset.go
@@ -1,7 +1,9 @@
package reset
import (
+ "context"
"net/http"
+ "time"
"github.com/ComputerScienceHouse/counters/internal/database"
"github.com/ComputerScienceHouse/counters/internal/logging"
@@ -11,6 +13,62 @@ import (
"github.com/sirupsen/logrus"
)
+func GetResets(c *gin.Context) {
+ user, err := users.GetCSHAuth(c)
+ if err != nil {
+ c.Status(http.StatusUnauthorized)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15)
+ defer cancel()
+
+ var req models.GetResetsInput
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error binding req")
+ c.Status(http.StatusBadRequest)
+ return
+ }
+
+ resets, err := database.GetResetsFromCounterId(ctx, req.CounterID, &req.Filter)
+ if err != nil {
+ logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error getting resets for counter id")
+ c.Status(http.StatusBadRequest)
+ return
+ }
+
+ finalResets := make([]*models.GetResetsResetPart, len(resets))
+
+ isCounterOwner := req.CounterID == user.Uuid
+
+ for index, reset := range resets {
+ isResetOwner := reset.ResetID == user.Uuid
+
+ finalResets[index] = &models.GetResetsResetPart{
+ Id: reset.ResetID,
+ Username: reset.ResetUsername,
+ Description: reset.ResetDescription,
+ Timestamp: reset.ResetOccurredAt,
+ CanDelete: (users.IsEboard(user) || users.IsActiveRTP(user) || isCounterOwner || isResetOwner),
+ }
+ }
+
+ var hasMore bool = false
+ var timestamp int64 = 0
+
+ if len(finalResets) > 1 {
+ hasMore = (len(finalResets) > (database.MAX_RESETS_IN_QUERY)-1)
+ timestamp = finalResets[len(finalResets)-1].Timestamp
+ }
+
+ c.JSON(http.StatusOK, models.GetResetsOutput{
+ Resets: finalResets,
+ HasMore: hasMore,
+ Timestamp: ×tamp,
+ })
+}
+
func CreateReset(c *gin.Context) {
user, err := users.GetCSHAuth(c)
if err != nil {
@@ -18,6 +76,9 @@ func CreateReset(c *gin.Context) {
return
}
+ ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15)
+ defer cancel()
+
var req models.CreateResetInput
if err := c.ShouldBindJSON(&req); err != nil {
logging.Logger.Warnf("failed to cast error %s", err)
@@ -31,13 +92,13 @@ func CreateReset(c *gin.Context) {
return
}
- if _, err := database.CreateReset(c.Request.Context(), user.Uuid, user.Username, &req); err != nil {
+ if _, err := database.CreateReset(ctx, user.Uuid, user.Username, &req); err != nil {
logging.Logger.Warnf("failed to create error %s", err)
c.Status(http.StatusBadRequest)
return
}
- if _, err := database.UpdateCounterLastReset(c.Request.Context(), req.CounterID); err != nil {
+ if _, err := database.UpdateCounterLastReset(ctx, req.CounterID); err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error updating last reset")
c.Status(http.StatusInternalServerError)
return
@@ -54,6 +115,9 @@ func DeleteReset(c *gin.Context) {
return
}
+ ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15)
+ defer cancel()
+
var req models.DeleteResetInput
if err := c.ShouldBindJSON(&req); err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error compiling reset")
@@ -61,21 +125,21 @@ func DeleteReset(c *gin.Context) {
return
}
- counterID, err := database.GetCounterFromReset(c.Request.Context(), req.RowID)
+ counterID, err := database.GetCounterFromReset(ctx, req.RowID)
if err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error getting counter from reset")
c.Status(http.StatusBadRequest)
return
}
- counterOwner, err := database.GetCounterOwner(c.Request.Context(), counterID)
+ counterOwner, err := database.GetCounterOwner(ctx, counterID)
if err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error getting counter owner")
c.Status(http.StatusBadRequest)
return
}
- resetOwner, err := database.GetResetOwner(c.Request.Context(), req.RowID)
+ resetOwner, err := database.GetResetOwner(ctx, req.RowID)
if err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error deleting reset")
c.Status(http.StatusBadRequest)
@@ -90,13 +154,13 @@ func DeleteReset(c *gin.Context) {
return
}
- if err := database.DeleteReset(c.Request.Context(), counterID, req.RowID); err != nil {
+ if err := database.DeleteReset(ctx, counterID, req.RowID); err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error deleting reset")
c.Status(http.StatusInternalServerError)
return
}
- counterExists, err := database.UpdateCounterLastReset(c.Request.Context(), counterID)
+ counterExists, err := database.UpdateCounterLastReset(ctx, counterID)
if err != nil {
logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error updating last reset")
c.Status(http.StatusInternalServerError)
@@ -110,6 +174,8 @@ func Routes(r *gin.RouterGroup) {
rGroup := r.Group("/reset")
rGroup.GET("/:id")
- rGroup.POST("", CreateReset) // create reset
+ rGroup.POST("", CreateReset)
rGroup.DELETE("", DeleteReset)
+
+ rGroup.Handle("QUERY", "", GetResets)
}
diff --git a/internal/routes/web/pages/counter.go b/internal/routes/web/pages/counter.go
index e736f22..6c37edf 100644
--- a/internal/routes/web/pages/counter.go
+++ b/internal/routes/web/pages/counter.go
@@ -32,23 +32,13 @@ func GetCounterPage(c *gin.Context) {
return
}
- history, err := database.GetResetsFromCounterId(c.Request.Context(), idToUse)
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error getting history for counter")
- c.JSON(http.StatusBadRequest, models.NewErrorResponse())
- return
- }
-
- logging.Logger.Info(counter.CounterDescription)
-
c.HTML(http.StatusOK, "counter.tmpl", gin.H{
- "Id": counter.CounterID,
- "CounterID": counter.CounterOwner,
- "Title": counter.CounterTitle,
- "Description": counter.CounterDescription,
- "Timestamp": counter.ResetOccuredAt,
+ "CounterID": counter.CounterID,
+ "CounterOwnerUUID": counter.CounterOwner,
+ "CounterTitle": counter.CounterTitle,
+ "CounterDescription": counter.CounterDescription,
+ "MostRecentTimestamp": counter.ResetOccuredAt,
- "History": history,
"Username": user.Username,
"FullName": user.FullName,
"UserID": user.Uuid,
diff --git a/internal/routes/web/pages/home.go b/internal/routes/web/pages/home.go
index 1c22429..3344dc6 100644
--- a/internal/routes/web/pages/home.go
+++ b/internal/routes/web/pages/home.go
@@ -2,13 +2,9 @@ package pages
import (
"net/http"
- "sort"
- "github.com/ComputerScienceHouse/counters/internal/database"
- "github.com/ComputerScienceHouse/counters/internal/logging"
"github.com/ComputerScienceHouse/counters/internal/users"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
)
func GetHomePage(c *gin.Context) {
@@ -18,20 +14,7 @@ func GetHomePage(c *gin.Context) {
return
}
- counters, err := database.GetCounters(c.Request.Context())
-
- if err != nil {
- logging.Logger.WithFields(logrus.Fields{"error": err, "module": "api", "method": "GetHomePage"}).Warn("Unable to load counters!")
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load counters!"})
- return
- }
-
- sort.Slice(counters, func(i, j int) bool {
- return counters[i].ResetOccuredAt > counters[j].ResetOccuredAt
- })
-
- c.HTML(http.StatusOK, "index.html", gin.H{
- "Counters": counters,
+ c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Username": user.Username,
"FullName": user.FullName,
"EBoard": users.IsEboard(user),
diff --git a/internal/routes/web/pages/reset.go b/internal/routes/web/pages/reset.go
index 79aadec..f128874 100644
--- a/internal/routes/web/pages/reset.go
+++ b/internal/routes/web/pages/reset.go
@@ -15,6 +15,7 @@ func GetResetPage(c *gin.Context) {
}
idToUse := c.Param("id")
+
c.HTML(http.StatusOK, "reset.tmpl", gin.H{
"Id": idToUse,
"Username": user.Username,
diff --git a/templates/counter.tmpl b/templates/counter.tmpl
index 49ff42c..94e57a5 100644
--- a/templates/counter.tmpl
+++ b/templates/counter.tmpl
@@ -1,5 +1,34 @@
- {{ define "counter.tmpl"}}
- {{ template "header.tmpl" . }}
+{{ define "counter.tmpl"}}
+{{ template "header.tmpl" . }}
+
+
+
+ reported by:
+