diff --git a/data/.gitignore b/data/.gitignore
index 2c57205..fc5edfe 100644
--- a/data/.gitignore
+++ b/data/.gitignore
@@ -1 +1,3 @@
-data.db*
\ No newline at end of file
+*.db
+*.db-wal
+*.db-shm
\ No newline at end of file
diff --git a/data/tracking.db b/data/tracking.db
deleted file mode 100644
index dd167d4..0000000
Binary files a/data/tracking.db and /dev/null differ
diff --git a/go.mod b/go.mod
index d428ee8..c7ec811 100644
--- a/go.mod
+++ b/go.mod
@@ -8,22 +8,31 @@ require (
github.com/labstack/gommon v0.4.2
github.com/prometheus/client_golang v1.23.0
golang.org/x/net v0.42.0
+ modernc.org/sqlite v1.38.2
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.40.0 // indirect
+ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
+ modernc.org/libc v1.66.3 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
)
diff --git a/go.sum b/go.sum
index 03561b1..2833817 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -22,6 +28,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
@@ -32,6 +40,8 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -42,8 +52,14 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
+golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
+golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
@@ -51,7 +67,35 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
+golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
+modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
+modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
+modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
+modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
+modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
+modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
+modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/prometheus.yml b/prometheus.yml
index 651b6c6..38cde9d 100644
--- a/prometheus.yml
+++ b/prometheus.yml
@@ -34,13 +34,16 @@ scrape_configs:
# Timeout for scraping
scrape_timeout: 10s
+ # Use HTTPS to connect to the target
+ scheme: https
+
# Basic authentication for protected endpoints
basic_auth:
username: 'api'
password: 'your-default-token' # This should match your API_TOKEN env var
static_configs:
- - targets: ['knet:54321'] # Your knet application
+ - targets: ['knet.sk'] # Your knet application
labels:
instance: 'knet-prod'
environment: 'production'
diff --git a/src/main.go b/src/main.go
index 837b27f..4442060 100644
--- a/src/main.go
+++ b/src/main.go
@@ -16,6 +16,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"knet.sk/src/boxes"
"knet.sk/src/draw"
+ "knet.sk/src/tracking"
"knet.sk/src/util"
)
@@ -100,6 +101,11 @@ func main() {
}))
}
+ // MY supper tracking
+ e.Use(tracking.Echo)
+ trackingGroup := e.Group("/tracking")
+ trackingGroup.GET("", tracking.BasicTrackingHandler)
+
// Prometheus metrics endpoint (standard /metrics)
metricsGroup := e.Group("/metrics")
if util.IsProd() {
@@ -148,7 +154,9 @@ func main() {
return draw.Page(c)
})
e.GET("/draw/ws", draw.InitWs)
- e.GET("/draw/ws", draw.InitWs)
+
+ // Tracking dashboard
+ e.GET("/tracking", tracking.BasicTrackingHandler)
go boxes.RegisterTicker()
diff --git a/src/tracking/middleware.go b/src/tracking/middleware.go
new file mode 100644
index 0000000..7d72a0c
--- /dev/null
+++ b/src/tracking/middleware.go
@@ -0,0 +1,61 @@
+package tracking
+
+import (
+ "fmt"
+
+ "github.com/labstack/echo/v4"
+ "knet.sk/src/util"
+)
+
+type TrackingData struct {
+ BuildNumber string
+ Integrity *util.AssetIntegrity
+ TotalVisits int
+ UniqueVisitors int
+ TodayVisits int
+ ActiveSessions int
+ RecentVisits []VisitDisplay
+ TopPages []PageStat
+}
+
+type VisitDisplay struct {
+ Timestamp string
+ IPAddress string
+ Path string
+ UserAgent string
+ Referrer string
+}
+
+type PageStat struct {
+ Path string
+ Count int
+ Percentage float64
+}
+
+var ts *TrackingService
+
+func Echo(next echo.HandlerFunc) echo.HandlerFunc {
+ ts = StartTrackingService()
+ return func(c echo.Context) error {
+ fmt.Printf("Tracking visit: %s %s\n", c.RealIP(), c.Request().URL.Path)
+ go ts.AddVisit(c)
+ return next(c)
+ }
+}
+
+func BasicTrackingHandler(c echo.Context) error {
+
+ // Generate mock data
+ mockData := TrackingData{
+ BuildNumber: util.GetBuildNumber(),
+ Integrity: util.CalculateAssetIntegrities(),
+ TotalVisits: ts.GetTotalVisits(),
+ UniqueVisitors: ts.GetUniqueVisitors(),
+ TodayVisits: ts.GetTodayVisits(),
+ ActiveSessions: ts.GetActiveSessions(),
+ RecentVisits: ts.GetRecentVisits(20),
+ TopPages: ts.GetTopPages(5),
+ }
+
+ return c.Render(200, "tracking", mockData)
+}
diff --git a/src/tracking/tracking-store.go b/src/tracking/tracking-store.go
new file mode 100644
index 0000000..b3cddf1
--- /dev/null
+++ b/src/tracking/tracking-store.go
@@ -0,0 +1,225 @@
+package tracking
+
+import (
+ "database/sql"
+ "sync"
+
+ "github.com/labstack/echo/v4"
+ _ "modernc.org/sqlite"
+)
+
+type TrackingService struct {
+ db *sql.DB
+ mu sync.RWMutex // Add mutex for thread safety
+}
+
+var trackingService *TrackingService
+
+func StartTrackingService() *TrackingService {
+ if trackingService == nil {
+ db, err := sql.Open("sqlite", "./data/tracking.db")
+ if err != nil {
+ panic(err)
+ }
+
+ // Apply SQLite settings explicitly with PRAGMA statements
+ pragmas := []string{
+ "PRAGMA busy_timeout = 10000", // 10 second busy timeout
+ "PRAGMA journal_mode = WAL", // Write-Ahead Logging mode
+ "PRAGMA synchronous = NORMAL", // Normal synchronization
+ "PRAGMA foreign_keys = ON", // Enable foreign key constraints
+ "PRAGMA temp_store = MEMORY", // Store temp tables in memory
+ }
+
+ for _, pragma := range pragmas {
+ if _, err := db.Exec(pragma); err != nil {
+ println("Warning: Failed to set pragma:", pragma, "Error:", err.Error())
+ }
+ }
+
+ // With mutex protection, we can use a single connection
+ db.SetMaxOpenConns(1)
+ db.SetMaxIdleConns(1)
+
+ trackingService = &TrackingService{db: db}
+ err = trackingService.initDB()
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ return trackingService
+}
+
+func (ts *TrackingService) AddVisit(c echo.Context) {
+ ts.mu.Lock()
+ defer ts.mu.Unlock()
+
+ query := `INSERT INTO user_visits (ip, path, method, fingerprint, user_agent, referer, accept_language)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
+
+ fingerprint := getFingerprint(c)
+ referer := c.Request().Header.Get("Referer")
+ userAgent := c.Request().Header.Get("User-Agent")
+ acceptLanguage := c.Request().Header.Get("Accept-Language")
+ ip := c.RealIP()
+ path := c.Request().URL.Path
+ method := c.Request().Method
+
+ _, err := ts.db.Exec(query,
+ ip,
+ path,
+ method,
+ fingerprint,
+ userAgent,
+ referer,
+ acceptLanguage)
+
+ if err != nil {
+ // Log error instead of panic to prevent crashes
+ println("Error adding visit:", err.Error())
+ }
+}
+
+func (ts *TrackingService) initDB() error {
+ query := `
+ CREATE TABLE IF NOT EXISTS user_visits (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ ip TEXT NOT NULL,
+ path TEXT NOT NULL,
+ method TEXT NOT NULL,
+ fingerprint TEXT,
+ user_agent TEXT,
+ referer TEXT,
+ accept_language TEXT,
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ `
+
+ _, err := ts.db.Exec(query)
+ return err
+}
+
+func (ts *TrackingService) GetTotalVisits() int {
+ ts.mu.RLock()
+ defer ts.mu.RUnlock()
+
+ var count int
+ query := `SELECT COUNT(*) FROM user_visits`
+ err := ts.db.QueryRow(query).Scan(&count)
+ if err != nil {
+ println("Error getting total visits:", err.Error())
+ return 0
+ }
+
+ return count
+}
+
+func (ts *TrackingService) GetUniqueVisitors() int {
+ ts.mu.RLock()
+ defer ts.mu.RUnlock()
+
+ var count int
+ query := `SELECT COUNT(DISTINCT fingerprint) FROM user_visits`
+ err := ts.db.QueryRow(query).Scan(&count)
+ if err != nil {
+ println("Error getting unique visitors:", err.Error())
+ return 0
+ }
+
+ return count
+}
+
+func (ts *TrackingService) GetTodayVisits() int {
+ ts.mu.RLock()
+ defer ts.mu.RUnlock()
+
+ var count int
+ query := `SELECT COUNT(*) FROM user_visits WHERE DATE(timestamp) = DATE('now')`
+ err := ts.db.QueryRow(query).Scan(&count)
+ if err != nil {
+ println("Error getting today visits:", err.Error())
+ return 0
+ }
+
+ return count
+}
+
+func (ts *TrackingService) GetActiveSessions() int {
+ ts.mu.RLock()
+ defer ts.mu.RUnlock()
+
+ var count int
+ query := `SELECT COUNT(DISTINCT fingerprint) FROM user_visits WHERE timestamp >= datetime('now', '-30 minutes')`
+ err := ts.db.QueryRow(query).Scan(&count)
+ if err != nil {
+ println("Error getting active sessions:", err.Error())
+ return 0
+ }
+
+ return count
+}
+
+func (ts *TrackingService) GetRecentVisits(limit int) []VisitDisplay {
+ ts.mu.RLock()
+ defer ts.mu.RUnlock()
+
+ query := `SELECT timestamp, ip, path, user_agent, referer FROM user_visits ORDER BY timestamp DESC LIMIT ?`
+ rows, err := ts.db.Query(query, limit)
+ if err != nil {
+ println("Error getting recent visits:", err.Error())
+ return []VisitDisplay{}
+ }
+ defer rows.Close()
+
+ var visits []VisitDisplay
+ for rows.Next() {
+ var visit VisitDisplay
+ if err := rows.Scan(&visit.Timestamp, &visit.IPAddress, &visit.Path, &visit.UserAgent, &visit.Referrer); err != nil {
+ println("Error scanning visit:", err.Error())
+ continue
+ }
+ visits = append(visits, visit)
+ }
+
+ return visits
+}
+
+func (ts *TrackingService) GetTopPages(limit int) []PageStat {
+ ts.mu.RLock()
+ defer ts.mu.RUnlock()
+
+ // First get total visits count
+ var totalVisits int
+ totalQuery := `SELECT COUNT(*) FROM user_visits`
+ err := ts.db.QueryRow(totalQuery).Scan(&totalVisits)
+ if err != nil {
+ println("Error getting total visits for top pages:", err.Error())
+ return []PageStat{}
+ }
+
+ // Then get the top pages
+ query := `SELECT path, COUNT(*) as count FROM user_visits GROUP BY path ORDER BY count DESC LIMIT ?`
+ rows, err := ts.db.Query(query, limit)
+ if err != nil {
+ println("Error getting top pages:", err.Error())
+ return []PageStat{}
+ }
+ defer rows.Close()
+
+ var pages []PageStat
+ for rows.Next() {
+ var page PageStat
+ if err := rows.Scan(&page.Path, &page.Count); err != nil {
+ println("Error scanning page:", err.Error())
+ continue
+ }
+ // Calculate percentage using the totalVisits we got earlier
+ if totalVisits > 0 {
+ page.Percentage = float64(page.Count) / float64(totalVisits) * 100
+ }
+ pages = append(pages, page)
+ }
+
+ return pages
+}
diff --git a/src/tracking/util.go b/src/tracking/util.go
new file mode 100644
index 0000000..cafd90f
--- /dev/null
+++ b/src/tracking/util.go
@@ -0,0 +1,15 @@
+package tracking
+
+import (
+ "github.com/labstack/echo/v4"
+)
+
+func getFingerprint(c echo.Context) string {
+ // ip, userAgent acceptLangueage Header
+ ip := c.RealIP()
+ userAgent := c.Request().Header.Get("User-Agent")
+ acceptLanguage := c.Request().Header.Get("Accept-Language")
+
+ // Combine them into a fingerprint string
+ return ip + userAgent + acceptLanguage
+}
diff --git a/views/tracking.html b/views/tracking.html
new file mode 100644
index 0000000..78188df
--- /dev/null
+++ b/views/tracking.html
@@ -0,0 +1,161 @@
+{{block "tracking" .}}
+
+
+
+
+
+ Tracking Dashboard
+
+
+
+
+
+
+
+
+
+
+
Tracking Dashboard
+
+
+
+
{{.TotalVisits}}
+
Total Visits
+
+
+
{{.UniqueVisitors}}
+
Unique Visitors
+
+
+
{{.TodayVisits}}
+
Today's Visits
+
+
+
{{.ActiveSessions}}
+
Active Sessions
+
+
+
+
Recent Visits
+
+
+
+ | Timestamp |
+ IP Address |
+ Path |
+ User Agent |
+ Referrer |
+
+
+
+ {{range .RecentVisits}}
+
+ | {{.Timestamp}} |
+ {{.IPAddress}} |
+ {{.Path}} |
+ {{.UserAgent}} |
+ {{.Referrer}} |
+
+ {{else}}
+
+ |
+ No visits recorded yet
+ |
+
+ {{end}}
+
+
+
+
Top Pages
+
+
+
+ | Path |
+ Visits |
+ Percentage |
+
+
+
+ {{range .TopPages}}
+
+ | {{.Path}} |
+ {{.Count}} |
+ {{printf "%.2f" .Percentage}}% |
+
+ {{else}}
+
+ |
+ No page data available
+ |
+
+ {{end}}
+
+
+
+
+
+
+
+
+{{end}}