Added per-forum analytics.

Added one week as a time range option.

Moved routeForum to routes.ViewForum
Renamed form_vars to formVars.
Moved the analytics chart JS into it's own file.
Changed the header text on the requests chart from Views to Requests.
Added simple hooks to global.js
Moved some of the in funcname prints to debug mode.
De-duplicated some of the back-end analytics logic.
This commit is contained in:
Azareal 2018-02-22 02:27:17 +00:00
parent 60964868d4
commit 0879f56893
30 changed files with 706 additions and 536 deletions

View File

@ -8,7 +8,7 @@ import (
"../../query_gen/lib"
)
// TODO: The forum view counter
var ForumViewCounter *DefaultForumViewCounter
// TODO: Unload forum counters without any views over the past 15 minutes, if the admin has configured the forumstore with a cap and it's been hit?
// Forums can be reloaded from the database at any time, so we want to keep the counters separate from them
@ -29,7 +29,7 @@ func NewDefaultForumViewCounter() (*DefaultForumViewCounter, error) {
insert: acc.Insert("viewchunks_forums").Columns("count, createdAt, forum").Fields("?,UTC_TIMESTAMP(),?").Prepare(),
}
common.AddScheduledFifteenMinuteTask(counter.Tick) // There could be a lot of routes, so we don't want to be running this every second
//AddScheduledSecondTask(counter.Tick)
//common.AddScheduledSecondTask(counter.Tick)
common.AddShutdownTask(counter.Tick)
return counter, acc.FirstError()
}
@ -83,4 +83,36 @@ func (counter *DefaultForumViewCounter) insertChunk(count int, forum int) error
return err
}
func (counter *DefaultForumViewCounter) Bump(forumID int) {
// Is the ID even?
if forumID%2 == 0 {
counter.evenLock.RLock()
forum, ok := counter.evenMap[forumID]
counter.evenLock.RUnlock()
if ok {
forum.Lock()
forum.counter++
forum.Unlock()
} else {
counter.evenLock.Lock()
counter.evenMap[forumID] = &RWMutexCounterBucket{counter: 1}
counter.evenLock.Unlock()
}
return
}
counter.oddLock.RLock()
forum, ok := counter.oddMap[forumID]
counter.oddLock.RUnlock()
if ok {
forum.Lock()
forum.counter++
forum.Unlock()
} else {
counter.oddLock.Lock()
counter.oddMap[forumID] = &RWMutexCounterBucket{counter: 1}
counter.oddLock.Unlock()
}
}
// TODO: Add a forum counter backed by two maps which grow as forums are created but never shrinks

View File

