diff --git a/common/extend.go b/common/extend.go index 3516cbe6..62b4cff6 100644 --- a/common/extend.go +++ b/common/extend.go @@ -92,24 +92,25 @@ var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User "pre_render_ban": nil, "pre_render_ips": nil, - "pre_render_panel_dashboard": nil, - "pre_render_panel_forums": nil, - "pre_render_panel_delete_forum": nil, - "pre_render_panel_edit_forum": nil, - "pre_render_panel_analytics": nil, - "pre_render_panel_analytics_routes": nil, - "pre_render_panel_settings": nil, - "pre_render_panel_setting": nil, - "pre_render_panel_word_filters": nil, - "pre_render_panel_word_filters_edit": nil, - "pre_render_panel_plugins": nil, - "pre_render_panel_users": nil, - "pre_render_panel_edit_user": nil, - "pre_render_panel_groups": nil, - "pre_render_panel_edit_group": nil, - "pre_render_panel_edit_group_perms": nil, - "pre_render_panel_themes": nil, - "pre_render_panel_mod_log": nil, + "pre_render_panel_dashboard": nil, + "pre_render_panel_forums": nil, + "pre_render_panel_delete_forum": nil, + "pre_render_panel_edit_forum": nil, + "pre_render_panel_analytics": nil, + "pre_render_panel_analytics_routes": nil, + "pre_render_panel_analytics_route_views": nil, + "pre_render_panel_settings": nil, + "pre_render_panel_setting": nil, + "pre_render_panel_word_filters": nil, + "pre_render_panel_word_filters_edit": nil, + "pre_render_panel_plugins": nil, + "pre_render_panel_users": nil, + "pre_render_panel_edit_user": nil, + "pre_render_panel_groups": nil, + "pre_render_panel_edit_group": nil, + "pre_render_panel_edit_group_perms": nil, + "pre_render_panel_themes": nil, + "pre_render_panel_mod_log": nil, "pre_render_error": nil, // Note: This hook isn't run for a few errors whose templates are computed at startup and reused, such as InternalError. This hook is also not available in JS mode. "pre_render_security_error": nil, diff --git a/common/pages.go b/common/pages.go index 67b8f6a1..cb624a97 100644 --- a/common/pages.go +++ b/common/pages.go @@ -174,7 +174,17 @@ type PanelAnalyticsRoutesPage struct { Header *HeaderVars Stats PanelStats Zone string - ItemList []PanelAnalyticsRoutesItem + ItemList []PanelAnalyticsRoutesItem +} + +type PanelAnalyticsRoutePage struct { + Title string + CurrentUser User + Header *HeaderVars + Stats PanelStats + Zone string + Route string + PrimaryGraph PanelTimeGraph } type PanelThemesPage struct { diff --git a/gen_router.go b/gen_router.go index 86fae437..a2562d6c 100644 --- a/gen_router.go +++ b/gen_router.go @@ -51,6 +51,7 @@ var RouteMap = map[string]interface{}{ "routePanelUsersEditSubmit": routePanelUsersEditSubmit, "routePanelAnalyticsViews": routePanelAnalyticsViews, "routePanelAnalyticsRoutes": routePanelAnalyticsRoutes, + "routePanelAnalyticsRouteViews": routePanelAnalyticsRouteViews, "routePanelGroups": routePanelGroups, "routePanelGroupsEdit": routePanelGroupsEdit, "routePanelGroupsEditPerms": routePanelGroupsEditPerms, @@ -116,31 +117,32 @@ var routeMapEnum = map[string]int{ "routePanelUsersEditSubmit": 33, "routePanelAnalyticsViews": 34, "routePanelAnalyticsRoutes": 35, - "routePanelGroups": 36, - "routePanelGroupsEdit": 37, - "routePanelGroupsEditPerms": 38, - "routePanelGroupsEditSubmit": 39, - "routePanelGroupsEditPermsSubmit": 40, - "routePanelGroupsCreateSubmit": 41, - "routePanelBackups": 42, - "routePanelLogsMod": 43, - "routePanelDebug": 44, - "routePanel": 45, - "routeAccountEditCritical": 46, - "routeAccountEditCriticalSubmit": 47, - "routeAccountEditAvatar": 48, - "routeAccountEditAvatarSubmit": 49, - "routeAccountEditUsername": 50, - "routeAccountEditUsernameSubmit": 51, - "routeAccountEditEmail": 52, - "routeAccountEditEmailTokenSubmit": 53, - "routeProfile": 54, - "routeBanSubmit": 55, - "routeUnban": 56, - "routeActivate": 57, - "routeIps": 58, - "routeDynamic": 59, - "routeUploads": 60, + "routePanelAnalyticsRouteViews": 36, + "routePanelGroups": 37, + "routePanelGroupsEdit": 38, + "routePanelGroupsEditPerms": 39, + "routePanelGroupsEditSubmit": 40, + "routePanelGroupsEditPermsSubmit": 41, + "routePanelGroupsCreateSubmit": 42, + "routePanelBackups": 43, + "routePanelLogsMod": 44, + "routePanelDebug": 45, + "routePanel": 46, + "routeAccountEditCritical": 47, + "routeAccountEditCriticalSubmit": 48, + "routeAccountEditAvatar": 49, + "routeAccountEditAvatarSubmit": 50, + "routeAccountEditUsername": 51, + "routeAccountEditUsernameSubmit": 52, + "routeAccountEditEmail": 53, + "routeAccountEditEmailTokenSubmit": 54, + "routeProfile": 55, + "routeBanSubmit": 56, + "routeUnban": 57, + "routeActivate": 58, + "routeIps": 59, + "routeDynamic": 60, + "routeUploads": 61, } var reverseRouteMapEnum = map[int]string{ 0: "routeAPI", @@ -179,31 +181,32 @@ var reverseRouteMapEnum = map[int]string{ 33: "routePanelUsersEditSubmit", 34: "routePanelAnalyticsViews", 35: "routePanelAnalyticsRoutes", - 36: "routePanelGroups", - 37: "routePanelGroupsEdit", - 38: "routePanelGroupsEditPerms", - 39: "routePanelGroupsEditSubmit", - 40: "routePanelGroupsEditPermsSubmit", - 41: "routePanelGroupsCreateSubmit", - 42: "routePanelBackups", - 43: "routePanelLogsMod", - 44: "routePanelDebug", - 45: "routePanel", - 46: "routeAccountEditCritical", - 47: "routeAccountEditCriticalSubmit", - 48: "routeAccountEditAvatar", - 49: "routeAccountEditAvatarSubmit", - 50: "routeAccountEditUsername", - 51: "routeAccountEditUsernameSubmit", - 52: "routeAccountEditEmail", - 53: "routeAccountEditEmailTokenSubmit", - 54: "routeProfile", - 55: "routeBanSubmit", - 56: "routeUnban", - 57: "routeActivate", - 58: "routeIps", - 59: "routeDynamic", - 60: "routeUploads", + 36: "routePanelAnalyticsRouteViews", + 37: "routePanelGroups", + 38: "routePanelGroupsEdit", + 39: "routePanelGroupsEditPerms", + 40: "routePanelGroupsEditSubmit", + 41: "routePanelGroupsEditPermsSubmit", + 42: "routePanelGroupsCreateSubmit", + 43: "routePanelBackups", + 44: "routePanelLogsMod", + 45: "routePanelDebug", + 46: "routePanel", + 47: "routeAccountEditCritical", + 48: "routeAccountEditCriticalSubmit", + 49: "routeAccountEditAvatar", + 50: "routeAccountEditAvatarSubmit", + 51: "routeAccountEditUsername", + 52: "routeAccountEditUsernameSubmit", + 53: "routeAccountEditEmail", + 54: "routeAccountEditEmailTokenSubmit", + 55: "routeProfile", + 56: "routeBanSubmit", + 57: "routeUnban", + 58: "routeActivate", + 59: "routeIps", + 60: "routeDynamic", + 61: "routeUploads", } // TODO: Stop spilling these into the package scope? @@ -577,14 +580,17 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/panel/analytics/routes/": common.RouteViewCounter.Bump(35) err = routePanelAnalyticsRoutes(w,req,user) - case "/panel/groups/": + case "/panel/analytics/route/": common.RouteViewCounter.Bump(36) + err = routePanelAnalyticsRouteViews(w,req,user,extraData) + case "/panel/groups/": + common.RouteViewCounter.Bump(37) err = routePanelGroups(w,req,user) case "/panel/groups/edit/": - common.RouteViewCounter.Bump(37) + common.RouteViewCounter.Bump(38) err = routePanelGroupsEdit(w,req,user,extraData) case "/panel/groups/edit/perms/": - common.RouteViewCounter.Bump(38) + common.RouteViewCounter.Bump(39) err = routePanelGroupsEditPerms(w,req,user,extraData) case "/panel/groups/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -593,7 +599,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(39) + common.RouteViewCounter.Bump(40) err = routePanelGroupsEditSubmit(w,req,user,extraData) case "/panel/groups/edit/perms/submit/": err = common.NoSessionMismatch(w,req,user) @@ -602,7 +608,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(40) + common.RouteViewCounter.Bump(41) err = routePanelGroupsEditPermsSubmit(w,req,user,extraData) case "/panel/groups/create/": err = common.NoSessionMismatch(w,req,user) @@ -611,7 +617,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(41) + common.RouteViewCounter.Bump(42) err = routePanelGroupsCreateSubmit(w,req,user) case "/panel/backups/": err = common.SuperAdminOnly(w,req,user) @@ -620,10 +626,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(42) + common.RouteViewCounter.Bump(43) err = routePanelBackups(w,req,user,extraData) case "/panel/logs/mod/": - common.RouteViewCounter.Bump(43) + common.RouteViewCounter.Bump(44) err = routePanelLogsMod(w,req,user) case "/panel/debug/": err = common.AdminOnly(w,req,user) @@ -632,10 +638,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(44) + common.RouteViewCounter.Bump(45) err = routePanelDebug(w,req,user) default: - common.RouteViewCounter.Bump(45) + common.RouteViewCounter.Bump(46) err = routePanel(w,req,user) } if err != nil { @@ -650,7 +656,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(46) + common.RouteViewCounter.Bump(47) err = routeAccountEditCritical(w,req,user) case "/user/edit/critical/submit/": err = common.NoSessionMismatch(w,req,user) @@ -665,7 +671,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(47) + common.RouteViewCounter.Bump(48) err = routeAccountEditCriticalSubmit(w,req,user) case "/user/edit/avatar/": err = common.MemberOnly(w,req,user) @@ -674,7 +680,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(48) + common.RouteViewCounter.Bump(49) err = routeAccountEditAvatar(w,req,user) case "/user/edit/avatar/submit/": err = common.MemberOnly(w,req,user) @@ -683,7 +689,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(49) + common.RouteViewCounter.Bump(50) err = routeAccountEditAvatarSubmit(w,req,user) case "/user/edit/username/": err = common.MemberOnly(w,req,user) @@ -692,7 +698,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(50) + common.RouteViewCounter.Bump(51) err = routeAccountEditUsername(w,req,user) case "/user/edit/username/submit/": err = common.NoSessionMismatch(w,req,user) @@ -707,7 +713,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(51) + common.RouteViewCounter.Bump(52) err = routeAccountEditUsernameSubmit(w,req,user) case "/user/edit/email/": err = common.MemberOnly(w,req,user) @@ -716,7 +722,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(52) + common.RouteViewCounter.Bump(53) err = routeAccountEditEmail(w,req,user) case "/user/edit/token/": err = common.NoSessionMismatch(w,req,user) @@ -731,11 +737,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(53) + common.RouteViewCounter.Bump(54) err = routeAccountEditEmailTokenSubmit(w,req,user,extraData) default: req.URL.Path += extraData - common.RouteViewCounter.Bump(54) + common.RouteViewCounter.Bump(55) err = routeProfile(w,req,user) } if err != nil { @@ -756,7 +762,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(55) + common.RouteViewCounter.Bump(56) err = routeBanSubmit(w,req,user,extraData) case "/users/unban/": err = common.NoSessionMismatch(w,req,user) @@ -771,7 +777,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(56) + common.RouteViewCounter.Bump(57) err = routeUnban(w,req,user,extraData) case "/users/activate/": err = common.NoSessionMismatch(w,req,user) @@ -786,7 +792,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(57) + common.RouteViewCounter.Bump(58) err = routeActivate(w,req,user,extraData) case "/users/ips/": err = common.MemberOnly(w,req,user) @@ -795,7 +801,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - common.RouteViewCounter.Bump(58) + common.RouteViewCounter.Bump(59) err = routeIps(w,req,user) } if err != nil { @@ -812,7 +818,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { common.NotFound(w,req) return } - common.RouteViewCounter.Bump(60) + common.RouteViewCounter.Bump(61) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? router.UploadHandler(w,req) // TODO: Count these views @@ -856,7 +862,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { router.RUnlock() if ok { - common.RouteViewCounter.Bump(59) // TODO: Be more specific about *which* dynamic route it is + common.RouteViewCounter.Bump(60) // TODO: Be more specific about *which* dynamic route it is req.URL.Path += extraData err = handle(w,req,user) if err != nil { diff --git a/panel_routes.go b/panel_routes.go index fe4b76b1..c21aca44 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -510,7 +510,7 @@ func routePanelAnalyticsRoutes(w http.ResponseWriter, r *http.Request, user comm var routeMap = make(map[string]int) acc := qgen.Builder.Accumulator() - rows, err := acc.Select("viewchunks").Columns("count, createdAt, route").Where("route != ''").DateCutoff("createdAt", 1, "day").Query() + rows, err := acc.Select("viewchunks").Columns("count, route").Where("route != ''").DateCutoff("createdAt", 1, "day").Query() if err != nil && err != ErrNoRows { return common.InternalError(err, w, r) } @@ -518,15 +518,13 @@ func routePanelAnalyticsRoutes(w http.ResponseWriter, r *http.Request, user comm for rows.Next() { var count int - var createdAt time.Time var route string - err := rows.Scan(&count, &createdAt, &route) + err := rows.Scan(&count, &route) if err != nil { return common.InternalError(err, w, r) } log.Print("count: ", count) - log.Print("createdAt: ", createdAt) log.Print("route: ", route) routeMap[route] += count } @@ -557,6 +555,81 @@ func routePanelAnalyticsRoutes(w http.ResponseWriter, r *http.Request, user comm return nil } +func routePanelAnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user common.User, route 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") + + var revLabelList []int64 + var labelList []int64 + var viewMap = make(map[int64]int64) + var currentTime = time.Now().Unix() + + for i := 1; i <= 12; i++ { + var label = currentTime - int64(i*60*30) + revLabelList = append(revLabelList, label) + viewMap[label] = 0 + } + for _, value := range revLabelList { + labelList = append(labelList, value) + } + + var viewList []int64 + log.Print("in routePanelAnalyticsRouteViews") + + acc := qgen.Builder.Accumulator() + rows, err := acc.Select("viewchunks").Columns("count, createdAt").Where("route = ?").DateCutoff("createdAt", 6, "hour").Query(route) + 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) + } + log.Print("count: ", count) + log.Print("createdAt: ", createdAt) + + var unixCreatedAt = createdAt.Unix() + 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) + + pi := common.PanelAnalyticsRoutePage{common.GetTitlePhrase("panel-analytics"), user, headerVars, stats, "analytics", html.EscapeString(route), graph} + if common.PreRenderHooks["pre_render_panel_analytics_route_views"] != nil { + if common.RunPreRenderHook("pre_render_panel_analytics_route_views", w, r, &user, &pi) { + return nil + } + } + err = common.Templates.ExecuteTemplate(w, "panel-analytics-route-views.html", pi) + if err != nil { + return common.InternalError(err, w, r) + } + return nil +} + func routePanelSettings(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { headerVars, stats, ferr := common.PanelUserCheck(w, r, &user) if ferr != nil { diff --git a/router_gen/routes.go b/router_gen/routes.go index 503e08c5..4b6b52e5 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -91,6 +91,7 @@ func buildPanelRoutes() { View("routePanelAnalyticsViews", "/panel/analytics/views/"), View("routePanelAnalyticsRoutes", "/panel/analytics/routes/"), + View("routePanelAnalyticsRouteViews", "/panel/analytics/route/", "extraData"), View("routePanelGroups", "/panel/groups/"), View("routePanelGroupsEdit", "/panel/groups/edit/", "extraData"), diff --git a/templates/account-own-edit-email.html b/templates/account-own-edit-email.html index 096eb740..de675f4a 100644 --- a/templates/account-own-edit-email.html +++ b/templates/account-own-edit-email.html @@ -1,18 +1,18 @@ {{template "header.html" . }} -
+
{{template "account-menu.html" . }}