@ -23,7 +23,6 @@ type Stmts struct {
getActivityEntry *sql.Stmt
forumEntryExists *sql.Stmt
groupEntryExists *sql.Stmt
getForumTopicsOffset *sql.Stmt
getAttachment *sql.Stmt
getForumTopics *sql.Stmt
getProfileReplies *sql.Stmt
@ -164,13 +163,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing getForumTopicsOffset statement.")
stmts.getForumTopicsOffset, err = db.Prepare("SELECT [tid],[title],[content],[createdBy],[is_closed],[sticky],[createdAt],[lastReplyAt],[lastReplyBy],[parentID],[postCount],[likeCount] FROM [topics] WHERE [parentID] = ?1 ORDER BY sticky DESC,lastReplyAt DESC,createdBy DESC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY")
if err != nil {
log.Print("Bad Query: ","SELECT [tid],[title],[content],[createdBy],[is_closed],[sticky],[createdAt],[lastReplyAt],[lastReplyBy],[parentID],[postCount],[likeCount] FROM [topics] WHERE [parentID] = ?1 ORDER BY sticky DESC,lastReplyAt DESC,createdBy DESC OFFSET ?2 ROWS FETCH NEXT ?3 ROWS ONLY")
return err
}
log.Print("Preparing getAttachment statement.")
stmts.getAttachment, err = db.Prepare("SELECT [sectionID],[sectionTable],[originID],[originTable],[uploadedBy],[path] FROM [attachments] WHERE [path] = ?1 AND [sectionID] = ?2 AND [sectionTable] = ?3")
if err != nil {

View File

@ -25,7 +25,6 @@ type Stmts struct {
getActivityEntry *sql.Stmt
forumEntryExists *sql.Stmt
groupEntryExists *sql.Stmt
getForumTopicsOffset *sql.Stmt
getAttachment *sql.Stmt
getForumTopics *sql.Stmt
getProfileReplies *sql.Stmt
@ -152,12 +151,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing getForumTopicsOffset statement.")
stmts.getForumTopicsOffset, err = db.Prepare("SELECT `tid`,`title`,`content`,`createdBy`,`is_closed`,`sticky`,`createdAt`,`lastReplyAt`,`lastReplyBy`,`parentID`,`postCount`,`likeCount` FROM `topics` WHERE `parentID` = ? ORDER BY sticky DESC,lastReplyAt DESC,createdBy DESC LIMIT ?,?")
if err != nil {
return err
}
log.Print("Preparing getAttachment statement.")
stmts.getAttachment, err = db.Prepare("SELECT `sectionID`,`sectionTable`,`originID`,`originTable`,`uploadedBy`,`path` FROM `attachments` WHERE `path` = ? AND `sectionID` = ? AND `sectionTable` = ?")
if err != nil {

View File

@ -21,7 +21,7 @@ var RouteMap = map[string]interface{}{
"routes.Overview": routes.Overview,
"routes.CustomPage": routes.CustomPage,
"routeForums": routeForums,
"routeForum": routeForum,
"routes.ViewForum": routes.ViewForum,
"routeChangeTheme": routeChangeTheme,
"routeShowAttachment": routeShowAttachment,
"routeWebsockets": routeWebsockets,
@ -61,10 +61,12 @@ var RouteMap = map[string]interface{}{
"routePanelAnalyticsReferrers": routePanelAnalyticsReferrers,
"routePanelAnalyticsRouteViews": routePanelAnalyticsRouteViews,
"routePanelAnalyticsAgentViews": routePanelAnalyticsAgentViews,
"routePanelAnalyticsForumViews": routePanelAnalyticsForumViews,
"routePanelAnalyticsSystemViews": routePanelAnalyticsSystemViews,
"routePanelAnalyticsReferrerViews": routePanelAnalyticsReferrerViews,
"routePanelAnalyticsPosts": routePanelAnalyticsPosts,
"routePanelAnalyticsTopics": routePanelAnalyticsTopics,
"routePanelAnalyticsForums": routePanelAnalyticsForums,
"routePanelGroups": routePanelGroups,
"routePanelGroupsEdit": routePanelGroupsEdit,
"routePanelGroupsEditPerms": routePanelGroupsEditPerms,
@ -124,7 +126,7 @@ var routeMapEnum = map[string]int{
"routes.Overview": 1,
"routes.CustomPage": 2,
"routeForums": 3,
"routeForum": 4,
"routes.ViewForum": 4,
"routeChangeTheme": 5,
"routeShowAttachment": 6,
"routeWebsockets": 7,
@ -164,68 +166,70 @@ var routeMapEnum = map[string]int{
"routePanelAnalyticsReferrers": 41,
"routePanelAnalyticsRouteViews": 42,
"routePanelAnalyticsAgentViews": 43,
"routePanelAnalyticsSystemViews": 44,
"routePanelAnalyticsReferrerViews": 45,
"routePanelAnalyticsPosts": 46,
"routePanelAnalyticsTopics": 47,
"routePanelGroups": 48,
"routePanelGroupsEdit": 49,
"routePanelGroupsEditPerms": 50,
"routePanelGroupsEditSubmit": 51,
"routePanelGroupsEditPermsSubmit": 52,
"routePanelGroupsCreateSubmit": 53,
"routePanelBackups": 54,
"routePanelLogsMod": 55,
"routePanelDebug": 56,
"routePanelDashboard": 57,
"routes.AccountEditCritical": 58,
"routeAccountEditCriticalSubmit": 59,
"routeAccountEditAvatar": 60,
"routeAccountEditAvatarSubmit": 61,
"routeAccountEditUsername": 62,
"routeAccountEditUsernameSubmit": 63,
"routeAccountEditEmail": 64,
"routeAccountEditEmailTokenSubmit": 65,
"routeProfile": 66,
"routes.BanUserSubmit": 67,
"routes.UnbanUser": 68,
"routes.ActivateUser": 69,
"routes.IPSearch": 70,
"routes.CreateTopicSubmit": 71,
"routes.EditTopicSubmit": 72,
"routes.DeleteTopicSubmit": 73,
"routes.StickTopicSubmit": 74,
"routes.UnstickTopicSubmit": 75,
"routes.LockTopicSubmit": 76,
"routes.UnlockTopicSubmit": 77,
"routes.MoveTopicSubmit": 78,
"routeLikeTopicSubmit": 79,
"routes.ViewTopic": 80,
"routeCreateReplySubmit": 81,
"routes.ReplyEditSubmit": 82,
"routes.ReplyDeleteSubmit": 83,
"routeReplyLikeSubmit": 84,
"routeProfileReplyCreateSubmit": 85,
"routes.ProfileReplyEditSubmit": 86,
"routes.ProfileReplyDeleteSubmit": 87,
"routes.PollVote": 88,
"routes.PollResults": 89,
"routes.AccountLogin": 90,
"routes.AccountRegister": 91,
"routeLogout": 92,
"routes.AccountLoginSubmit": 93,
"routes.AccountRegisterSubmit": 94,
"routeDynamic": 95,
"routeUploads": 96,
"routes.StaticFile": 97,
"BadRoute": 98,
"routePanelAnalyticsForumViews": 44,
"routePanelAnalyticsSystemViews": 45,
"routePanelAnalyticsReferrerViews": 46,
"routePanelAnalyticsPosts": 47,
"routePanelAnalyticsTopics": 48,
"routePanelAnalyticsForums": 49,
"routePanelGroups": 50,
"routePanelGroupsEdit": 51,
"routePanelGroupsEditPerms": 52,
"routePanelGroupsEditSubmit": 53,
"routePanelGroupsEditPermsSubmit": 54,
"routePanelGroupsCreateSubmit": 55,
"routePanelBackups": 56,
"routePanelLogsMod": 57,
"routePanelDebug": 58,
"routePanelDashboard": 59,
"routes.AccountEditCritical": 60,
"routeAccountEditCriticalSubmit": 61,
"routeAccountEditAvatar": 62,
"routeAccountEditAvatarSubmit": 63,
"routeAccountEditUsername": 64,
"routeAccountEditUsernameSubmit": 65,
"routeAccountEditEmail": 66,
"routeAccountEditEmailTokenSubmit": 67,
"routeProfile": 68,
"routes.BanUserSubmit": 69,
"routes.UnbanUser": 70,
"routes.ActivateUser": 71,
"routes.IPSearch": 72,
"routes.CreateTopicSubmit": 73,
"routes.EditTopicSubmit": 74,
"routes.DeleteTopicSubmit": 75,
"routes.StickTopicSubmit": 76,
"routes.UnstickTopicSubmit": 77,
"routes.LockTopicSubmit": 78,
"routes.UnlockTopicSubmit": 79,
"routes.MoveTopicSubmit": 80,
"routeLikeTopicSubmit": 81,
"routes.ViewTopic": 82,
"routeCreateReplySubmit": 83,
"routes.ReplyEditSubmit": 84,
"routes.ReplyDeleteSubmit": 85,
"routeReplyLikeSubmit": 86,
"routeProfileReplyCreateSubmit": 87,
"routes.ProfileReplyEditSubmit": 88,
"routes.ProfileReplyDeleteSubmit": 89,
"routes.PollVote": 90,
"routes.PollResults": 91,
"routes.AccountLogin": 92,
"routes.AccountRegister": 93,
"routeLogout": 94,
"routes.AccountLoginSubmit": 95,
"routes.AccountRegisterSubmit": 96,
"routeDynamic": 97,
"routeUploads": 98,
"routes.StaticFile": 99,
"BadRoute": 100,
}
var reverseRouteMapEnum = map[int]string{
0: "routeAPI",
1: "routes.Overview",
2: "routes.CustomPage",
3: "routeForums",
4: "routeForum",
4: "routes.ViewForum",
5: "routeChangeTheme",
6: "routeShowAttachment",
7: "routeWebsockets",
@ -265,61 +269,63 @@ var reverseRouteMapEnum = map[int]string{
41: "routePanelAnalyticsReferrers",
42: "routePanelAnalyticsRouteViews",
43: "routePanelAnalyticsAgentViews",
44: "routePanelAnalyticsSystemViews",
45: "routePanelAnalyticsReferrerViews",
46: "routePanelAnalyticsPosts",
47: "routePanelAnalyticsTopics",
48: "routePanelGroups",
49: "routePanelGroupsEdit",
50: "routePanelGroupsEditPerms",
51: "routePanelGroupsEditSubmit",
52: "routePanelGroupsEditPermsSubmit",
53: "routePanelGroupsCreateSubmit",
54: "routePanelBackups",
55: "routePanelLogsMod",
56: "routePanelDebug",
57: "routePanelDashboard",
58: "routes.AccountEditCritical",
59: "routeAccountEditCriticalSubmit",
60: "routeAccountEditAvatar",
61: "routeAccountEditAvatarSubmit",
62: "routeAccountEditUsername",
63: "routeAccountEditUsernameSubmit",
64: "routeAccountEditEmail",
65: "routeAccountEditEmailTokenSubmit",
66: "routeProfile",
67: "routes.BanUserSubmit",
68: "routes.UnbanUser",
69: "routes.ActivateUser",
70: "routes.IPSearch",
71: "routes.CreateTopicSubmit",
72: "routes.EditTopicSubmit",
73: "routes.DeleteTopicSubmit",
74: "routes.StickTopicSubmit",
75: "routes.UnstickTopicSubmit",
76: "routes.LockTopicSubmit",
77: "routes.UnlockTopicSubmit",
78: "routes.MoveTopicSubmit",
79: "routeLikeTopicSubmit",
80: "routes.ViewTopic",
81: "routeCreateReplySubmit",
82: "routes.ReplyEditSubmit",
83: "routes.ReplyDeleteSubmit",
84: "routeReplyLikeSubmit",
85: "routeProfileReplyCreateSubmit",
86: "routes.ProfileReplyEditSubmit",
87: "routes.ProfileReplyDeleteSubmit",
88: "routes.PollVote",
89: "routes.PollResults",
90: "routes.AccountLogin",
91: "routes.AccountRegister",
92: "routeLogout",
93: "routes.AccountLoginSubmit",
94: "routes.AccountRegisterSubmit",
95: "routeDynamic",
96: "routeUploads",
97: "routes.StaticFile",
98: "BadRoute",
44: "routePanelAnalyticsForumViews",
45: "routePanelAnalyticsSystemViews",
46: "routePanelAnalyticsReferrerViews",
47: "routePanelAnalyticsPosts",
48: "routePanelAnalyticsTopics",
49: "routePanelAnalyticsForums",
50: "routePanelGroups",
51: "routePanelGroupsEdit",
52: "routePanelGroupsEditPerms",
53: "routePanelGroupsEditSubmit",
54: "routePanelGroupsEditPermsSubmit",
55: "routePanelGroupsCreateSubmit",
56: "routePanelBackups",
57: "routePanelLogsMod",
58: "routePanelDebug",
59: "routePanelDashboard",
60: "routes.AccountEditCritical",
61: "routeAccountEditCriticalSubmit",
62: "routeAccountEditAvatar",
63: "routeAccountEditAvatarSubmit",
64: "routeAccountEditUsername",
65: "routeAccountEditUsernameSubmit",
66: "routeAccountEditEmail",
67: "routeAccountEditEmailTokenSubmit",
68: "routeProfile",
69: "routes.BanUserSubmit",
70: "routes.UnbanUser",
71: "routes.ActivateUser",
72: "routes.IPSearch",
73: "routes.CreateTopicSubmit",
74: "routes.EditTopicSubmit",
75: "routes.DeleteTopicSubmit",
76: "routes.StickTopicSubmit",
77: "routes.UnstickTopicSubmit",
78: "routes.LockTopicSubmit",
79: "routes.UnlockTopicSubmit",
80: "routes.MoveTopicSubmit",
81: "routeLikeTopicSubmit",
82: "routes.ViewTopic",
83: "routeCreateReplySubmit",
84: "routes.ReplyEditSubmit",
85: "routes.ReplyDeleteSubmit",
86: "routeReplyLikeSubmit",
87: "routeProfileReplyCreateSubmit",
88: "routes.ProfileReplyEditSubmit",
89: "routes.ProfileReplyDeleteSubmit",
90: "routes.PollVote",
91: "routes.PollResults",
92: "routes.AccountLogin",
93: "routes.AccountRegister",
94: "routeLogout",
95: "routes.AccountLoginSubmit",
96: "routes.AccountRegisterSubmit",
97: "routeDynamic",
98: "routeUploads",
99: "routes.StaticFile",
100: "BadRoute",
}
var osMapEnum = map[string]int{
"unknown": 0,
@ -571,7 +577,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
counters.GlobalViewCounter.Bump()
if prefix == "/static" {
counters.RouteViewCounter.Bump(97)
counters.RouteViewCounter.Bump(99)
req.URL.Path += extraData
routes.StaticFile(w, req)
return
@ -745,7 +751,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
case "/forum":
counters.RouteViewCounter.Bump(4)
err = routeForum(w,req,user,extraData)
err = routes.ViewForum(w,req,user,extraData)
if err != nil {
router.handleError(err,w,req,user)
}
@ -1052,11 +1058,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/panel/analytics/agent/":
counters.RouteViewCounter.Bump(43)
err = routePanelAnalyticsAgentViews(w,req,user,extraData)
case "/panel/analytics/system/":
case "/panel/analytics/forum/":
counters.RouteViewCounter.Bump(44)
err = routePanelAnalyticsForumViews(w,req,user,extraData)
case "/panel/analytics/system/":
counters.RouteViewCounter.Bump(45)
err = routePanelAnalyticsSystemViews(w,req,user,extraData)
case "/panel/analytics/referrer/":
counters.RouteViewCounter.Bump(45)
counters.RouteViewCounter.Bump(46)
err = routePanelAnalyticsReferrerViews(w,req,user,extraData)
case "/panel/analytics/posts/":
err = common.ParseForm(w,req,user)
@ -1065,7 +1074,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(46)
counters.RouteViewCounter.Bump(47)
err = routePanelAnalyticsPosts(w,req,user)
case "/panel/analytics/topics/":
err = common.ParseForm(w,req,user)
@ -1074,16 +1083,25 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(47)
err = routePanelAnalyticsTopics(w,req,user)
case "/panel/groups/":
counters.RouteViewCounter.Bump(48)
err = routePanelAnalyticsTopics(w,req,user)
case "/panel/analytics/forums/":
err = common.ParseForm(w,req,user)
if err != nil {
router.handleError(err,w,req,user)
return
}
counters.RouteViewCounter.Bump(49)
err = routePanelAnalyticsForums(w,req,user)
case "/panel/groups/":
counters.RouteViewCounter.Bump(50)
err = routePanelGroups(w,req,user)
case "/panel/groups/edit/":
counters.RouteViewCounter.Bump(49)
counters.RouteViewCounter.Bump(51)
err = routePanelGroupsEdit(w,req,user,extraData)
case "/panel/groups/edit/perms/":
counters.RouteViewCounter.Bump(50)
counters.RouteViewCounter.Bump(52)
err = routePanelGroupsEditPerms(w,req,user,extraData)
case "/panel/groups/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1092,7 +1110,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(51)
counters.RouteViewCounter.Bump(53)
err = routePanelGroupsEditSubmit(w,req,user,extraData)
case "/panel/groups/edit/perms/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1101,7 +1119,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(52)
counters.RouteViewCounter.Bump(54)
err = routePanelGroupsEditPermsSubmit(w,req,user,extraData)
case "/panel/groups/create/":
err = common.NoSessionMismatch(w,req,user)
@ -1110,7 +1128,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(53)
counters.RouteViewCounter.Bump(55)
err = routePanelGroupsCreateSubmit(w,req,user)
case "/panel/backups/":
err = common.SuperAdminOnly(w,req,user)
@ -1119,10 +1137,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(54)
counters.RouteViewCounter.Bump(56)
err = routePanelBackups(w,req,user,extraData)
case "/panel/logs/mod/":
counters.RouteViewCounter.Bump(55)
counters.RouteViewCounter.Bump(57)
err = routePanelLogsMod(w,req,user)
case "/panel/debug/":
err = common.AdminOnly(w,req,user)
@ -1131,10 +1149,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(56)
counters.RouteViewCounter.Bump(58)
err = routePanelDebug(w,req,user)
default:
counters.RouteViewCounter.Bump(57)
counters.RouteViewCounter.Bump(59)
err = routePanelDashboard(w,req,user)
}
if err != nil {
@ -1149,7 +1167,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(58)
counters.RouteViewCounter.Bump(60)
err = routes.AccountEditCritical(w,req,user)
case "/user/edit/critical/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1164,7 +1182,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(59)
counters.RouteViewCounter.Bump(61)
err = routeAccountEditCriticalSubmit(w,req,user)
case "/user/edit/avatar/":
err = common.MemberOnly(w,req,user)
@ -1173,7 +1191,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(60)
counters.RouteViewCounter.Bump(62)
err = routeAccountEditAvatar(w,req,user)
case "/user/edit/avatar/submit/":
err = common.MemberOnly(w,req,user)
@ -1193,7 +1211,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(61)
counters.RouteViewCounter.Bump(63)
err = routeAccountEditAvatarSubmit(w,req,user)
case "/user/edit/username/":
err = common.MemberOnly(w,req,user)
@ -1202,7 +1220,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(62)
counters.RouteViewCounter.Bump(64)
err = routeAccountEditUsername(w,req,user)
case "/user/edit/username/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1217,7 +1235,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(63)
counters.RouteViewCounter.Bump(65)
err = routeAccountEditUsernameSubmit(w,req,user)
case "/user/edit/email/":
err = common.MemberOnly(w,req,user)
@ -1226,7 +1244,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(64)
counters.RouteViewCounter.Bump(66)
err = routeAccountEditEmail(w,req,user)
case "/user/edit/token/":
err = common.NoSessionMismatch(w,req,user)
@ -1241,11 +1259,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(65)
counters.RouteViewCounter.Bump(67)
err = routeAccountEditEmailTokenSubmit(w,req,user,extraData)
default:
req.URL.Path += extraData
counters.RouteViewCounter.Bump(66)
counters.RouteViewCounter.Bump(68)
err = routeProfile(w,req,user)
}
if err != nil {
@ -1266,7 +1284,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(67)
counters.RouteViewCounter.Bump(69)
err = routes.BanUserSubmit(w,req,user,extraData)
case "/users/unban/":
err = common.NoSessionMismatch(w,req,user)
@ -1281,7 +1299,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(68)
counters.RouteViewCounter.Bump(70)
err = routes.UnbanUser(w,req,user,extraData)
case "/users/activate/":
err = common.NoSessionMismatch(w,req,user)
@ -1296,7 +1314,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(69)
counters.RouteViewCounter.Bump(71)
err = routes.ActivateUser(w,req,user,extraData)
case "/users/ips/":
err = common.MemberOnly(w,req,user)
@ -1305,7 +1323,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(70)
counters.RouteViewCounter.Bump(72)
err = routes.IPSearch(w,req,user)
}
if err != nil {
@ -1331,7 +1349,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(71)
counters.RouteViewCounter.Bump(73)
err = routes.CreateTopicSubmit(w,req,user)
case "/topic/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1346,7 +1364,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(72)
counters.RouteViewCounter.Bump(74)
err = routes.EditTopicSubmit(w,req,user,extraData)
case "/topic/delete/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1362,7 +1380,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
req.URL.Path += extraData
counters.RouteViewCounter.Bump(73)
counters.RouteViewCounter.Bump(75)
err = routes.DeleteTopicSubmit(w,req,user)
case "/topic/stick/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1377,7 +1395,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(74)
counters.RouteViewCounter.Bump(76)
err = routes.StickTopicSubmit(w,req,user,extraData)
case "/topic/unstick/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1392,7 +1410,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(75)
counters.RouteViewCounter.Bump(77)
err = routes.UnstickTopicSubmit(w,req,user,extraData)
case "/topic/lock/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1408,7 +1426,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
req.URL.Path += extraData
counters.RouteViewCounter.Bump(76)
counters.RouteViewCounter.Bump(78)
err = routes.LockTopicSubmit(w,req,user)
case "/topic/unlock/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1423,7 +1441,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(77)
counters.RouteViewCounter.Bump(79)
err = routes.UnlockTopicSubmit(w,req,user,extraData)
case "/topic/move/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1438,7 +1456,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(78)
counters.RouteViewCounter.Bump(80)
err = routes.MoveTopicSubmit(w,req,user,extraData)
case "/topic/like/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1453,10 +1471,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(79)
counters.RouteViewCounter.Bump(81)
err = routeLikeTopicSubmit(w,req,user,extraData)
default:
counters.RouteViewCounter.Bump(80)
counters.RouteViewCounter.Bump(82)
err = routes.ViewTopic(w,req,user, extraData)
}
if err != nil {
@ -1482,7 +1500,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(81)
counters.RouteViewCounter.Bump(83)
err = routeCreateReplySubmit(w,req,user)
case "/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1497,7 +1515,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(82)
counters.RouteViewCounter.Bump(84)
err = routes.ReplyEditSubmit(w,req,user,extraData)
case "/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1512,7 +1530,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(83)
counters.RouteViewCounter.Bump(85)
err = routes.ReplyDeleteSubmit(w,req,user,extraData)
case "/reply/like/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1527,7 +1545,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(84)
counters.RouteViewCounter.Bump(86)
err = routeReplyLikeSubmit(w,req,user,extraData)
}
if err != nil {
@ -1548,7 +1566,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(85)
counters.RouteViewCounter.Bump(87)
err = routeProfileReplyCreateSubmit(w,req,user)
case "/profile/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1563,7 +1581,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(86)
counters.RouteViewCounter.Bump(88)
err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
case "/profile/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -1578,7 +1596,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(87)
counters.RouteViewCounter.Bump(89)
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
}
if err != nil {
@ -1599,10 +1617,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(88)
counters.RouteViewCounter.Bump(90)
err = routes.PollVote(w,req,user,extraData)
case "/poll/results/":
counters.RouteViewCounter.Bump(89)
counters.RouteViewCounter.Bump(91)
err = routes.PollResults(w,req,user,extraData)
}
if err != nil {
@ -1611,10 +1629,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/accounts":
switch(req.URL.Path) {
case "/accounts/login/":
counters.RouteViewCounter.Bump(90)
counters.RouteViewCounter.Bump(92)
err = routes.AccountLogin(w,req,user)
case "/accounts/create/":
counters.RouteViewCounter.Bump(91)
counters.RouteViewCounter.Bump(93)
err = routes.AccountRegister(w,req,user)
case "/accounts/logout/":
err = common.NoSessionMismatch(w,req,user)
@ -1629,7 +1647,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(92)
counters.RouteViewCounter.Bump(94)
err = routeLogout(w,req,user)
case "/accounts/login/submit/":
err = common.ParseForm(w,req,user)
@ -1638,7 +1656,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(93)
counters.RouteViewCounter.Bump(95)
err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/create/submit/":
err = common.ParseForm(w,req,user)
@ -1647,7 +1665,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
counters.RouteViewCounter.Bump(94)
counters.RouteViewCounter.Bump(96)
err = routes.AccountRegisterSubmit(w,req,user)
}
if err != nil {
@ -1664,7 +1682,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
common.NotFound(w,req,nil)
return
}
counters.RouteViewCounter.Bump(96)
counters.RouteViewCounter.Bump(98)
req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this?
router.UploadHandler(w,req) // TODO: Count these views
@ -1707,7 +1725,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
router.RUnlock()
if ok {
counters.RouteViewCounter.Bump(95) // TODO: Be more specific about *which* dynamic route it is
counters.RouteViewCounter.Bump(97) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData
err = handle(w,req,user)
if err != nil {
@ -1721,7 +1739,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if strings.Contains(lowerPath,"admin") || strings.Contains(lowerPath,"sql") || strings.Contains(lowerPath,"manage") || strings.Contains(lowerPath,"//") || strings.Contains(lowerPath,"\\\\") || strings.Contains(lowerPath,"wp") || strings.Contains(lowerPath,"wordpress") || strings.Contains(lowerPath,"config") || strings.Contains(lowerPath,"setup") || strings.Contains(lowerPath,"install") || strings.Contains(lowerPath,"update") || strings.Contains(lowerPath,"php") {
router.SuspiciousRequest(req)
}
counters.RouteViewCounter.Bump(98)
counters.RouteViewCounter.Bump(100)
common.NotFound(w,req,nil)
}
}

View File

@ -130,6 +130,10 @@ func afterDBInit() (err error) {
if err != nil {
return err
}
counters.ForumViewCounter, err = counters.NewDefaultForumViewCounter()
if err != nil {
return err
}
counters.ReferrerTracker, err = counters.NewDefaultReferrerTracker()
if err != nil {
return err

View File

@ -575,6 +575,12 @@ func panelAnalyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange,
timeRange.Slices = 30
timeRange.SliceWidth = 60 * 60 * 24
timeRange.Range = "one-month"
case "one-week":
timeRange.Quantity = 7
timeRange.Unit = "day"
timeRange.Slices = 14
timeRange.SliceWidth = 60 * 60 * 12
timeRange.Range = "one-week"
case "two-days": // Two days is experimental
timeRange.Quantity = 2
timeRange.Unit = "day"
@ -599,24 +605,9 @@ func panelAnalyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange,
return timeRange, nil
}
func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
var revLabelList []int64
var labelList []int64
var viewMap = make(map[int64]int64)
func panelAnalyticsTimeRangeToLabelList(timeRange AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) {
viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {
var label = currentTime - int64(i*timeRange.SliceWidth)
revLabelList = append(revLabelList, label)
@ -625,9 +616,26 @@ func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user commo
for _, value := range revLabelList {
labelList = append(labelList, value)
}
return revLabelList, labelList, viewMap
}
func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
headerVars.Scripts = append(headerVars.Scripts, "analytics.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange)
var viewList []int64
log.Print("in routePanelAnalyticsViews")
common.DebugLog("in routePanelAnalyticsViews")
acc := qgen.Builder.Accumulator()
rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
@ -679,28 +687,16 @@ func routePanelAnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
headerVars.Scripts = append(headerVars.Scripts, "analytics.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
var revLabelList []int64
var labelList []int64
var viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {
var label = currentTime - int64(i*timeRange.SliceWidth)
revLabelList = append(revLabelList, label)
viewMap[label] = 0
}
for _, value := range revLabelList {
labelList = append(labelList, value)
}
revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange)
var viewList []int64
log.Print("in routePanelAnalyticsRouteViews")
common.DebugLog("in routePanelAnalyticsRouteViews")
acc := qgen.Builder.Accumulator()
// TODO: Validate the route is valid
@ -753,28 +749,16 @@ func routePanelAnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
headerVars.Scripts = append(headerVars.Scripts, "analytics.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
var revLabelList []int64
var labelList []int64
var viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {
var label = currentTime - int64(i*timeRange.SliceWidth)
revLabelList = append(revLabelList, label)
viewMap[label] = 0
}
for _, value := range revLabelList {
labelList = append(labelList, value)
}
revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange)
var viewList []int64
log.Print("in routePanelAnalyticsAgentViews")
common.DebugLog("in routePanelAnalyticsAgentViews")
acc := qgen.Builder.Accumulator()
// TODO: Verify the agent is valid
@ -829,6 +813,79 @@ func routePanelAnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user
return panelRenderTemplate("panel_analytics_agent_views", w, r, user, &pi)
}
func routePanelAnalyticsForumViews(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError {
headerVars, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
headerVars.Scripts = append(headerVars.Scripts, "analytics.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange)
var viewList []int64
common.DebugLog("in routePanelAnalyticsForumViews")
acc := qgen.Builder.Accumulator()
// TODO: Verify the agent is valid
rows, err := acc.Select("viewchunks_forums").Columns("count, createdAt").Where("forum = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(sfid)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
}
defer rows.Close()
for rows.Next() {
var count int64
var createdAt time.Time
err := rows.Scan(&count, &createdAt)
if err != nil {
return common.InternalError(err, w, r)
}
var unixCreatedAt = createdAt.Unix()
// TODO: Bulk log this
if common.Dev.SuperDebug {
log.Print("count: ", count)
log.Print("createdAt: ", createdAt)
log.Print("unixCreatedAt: ", unixCreatedAt)
}
for _, value := range labelList {
if unixCreatedAt > value {
viewMap[value] += count
break
}
}
}
err = rows.Err()
if err != nil {
return common.InternalError(err, w, r)
}
for _, value := range revLabelList {
viewList = append(viewList, viewMap[value])
}
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
log.Printf("graph: %+v\n", graph)
fid, err := strconv.Atoi(sfid)
if err != nil {
return common.InternalError(err, w, r)
}
forum, err := common.Forums.Get(fid)
if err != nil {
return common.InternalError(err, w, r)
}
pi := common.PanelAnalyticsAgentPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", sfid, forum.Name, graph, timeRange.Range}
return panelRenderTemplate("panel_analytics_forum_views", w, r, user, &pi)
}
func routePanelAnalyticsSystemViews(w http.ResponseWriter, r *http.Request, user common.User, system string) common.RouteError {
headerVars, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
@ -836,28 +893,16 @@ func routePanelAnalyticsSystemViews(w http.ResponseWriter, r *http.Request, user
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
headerVars.Scripts = append(headerVars.Scripts, "analytics.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
var revLabelList []int64
var labelList []int64
var viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {
var label = currentTime - int64(i*timeRange.SliceWidth)
revLabelList = append(revLabelList, label)
viewMap[label] = 0
}
for _, value := range revLabelList {
labelList = append(labelList, value)
}
revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange)
var viewList []int64
log.Print("in routePanelAnalyticsSystemViews")
common.DebugLog("in routePanelAnalyticsSystemViews")
acc := qgen.Builder.Accumulator()
// TODO: Verify the agent is valid
@ -918,28 +963,16 @@ func routePanelAnalyticsReferrerViews(w http.ResponseWriter, r *http.Request, us
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
headerVars.Scripts = append(headerVars.Scripts, "analytics.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
var revLabelList []int64
var labelList []int64
var viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {
var label = currentTime - int64(i*timeRange.SliceWidth)
revLabelList = append(revLabelList, label)
viewMap[label] = 0
}
for _, value := range revLabelList {
labelList = append(labelList, value)
}
revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange)
var viewList []int64
log.Print("in routePanelAnalyticsReferrerViews")
common.DebugLog("in routePanelAnalyticsReferrerViews")
acc := qgen.Builder.Accumulator()
// TODO: Verify the agent is valid
@ -994,28 +1027,16 @@ func routePanelAnalyticsTopics(w http.ResponseWriter, r *http.Request, user comm
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
headerVars.Scripts = append(headerVars.Scripts, "analytics.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
var revLabelList []int64
var labelList []int64
var viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {
var label = currentTime - int64(i*timeRange.SliceWidth)
revLabelList = append(revLabelList, label)
viewMap[label] = 0
}
for _, value := range revLabelList {
labelList = append(labelList, value)
}
revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange)
var viewList []int64
log.Print("in routePanelAnalyticsTopics")
common.DebugLog("in routePanelAnalyticsTopics")
acc := qgen.Builder.Accumulator()
rows, err := acc.Select("topicchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
@ -1071,28 +1092,16 @@ func routePanelAnalyticsPosts(w http.ResponseWriter, r *http.Request, user commo
}
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
headerVars.Scripts = append(headerVars.Scripts, "analytics.js")
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
var revLabelList []int64
var labelList []int64
var viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {
var label = currentTime - int64(i*timeRange.SliceWidth)
revLabelList = append(revLabelList, label)
viewMap[label] = 0
}
for _, value := range revLabelList {
labelList = append(labelList, value)
}
revLabelList, labelList, viewMap := panelAnalyticsTimeRangeToLabelList(timeRange)
var viewList []int64
log.Print("in routePanelAnalyticsPosts")
common.DebugLog("in routePanelAnalyticsPosts")
acc := qgen.Builder.Accumulator()
rows, err := acc.Select("postchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
@ -1140,6 +1149,67 @@ func routePanelAnalyticsPosts(w http.ResponseWriter, r *http.Request, user commo
return panelRenderTemplate("panel_analytics_posts", w, r, user, &pi)
}
func routePanelAnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
var forumMap = make(map[string]int)
timeRange, err := panelAnalyticsTimeRange(r.FormValue("timeRange"))
if err != nil {
return common.LocalError(err.Error(), w, r, user)
}
acc := qgen.Builder.Accumulator()
rows, err := acc.Select("viewchunks_forums").Columns("count, forum").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
}
defer rows.Close()
for rows.Next() {
var count int
var forum string
err := rows.Scan(&count, &forum)
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Bulk log this
if common.Dev.SuperDebug {
log.Print("count: ", count)
log.Print("forum: ", forum)
}
forumMap[forum] += count
}
err = rows.Err()
if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Sort this slice
var forumItems []common.PanelAnalyticsAgentsItem
for sfid, count := range forumMap {
fid, err := strconv.Atoi(sfid)
if err != nil {
return common.InternalError(err, w, r)
}
forum, err := common.Forums.Get(fid)
if err != nil {
return common.InternalError(err, w, r)
}
forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{
Agent: sfid,
FriendlyAgent: forum.Name,
Count: count,
})
}
pi := common.PanelAnalyticsAgentsPage{common.GetTitlePhrase("panel_analytics"), user, headerVars, stats, "analytics", forumItems, timeRange.Range}
return panelRenderTemplate("panel_analytics_forums", w, r, user, &pi)
}
func routePanelAnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {

39
public/analytics.js Normal file
View File

@ -0,0 +1,39 @@
/*addHook(() => {
})*/
function buildStatsChart(rawLabels, seriesData, timeRange) {
let labels = [];
if(timeRange=="one-month") {
labels = ["today","01 days"];
for(let i = 2; i < 30; i++) {
let label = "0" + i + " days";
if(label.length > "01 days".length) label = label.substr(1);
labels.push(label);
}
} else if(timeRange=="one-week") {
labels = ["today"];
for(let i = 2; i < 14; i++) {
if (i%2==0) labels.push("");
else labels.push(Math.floor(i/2) + " days");
}
} else {
for(const i in rawLabels) {
let date = new Date(rawLabels[i]*1000);
console.log("date: ", date);
let minutes = "0" + date.getMinutes();
let label = date.getHours() + ":" + minutes.substr(-2);
console.log("label:", label);
labels.push(label);
}
}
labels = labels.reverse()
seriesData = seriesData.reverse();
Chartist.Line('.ct_chart', {
labels: labels,
series: [seriesData],
}, {
height: '250px',
});
}

View File

@ -1,14 +1,27 @@
'use strict';
var form_vars = {};
var formVars = {};
var alertList = [];
var alertCount = 0;
var conn;
var selectedTopics = [];
var attachItemCallback = function(){}
var hooks = {
"start_init": [],
"end_init": [],
};
// Topic move
var forumToMoveTo = 0;
function runHook(name, ...args) {
if(!(name in hooks)) return;
let hook = hooks[name];
for (const callback in hook) {
callback(...args);
}
}
// TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts
function ajaxError(xhr,status,errstr) {
console.log("The AJAX request failed");
@ -203,6 +216,7 @@ function runWebSockets() {
}
$(document).ready(function(){
runHook("start_init");
if(window["WebSocket"]) runWebSockets();
else conn = false;
@ -311,7 +325,7 @@ $(document).ready(function(){
var fieldType = this.getAttribute("data-type");
if(fieldType=="list") {
var fieldValue = this.getAttribute("data-value");
if(fieldName in form_vars) var it = form_vars[fieldName];
if(fieldName in formVars) var it = formVars[fieldName];
else var it = ['No','Yes'];
var itLen = it.length;
var out = "";
@ -624,4 +638,6 @@ $(document).ready(function(){
});
})
});
runHook("end_init");
});

View File

@ -247,8 +247,6 @@ func writeSelects(adapter qgen.Adapter) error {
build.Select("groupEntryExists").Table("users_groups").Columns("gid").Where("name = ''").Orderby("gid ASC").Limit("0,1").Parse()
build.Select("getForumTopicsOffset").Table("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Parse()
build.Select("getAttachment").Table("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Where("path = ? AND sectionID = ? AND sectionTable = ?").Parse()
return nil

View File

@ -6,7 +6,7 @@ func routes() {
addRoute(View("routes.Overview", "/overview/"))
addRoute(View("routes.CustomPage", "/pages/", "extraData"))
addRoute(View("routeForums", "/forums/" /*,"&forums"*/))
addRoute(View("routeForum", "/forum/", "extraData"))
addRoute(View("routes.ViewForum", "/forum/", "extraData"))
addRoute(AnonAction("routeChangeTheme", "/theme/"))
addRoute(
View("routeShowAttachment", "/attachs/", "extraData").Before("ParseForm"),
@ -170,10 +170,12 @@ func buildPanelRoutes() {
View("routePanelAnalyticsReferrers", "/panel/analytics/referrers/").Before("ParseForm"),
View("routePanelAnalyticsRouteViews", "/panel/analytics/route/", "extraData"),
View("routePanelAnalyticsAgentViews", "/panel/analytics/agent/", "extraData"),
View("routePanelAnalyticsForumViews", "/panel/analytics/forum/", "extraData"),
View("routePanelAnalyticsSystemViews", "/panel/analytics/system/", "extraData"),
View("routePanelAnalyticsReferrerViews", "/panel/analytics/referrer/", "extraData"),
View("routePanelAnalyticsPosts", "/panel/analytics/posts/").Before("ParseForm"),
View("routePanelAnalyticsTopics", "/panel/analytics/topics/").Before("ParseForm"),
View("routePanelAnalyticsForums", "/panel/analytics/forums/").Before("ParseForm"),
View("routePanelGroups", "/panel/groups/"),
View("routePanelGroupsEdit", "/panel/groups/edit/", "extraData"),

View File

@ -44,102 +44,6 @@ func routeUploads() {
func BadRoute() {
}
func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError {
page, _ := strconv.Atoi(r.FormValue("page"))
// SEO URLs...
halves := strings.Split(sfid, ".")
if len(halves) < 2 {
halves = append(halves, halves[0])
}
fid, err := strconv.Atoi(halves[1])
if err != nil {
return common.PreError("The provided ForumID is not a valid number.", w, r)
}
headerVars, ferr := common.ForumUserCheck(w, r, &user, fid)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic {
return common.NoPermissions(w, r, user)
}
headerVars.Zone = "view_forum"
// TODO: Fix this double-check
forum, err := common.Forums.Get(fid)
if err == ErrNoRows {
return common.NotFound(w, r, headerVars)
} else if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete
offset, page, lastPage := common.PageOffset(forum.TopicCount, page, common.Config.ItemsPerPage)
// TODO: Move this to *Forum
rows, err := stmts.getForumTopicsOffset.Query(fid, offset, common.Config.ItemsPerPage)
if err != nil {
return common.InternalError(err, w, r)
}
defer rows.Close()
// TODO: Use something other than TopicsRow as we don't need to store the forum name and link on each and every topic item?
var topicList []*common.TopicsRow
var reqUserList = make(map[int]bool)
for rows.Next() {
var topicItem = common.TopicsRow{ID: 0}
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.PostCount, &topicItem.LikeCount)
if err != nil {
return common.InternalError(err, w, r)
}
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
common.RunVhook("forum_trow_assign", &topicItem, &forum)
topicList = append(topicList, &topicItem)
reqUserList[topicItem.CreatedBy] = true
reqUserList[topicItem.LastReplyBy] = true
}
err = rows.Err()
if err != nil {
return common.InternalError(err, w, r)
}
// Convert the user ID map to a slice, then bulk load the users
var idSlice = make([]int, len(reqUserList))
var i int
for userID := range reqUserList {
idSlice[i] = userID
i++
}
// TODO: What if a user is deleted via the Control Panel?
userList, err := common.Users.BulkGetMap(idSlice)
if err != nil {
return common.InternalError(err, w, r)
}
// Second pass to the add the user data
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
for _, topicItem := range topicList {
topicItem.Creator = userList[topicItem.CreatedBy]
topicItem.LastUser = userList[topicItem.LastReplyBy]
}
pageList := common.Paginate(forum.TopicCount, common.Config.ItemsPerPage, 5)
pi := common.ForumPage{forum.Name, user, headerVars, topicList, forum, pageList, page, lastPage}
if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) {
return nil
}
err = common.RunThemeTemplate(headerVars.Theme.Name, "forum", pi, w)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func routeForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
headerVars, ferr := common.UserCheck(w, r, &user)
if ferr != nil {

125
routes/forum.go Normal file
View File

@ -0,0 +1,125 @@
package routes
import (
"database/sql"
"net/http"
"strconv"
"strings"
"../common"
"../common/counters"
"../query_gen/lib"
)
type ForumStmts struct {
getTopics *sql.Stmt
}
var forumStmts ForumStmts
// TODO: Move these DbInits into *Forum as Topics()
func init() {
common.DbInits.Add(func(acc *qgen.Accumulator) error {
forumStmts = ForumStmts{
getTopics: acc.Select("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Prepare(),
}
return acc.FirstError()
})
}
func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError {
page, _ := strconv.Atoi(r.FormValue("page"))
// SEO URLs...
halves := strings.Split(sfid, ".")
if len(halves) < 2 {
halves = append(halves, halves[0])
}
fid, err := strconv.Atoi(halves[1])
if err != nil {
return common.PreError("The provided ForumID is not a valid number.", w, r)
}
headerVars, ferr := common.ForumUserCheck(w, r, &user, fid)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic {
return common.NoPermissions(w, r, user)
}
headerVars.Zone = "view_forum"
// TODO: Fix this double-check
forum, err := common.Forums.Get(fid)
if err == sql.ErrNoRows {
return common.NotFound(w, r, headerVars)
} else if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete
offset, page, lastPage := common.PageOffset(forum.TopicCount, page, common.Config.ItemsPerPage)
// TODO: Move this to *Forum
rows, err := forumStmts.getTopics.Query(fid, offset, common.Config.ItemsPerPage)
if err != nil {
return common.InternalError(err, w, r)
}
defer rows.Close()
// TODO: Use something other than TopicsRow as we don't need to store the forum name and link on each and every topic item?
var topicList []*common.TopicsRow
var reqUserList = make(map[int]bool)
for rows.Next() {
var topicItem = common.TopicsRow{ID: 0}
err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.PostCount, &topicItem.LikeCount)
if err != nil {
return common.InternalError(err, w, r)
}
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
common.RunVhook("forum_trow_assign", &topicItem, &forum)
topicList = append(topicList, &topicItem)
reqUserList[topicItem.CreatedBy] = true
reqUserList[topicItem.LastReplyBy] = true
}
err = rows.Err()
if err != nil {
return common.InternalError(err, w, r)
}
// Convert the user ID map to a slice, then bulk load the users
var idSlice = make([]int, len(reqUserList))
var i int
for userID := range reqUserList {
idSlice[i] = userID
i++
}
// TODO: What if a user is deleted via the Control Panel?
userList, err := common.Users.BulkGetMap(idSlice)
if err != nil {
return common.InternalError(err, w, r)
}
// Second pass to the add the user data
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
for _, topicItem := range topicList {
topicItem.Creator = userList[topicItem.CreatedBy]
topicItem.LastUser = userList[topicItem.LastReplyBy]
}
pageList := common.Paginate(forum.TopicCount, common.Config.ItemsPerPage, 5)
pi := common.ForumPage{forum.Name, user, headerVars, topicList, forum, pageList, page, lastPage}
if common.RunPreRenderHook("pre_render_forum", w, r, &user, &pi) {
return nil
}
err = common.RunThemeTemplate(headerVars.Theme.Name, "forum", pi, w)
if err != nil {
return common.InternalError(err, w, r)
}
counters.ForumViewCounter.Bump(forum.ID)
return nil
}

View File

@ -191,6 +191,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit
return common.InternalError(err, w, r)
}
counters.TopicViewCounter.Bump(topic.ID) // TODO: Move this into the router?
counters.ForumViewCounter.Bump(topic.ParentID)
return nil
}

View File

@ -3,7 +3,7 @@
<div class="colstack panel_stack">
{{template "panel-menu.html" . }}
<script>
var form_vars = {'perm_preset': ['can_moderate','can_post','read_only','no_access','default','custom']};
var formVars = {'perm_preset': ['can_moderate','can_post','read_only','no_access','default','custom']};
</script>
<main class="colstack_right">

View File

@ -35,6 +35,9 @@
<div class="rowitem passive submenu">
<a href="/panel/analytics/topics/">Topics</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/forums/">Forums</a>
</div>
<div class="rowitem passive submenu">
<a href="/panel/analytics/routes/">Routes</a>
</div>

View File

@ -8,6 +8,7 @@
<a>{{.FriendlyAgent}} Views</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
@ -17,35 +18,17 @@
</div>
</form>
<div id="panel_analytics_views" class="colstack_graph_holder">
<div class="ct-chart"></div>
<div class="ct_chart"></div>
</div>
</main>
</div>
<script>
let labels = [];
let rawLabels = [{{range .PrimaryGraph.Labels}}
{{.}},{{end}}
];
for(const i in rawLabels) {
let date = new Date(rawLabels[i]*1000);
console.log("date: ", date);
let minutes = "0" + date.getMinutes();
let label = date.getHours() + ":" + minutes.substr(-2);
console.log("label:", label);
labels.push(label);
}
labels = labels.reverse()
let seriesData = [{{range .PrimaryGraph.Series}}
{{.}},{{end}}
];
seriesData = seriesData.reverse();
Chartist.Line('.ct-chart', {
labels: labels,
series: [seriesData],
}, {
height: '250px',
});
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}");
</script>
{{template "footer.html" . }}

View File

@ -8,6 +8,7 @@
<a>User Agents</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>

View File

@ -0,0 +1,34 @@
{{template "header.html" . }}
<div class="colstack panel_stack">
{{template "panel-menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/forum/{{.Agent}}" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>{{.FriendlyAgent}} Views</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>6 hours</option>
</select>
</div>
</div>
</form>
<div id="panel_analytics_views" class="colstack_graph_holder">
<div class="ct_chart"></div>
</div>
</main>
</div>
<script>
let rawLabels = [{{range .PrimaryGraph.Labels}}
{{.}},{{end}}
];
let seriesData = [{{range .PrimaryGraph.Series}}
{{.}},{{end}}
];
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}");
</script>
{{template "footer.html" . }}

View File

@ -0,0 +1,30 @@
{{template "header.html" . }}
<div class="colstack panel_stack">
{{template "panel-menu.html" . }}
<main id="panel_analytics_right" class="colstack_right">
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/forums/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>Forums</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
<option val="six-hours"{{if eq .TimeRange "six-hours"}} selected{{end}}>6 hours</option>
</select>
</div>
</div>
</form>
<div id="panel_analytics_routes" class="colstack_item rowlist">
{{range .ItemList}}
<div class="rowitem panel_compactrow editable_parent">
<a href="/panel/analytics/forum/{{.Agent}}" class="panel_upshift">{{.FriendlyAgent}}</a>
<span class="panel_compacttext to_right">{{.Count}} views</span>
</div>
{{else}}<div class="rowitem passive rowmsg">No forum view counts could be found in the selected time range</div>{{end}}
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -8,6 +8,7 @@
<a>Post Counts</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
@ -33,30 +34,12 @@
</main>
</div>
<script>
let labels = [];
let rawLabels = [{{range .PrimaryGraph.Labels}}
{{.}},{{end}}
];
for(const i in rawLabels) {
let date = new Date(rawLabels[i]*1000);
console.log("date: ", date);
let minutes = "0" + date.getMinutes();
let label = date.getHours() + ":" + minutes.substr(-2);
console.log("label:", label);
labels.push(label);
}
labels = labels.reverse()
let seriesData = [{{range .PrimaryGraph.Series}}
{{.}},{{end}}
];
seriesData = seriesData.reverse();
Chartist.Line('.ct_chart', {
labels: labels,
series: [seriesData],
}, {
height: '250px',
});
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}");
</script>
{{template "footer.html" . }}

View File

@ -8,6 +8,7 @@
<a>{{.Agent}} Views</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
@ -17,35 +18,17 @@
</div>
</form>
<div id="panel_analytics_referrers" class="colstack_graph_holder">
<div class="ct-chart"></div>
<div class="ct_chart"></div>
</div>
</main>
</div>
<script>
let labels = [];
let rawLabels = [{{range .PrimaryGraph.Labels}}
{{.}},{{end}}
];
for(const i in rawLabels) {
let date = new Date(rawLabels[i]*1000);
console.log("date: ", date);
let minutes = "0" + date.getMinutes();
let label = date.getHours() + ":" + minutes.substr(-2);
console.log("label:", label);
labels.push(label);
}
labels = labels.reverse()
let seriesData = [{{range .PrimaryGraph.Series}}
{{.}},{{end}}
];
seriesData = seriesData.reverse();
Chartist.Line('.ct-chart', {
labels: labels,
series: [seriesData],
}, {
height: '250px',
});
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}");
</script>
{{template "footer.html" . }}

View File

@ -8,6 +8,7 @@
<a>Referrers</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>

View File

@ -8,6 +8,7 @@
<a>{{.Route}} Views</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
@ -17,7 +18,7 @@
</div>
</form>
<div id="panel_analytics_views" class="colstack_graph_holder">
<div class="ct-chart"></div>
<div class="ct_chart"></div>
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><a>Details</a></div>
@ -33,30 +34,12 @@
</main>
</div>
<script>
let labels = [];
let rawLabels = [{{range .PrimaryGraph.Labels}}
{{.}},{{end}}
];
for(const i in rawLabels) {
let date = new Date(rawLabels[i]*1000);
console.log("date: ", date);
let minutes = "0" + date.getMinutes();
let label = date.getHours() + ":" + minutes.substr(-2);
console.log("label:", label);
labels.push(label);
}
labels = labels.reverse()
let seriesData = [{{range .PrimaryGraph.Series}}
{{.}},{{end}}
];
seriesData = seriesData.reverse();
Chartist.Line('.ct-chart', {
labels: labels,
series: [seriesData],
}, {
height: '250px',
});
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}");
</script>
{{template "footer.html" . }}

View File

@ -8,6 +8,7 @@
<a>Routes</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>

View File

@ -8,6 +8,7 @@
<a>{{.FriendlyAgent}} Views</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
@ -17,35 +18,17 @@
</div>
</form>
<div id="panel_analytics_systems" class="colstack_graph_holder">
<div class="ct-chart"></div>
<div class="ct_chart"></div>
</div>
</main>
</div>
<script>
let labels = [];
let rawLabels = [{{range .PrimaryGraph.Labels}}
{{.}},{{end}}
];
for(const i in rawLabels) {
let date = new Date(rawLabels[i]*1000);
console.log("date: ", date);
let minutes = "0" + date.getMinutes();
let label = date.getHours() + ":" + minutes.substr(-2);
console.log("label:", label);
labels.push(label);
}
labels = labels.reverse()
let seriesData = [{{range .PrimaryGraph.Series}}
{{.}},{{end}}
];
seriesData = seriesData.reverse();
Chartist.Line('.ct-chart', {
labels: labels,
series: [seriesData],
}, {
height: '250px',
});
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}");
</script>
{{template "footer.html" . }}

View File

@ -8,6 +8,7 @@
<a>Operating Systems</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>

View File

@ -8,6 +8,7 @@
<a>Topic Counts</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
@ -33,30 +34,12 @@
</main>
</div>
<script>
let labels = [];
let rawLabels = [{{range .PrimaryGraph.Labels}}
{{.}},{{end}}
];
for(const i in rawLabels) {
let date = new Date(rawLabels[i]*1000);
console.log("date: ", date);
let minutes = "0" + date.getMinutes();
let label = date.getHours() + ":" + minutes.substr(-2);
console.log("label:", label);
labels.push(label);
}
labels = labels.reverse()
let seriesData = [{{range .PrimaryGraph.Series}}
{{.}},{{end}}
];
seriesData = seriesData.reverse();
Chartist.Line('.ct_chart', {
labels: labels,
series: [seriesData],
}, {
height: '250px',
});
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}");
</script>
{{template "footer.html" . }}

View File

@ -5,9 +5,10 @@
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/views/" method="get">
<div class="colstack_item colstack_head">
<div class="rowitem">
<a>Views</a>
<a>Requests</a>
<select class="timeRangeSelector to_right" name="timeRange">
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>1 month</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>1 week</option>
<option val="two-days"{{if eq .TimeRange "two-days"}} selected{{end}}>2 days</option>
<option val="one-day"{{if eq .TimeRange "one-day"}} selected{{end}}>1 day</option>
<option val="twelve-hours"{{if eq .TimeRange "twelve-hours"}} selected{{end}}>12 hours</option>
@ -33,30 +34,12 @@
</main>
</div>
<script>
let labels = [];
let rawLabels = [{{range .PrimaryGraph.Labels}}
{{.}},{{end}}
];
for(const i in rawLabels) {
let date = new Date(rawLabels[i]*1000);
console.log("date: ", date);
let minutes = "0" + date.getMinutes();
let label = date.getHours() + ":" + minutes.substr(-2);
console.log("label:", label);
labels.push(label);
}
labels = labels.reverse()
let seriesData = [{{range .PrimaryGraph.Series}}
{{.}},{{end}}
];
seriesData = seriesData.reverse();
Chartist.Line('.ct_chart', {
labels: labels,
series: [seriesData],
}, {
height: '250px',
});
buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}");
</script>
{{template "footer.html" . }}

View File

@ -2,7 +2,7 @@
<div class="colstack panel_stack">
{{template "panel-menu.html" . }}
<script>var form_vars = {
<script>var formVars = {
'forum_active': ['Hide','Show'],
'forum_preset': ['all','announce','members','staff','admins','archive','custom']};
</script>

View File

@ -196,6 +196,30 @@
.ct-point:hover {
stroke: hsl(359,98%,30%) !important;
}
/*.ct-chart-line {
height: 300px !important;
}*/
/*.ct-label.ct-horizontal {
position: fixed;
justify-content: flex-end;
text-align: right;
transform-origin: 100% 0;
transform: translate(-100%) rotate(-90deg);
white-space: nowrap;
/*padding-right: 48px;/
top: 8px;
}*/
/*.ct-label.ct-horizontal {
white-space: nowrap;
}*/
/*.ct-label.ct-horizontal {
position: fixed;
justify-content: flex-end;
text-align: right;
transform-origin: 100% 0;
transform: translate(-100%) rotate(-45deg);
white-space: nowrap;
}*/
.timeRangeSelector {
margin-top: -5px;
}