Emails

-
+
{{range .ItemList}}
{{.Email}} - - {{if .Primary}}Primary{{else}}Secondary{{end}} - {{if .Validated}}Verified{{else}}Resend Verification Email{{end}} + + {{if .Primary}}Primary{{else}}Secondary{{end}} + {{if .Validated}}Verified{{else}}Resend Verification Email{{end}}
{{end}} diff --git a/templates/panel-analytics-route-views.html b/templates/panel-analytics-route-views.html new file mode 100644 index 00000000..fe513207 --- /dev/null +++ b/templates/panel-analytics-route-views.html @@ -0,0 +1,40 @@ +{{template "header.html" . }} +
+{{template "panel-menu.html" . }} +
+ +
+
+
+
+
+ +{{template "footer.html" . }} diff --git a/templates/panel-analytics-routes.html b/templates/panel-analytics-routes.html index 36d8274b..9293bec1 100644 --- a/templates/panel-analytics-routes.html +++ b/templates/panel-analytics-routes.html @@ -3,13 +3,13 @@ {{template "panel-menu.html" . }}
{{range .ItemList}}
- {{.Route}} - {{.Count}} + {{.Route}} + {{.Count}} views
{{end}}
diff --git a/templates/panel-groups.html b/templates/panel-groups.html index 595ec6a6..2eeebadd 100644 --- a/templates/panel-groups.html +++ b/templates/panel-groups.html @@ -35,7 +35,7 @@
-
+
diff --git a/themes/cosora/public/main.css b/themes/cosora/public/main.css index 64f6852f..549c107a 100644 --- a/themes/cosora/public/main.css +++ b/themes/cosora/public/main.css @@ -406,16 +406,15 @@ ul { height: 27px; width: 100px; margin-left: 10px; - border: none; - border-bottom: 1px solid var(--header-border-color); - outline: none; } .topic_name_row input, .ip_search_input { width: 100%; - border: none; - border-bottom: 1px solid var(--header-border-color); display: inline-block; padding-left: 8px; +} +input, select { + border: none; + border-bottom: 1px solid var(--header-border-color); outline: none; } .topic_content_row textarea { @@ -423,11 +422,16 @@ ul { width: 100%; } +.formbutton { + margin-top: 12px; + margin-left: auto; + margin-right: auto; +} .quick_button_row .formitem { display: flex; margin-left: 2px; } -.quick_button_row button, .quick_button_row label, .ip_search_search { +.quick_button_row button, .quick_button_row label, .ip_search_search, .formbutton { padding-left: 10px; padding-right: 10px; padding-top: 6px; @@ -443,6 +447,9 @@ ul { background: hsl(209, 97%, 56%); border-radius: 2px; } +.quick_button_row button, .quick_button_row label, .ip_search_search { + margin-right: 0px; +} .quick_button_row button, .quick_button_row label { margin-left: 10px; margin-top: 8px; @@ -463,12 +470,24 @@ label.uploadItem { padding-left: 33px; } -select, input, textarea, button { +button { border: 1px solid var(--header-border-color); +} +select, input, textarea, button { background: var(--element-background-color); padding: 5px; color: hsl(0,0%,30%); } +input, select { + color: var(--primary-text-color); +} +input:not(:focus):not([type="submit"]), select:not(:focus) { + color: var(--light-text-color); +} +textarea { + outline: none; + border: 1px solid var(--header-border-color); +} .topic_reply_container { display: flex; @@ -1003,6 +1022,7 @@ select, input, textarea, button { } .formitem:only-child { width: 100%; + display: flex; } .the_form .formitem:only-child button { margin-left: auto; @@ -1100,7 +1120,7 @@ select, input, textarea, button { } #themeSelectorSelect { padding: 3px; - margin-top: 2px; + margin-top: 0px; } .colstack_grid {