Added Chartist as a dependency.

Fixed a XSS exploit.
Centralised the post escaping logic.

Began work on the Analytics UI.
This commit is contained in:
Azareal 2017-12-30 05:47:46 +00:00
parent ac9623ab6b
commit 547254c4a1
28 changed files with 5875 additions and 323 deletions

View File

@ -96,6 +96,7 @@ var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User
"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_settings": nil,
"pre_render_panel_setting": nil,
"pre_render_panel_word_filters": nil,

View File

@ -3,6 +3,7 @@ package common
import (
//"fmt"
"bytes"
"html"
"net/url"
"regexp"
"strconv"
@ -164,6 +165,7 @@ func shortcodeToUnicode(msg string) string {
return msg
}
// TODO: Write a test for this
func PreparseMessage(msg string) string {
msg = strings.Replace(msg, "<p><br>", "\n\n", -1)
msg = strings.Replace(msg, "<p>", "\n\n", -1)
@ -172,6 +174,7 @@ func PreparseMessage(msg string) string {
if Sshooks["preparse_preassign"] != nil {
msg = RunSshook("preparse_preassign", msg)
}
msg = html.EscapeString(msg)
return shortcodeToUnicode(msg)
}
@ -317,13 +320,6 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
outbytes = append(outbytes, uidBit...)
outbytes = append(outbytes, UrlClose...)
lastItem = i
//log.Print(string(msgbytes))
//log.Print(msgbytes)
//log.Print("msgbytes[lastItem - 1]: ", msgbytes[lastItem - 1])
//log.Print("lastItem - 1: ", lastItem - 1)
//log.Print("msgbytes[lastItem]: ", msgbytes[lastItem])
//log.Print("lastItem: ", lastItem)
} else if msgbytes[i] == 'h' || msgbytes[i] == 'f' || msgbytes[i] == 'g' {
//log.Print("IN hfg")
if msgbytes[i+1] == 't' && msgbytes[i+2] == 't' && msgbytes[i+3] == 'p' {

39
common/statistics.go Normal file
View File

@ -0,0 +1,39 @@
package common
// EXPERIMENTAL
import (
"errors"
)
var StatStore StatStoreInt
type StatStoreInt interface {
LookupInt(name string, duration int, unit string) (int, error)
}
type DefaultStatStore struct {
}
func NewDefaultStatStore() *DefaultStatStore {
return &DefaultStatStore{}
}
func (store *DefaultStatStore) LookupInt(name string, duration int, unit string) (int, error) {
switch name {
case "postCount":
return store.countTable("replies", duration, unit)
}
return 0, errors.New("The requested stat doesn't exist")
}
func (store *DefaultStatStore) countTable(table string, duration int, unit string) (stat int, err error) {
/*acc := qgen.Builder.Accumulator()
counter := acc.Count("replies").DateCutoff("createdAt", 1, "day").Prepare()
if acc.FirstError() != nil {
return 0, acc.FirstError()
}
err := counter.QueryRow().Scan(&stat)*/
return stat, err
}
//stmts.todaysPostCount, err = db.Prepare("select count(*) from replies where createdAt BETWEEN (utc_timestamp() - interval 1 day) and utc_timestamp()")

View File

@ -8,7 +8,6 @@ package common
import (
"database/sql"
"html"
"html/template"
"strconv"
"time"
@ -238,7 +237,7 @@ func (topic *Topic) Delete() error {
func (topic *Topic) Update(name string, content string) error {
content = PreparseMessage(content)
parsedContent := ParseMessage(html.EscapeString(content), topic.ParentID, "forums")
parsedContent := ParseMessage(content, topic.ParentID, "forums")
_, err := topicStmts.edit.Exec(name, content, parsedContent, topic.ID)
topic.cacheRemove()
return err

View File

@ -254,6 +254,7 @@ func RouteCreateGuildSubmit(w http.ResponseWriter, r *http.Request, user common.
var guildActive = true
var guildName = html.EscapeString(r.PostFormValue("group_name"))
// TODO: Allow Markdown / BBCode / Limited HTML in the description?
var guildDesc = html.EscapeString(r.PostFormValue("group_desc"))
var gprivacy = r.PostFormValue("group_privacy")

View File

@ -49,6 +49,7 @@ var RouteMap = map[string]interface{}{
"routePanelUsers": routePanelUsers,
"routePanelUsersEdit": routePanelUsersEdit,
"routePanelUsersEditSubmit": routePanelUsersEditSubmit,
"routePanelAnalyticsViews": routePanelAnalyticsViews,
"routePanelGroups": routePanelGroups,
"routePanelGroupsEdit": routePanelGroupsEdit,
"routePanelGroupsEditPerms": routePanelGroupsEditPerms,
@ -112,31 +113,32 @@ var routeMapEnum = map[string]int{
"routePanelUsers": 31,
"routePanelUsersEdit": 32,
"routePanelUsersEditSubmit": 33,
"routePanelGroups": 34,
"routePanelGroupsEdit": 35,
"routePanelGroupsEditPerms": 36,
"routePanelGroupsEditSubmit": 37,
"routePanelGroupsEditPermsSubmit": 38,
"routePanelGroupsCreateSubmit": 39,
"routePanelBackups": 40,
"routePanelLogsMod": 41,
"routePanelDebug": 42,
"routePanel": 43,
"routeAccountEditCritical": 44,
"routeAccountEditCriticalSubmit": 45,
"routeAccountEditAvatar": 46,
"routeAccountEditAvatarSubmit": 47,
"routeAccountEditUsername": 48,
"routeAccountEditUsernameSubmit": 49,
"routeAccountEditEmail": 50,
"routeAccountEditEmailTokenSubmit": 51,
"routeProfile": 52,
"routeBanSubmit": 53,
"routeUnban": 54,
"routeActivate": 55,
"routeIps": 56,
"routeDynamic": 57,
"routeUploads": 58,
"routePanelAnalyticsViews": 34,
"routePanelGroups": 35,
"routePanelGroupsEdit": 36,
"routePanelGroupsEditPerms": 37,
"routePanelGroupsEditSubmit": 38,
"routePanelGroupsEditPermsSubmit": 39,
"routePanelGroupsCreateSubmit": 40,
"routePanelBackups": 41,
"routePanelLogsMod": 42,
"routePanelDebug": 43,
"routePanel": 44,
"routeAccountEditCritical": 45,
"routeAccountEditCriticalSubmit": 46,
"routeAccountEditAvatar": 47,
"routeAccountEditAvatarSubmit": 48,
"routeAccountEditUsername": 49,
"routeAccountEditUsernameSubmit": 50,
"routeAccountEditEmail": 51,
"routeAccountEditEmailTokenSubmit": 52,
"routeProfile": 53,
"routeBanSubmit": 54,
"routeUnban": 55,
"routeActivate": 56,
"routeIps": 57,
"routeDynamic": 58,
"routeUploads": 59,
}
var reverseRouteMapEnum = map[int]string{
0: "routeAPI",
@ -173,31 +175,32 @@ var reverseRouteMapEnum = map[int]string{
31: "routePanelUsers",
32: "routePanelUsersEdit",
33: "routePanelUsersEditSubmit",
34: "routePanelGroups",
35: "routePanelGroupsEdit",
36: "routePanelGroupsEditPerms",
37: "routePanelGroupsEditSubmit",
38: "routePanelGroupsEditPermsSubmit",
39: "routePanelGroupsCreateSubmit",
40: "routePanelBackups",
41: "routePanelLogsMod",
42: "routePanelDebug",
43: "routePanel",
44: "routeAccountEditCritical",
45: "routeAccountEditCriticalSubmit",
46: "routeAccountEditAvatar",
47: "routeAccountEditAvatarSubmit",
48: "routeAccountEditUsername",
49: "routeAccountEditUsernameSubmit",
50: "routeAccountEditEmail",
51: "routeAccountEditEmailTokenSubmit",
52: "routeProfile",
53: "routeBanSubmit",
54: "routeUnban",
55: "routeActivate",
56: "routeIps",
57: "routeDynamic",
58: "routeUploads",
34: "routePanelAnalyticsViews",
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",
}
// TODO: Stop spilling these into the package scope?
@ -565,14 +568,17 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
common.RouteViewCounter.Bump(33)
err = routePanelUsersEditSubmit(w,req,user,extraData)
case "/panel/groups/":
case "/panel/analytics/views/":
common.RouteViewCounter.Bump(34)
err = routePanelAnalyticsViews(w,req,user)
case "/panel/groups/":
common.RouteViewCounter.Bump(35)
err = routePanelGroups(w,req,user)
case "/panel/groups/edit/":
common.RouteViewCounter.Bump(35)
common.RouteViewCounter.Bump(36)
err = routePanelGroupsEdit(w,req,user,extraData)
case "/panel/groups/edit/perms/":
common.RouteViewCounter.Bump(36)
common.RouteViewCounter.Bump(37)
err = routePanelGroupsEditPerms(w,req,user,extraData)
case "/panel/groups/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -581,7 +587,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(37)
common.RouteViewCounter.Bump(38)
err = routePanelGroupsEditSubmit(w,req,user,extraData)
case "/panel/groups/edit/perms/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -590,7 +596,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(38)
common.RouteViewCounter.Bump(39)
err = routePanelGroupsEditPermsSubmit(w,req,user,extraData)
case "/panel/groups/create/":
err = common.NoSessionMismatch(w,req,user)
@ -599,7 +605,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(39)
common.RouteViewCounter.Bump(40)
err = routePanelGroupsCreateSubmit(w,req,user)
case "/panel/backups/":
err = common.SuperAdminOnly(w,req,user)
@ -608,10 +614,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(40)
common.RouteViewCounter.Bump(41)
err = routePanelBackups(w,req,user,extraData)
case "/panel/logs/mod/":
common.RouteViewCounter.Bump(41)
common.RouteViewCounter.Bump(42)
err = routePanelLogsMod(w,req,user)
case "/panel/debug/":
err = common.AdminOnly(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 = routePanelDebug(w,req,user)
default:
common.RouteViewCounter.Bump(43)
common.RouteViewCounter.Bump(44)
err = routePanel(w,req,user)
}
if err != nil {
@ -638,7 +644,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(44)
common.RouteViewCounter.Bump(45)
err = routeAccountEditCritical(w,req,user)
case "/user/edit/critical/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -653,7 +659,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(45)
common.RouteViewCounter.Bump(46)
err = routeAccountEditCriticalSubmit(w,req,user)
case "/user/edit/avatar/":
err = common.MemberOnly(w,req,user)
@ -662,7 +668,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(46)
common.RouteViewCounter.Bump(47)
err = routeAccountEditAvatar(w,req,user)
case "/user/edit/avatar/submit/":
err = common.MemberOnly(w,req,user)
@ -671,7 +677,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(47)
common.RouteViewCounter.Bump(48)
err = routeAccountEditAvatarSubmit(w,req,user)
case "/user/edit/username/":
err = common.MemberOnly(w,req,user)
@ -680,7 +686,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(48)
common.RouteViewCounter.Bump(49)
err = routeAccountEditUsername(w,req,user)
case "/user/edit/username/submit/":
err = common.NoSessionMismatch(w,req,user)
@ -695,7 +701,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(49)
common.RouteViewCounter.Bump(50)
err = routeAccountEditUsernameSubmit(w,req,user)
case "/user/edit/email/":
err = common.MemberOnly(w,req,user)
@ -704,7 +710,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(50)
common.RouteViewCounter.Bump(51)
err = routeAccountEditEmail(w,req,user)
case "/user/edit/token/":
err = common.NoSessionMismatch(w,req,user)
@ -719,11 +725,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(51)
common.RouteViewCounter.Bump(52)
err = routeAccountEditEmailTokenSubmit(w,req,user,extraData)
default:
req.URL.Path += extraData
common.RouteViewCounter.Bump(52)
common.RouteViewCounter.Bump(53)
err = routeProfile(w,req,user)
}
if err != nil {
@ -744,7 +750,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(53)
common.RouteViewCounter.Bump(54)
err = routeBanSubmit(w,req,user,extraData)
case "/users/unban/":
err = common.NoSessionMismatch(w,req,user)
@ -759,7 +765,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(54)
common.RouteViewCounter.Bump(55)
err = routeUnban(w,req,user,extraData)
case "/users/activate/":
err = common.NoSessionMismatch(w,req,user)
@ -774,7 +780,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(55)
common.RouteViewCounter.Bump(56)
err = routeActivate(w,req,user,extraData)
case "/users/ips/":
err = common.MemberOnly(w,req,user)
@ -783,7 +789,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
common.RouteViewCounter.Bump(56)
common.RouteViewCounter.Bump(57)
err = routeIps(w,req,user)
}
if err != nil {
@ -800,7 +806,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
common.NotFound(w,req)
return
}
common.RouteViewCounter.Bump(58)
common.RouteViewCounter.Bump(59)
req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this?
router.UploadHandler(w,req) // TODO: Count these views
@ -844,7 +850,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
router.RUnlock()
if ok {
common.RouteViewCounter.Bump(57) // TODO: Be more specific about *which* dynamic route it is
common.RouteViewCounter.Bump(58) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData
err = handle(w,req,user)
if err != nil {

View File

@ -73,6 +73,7 @@
"panel-forums":"Forum Manager",
"panel-delete-forum":"Delete Forum",
"panel-edit-forum":"Forum Editor",
"panel-analytics":"Analytics",
"panel-settings":"Setting Manager",
"panel-edit-setting":"Edit Setting",
"panel-word-filters":"Word Filter Manager",

View File

@ -133,7 +133,8 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user common.
}
topicName := html.EscapeString(r.PostFormValue("topic-name"))
content := html.EscapeString(common.PreparseMessage(r.PostFormValue("topic-content")))
content := common.PreparseMessage(r.PostFormValue("topic-content"))
// TODO: Fully parse the post and store it in the parsed column
tid, err := common.Topics.Create(fid, topicName, content, user.ID, user.LastIP)
if err != nil {
switch err {
@ -333,7 +334,8 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user common.User)
}
}
content := common.PreparseMessage(html.EscapeString(r.PostFormValue("reply-content")))
content := common.PreparseMessage(r.PostFormValue("reply-content"))
// TODO: Fully parse the post and put that in the parsed column
_, err = common.Rstore.Create(topic, content, user.LastIP, user.ID)
if err != nil {
return common.InternalError(err, w, r)
@ -527,7 +529,8 @@ func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user common
return common.LocalError("Invalid UID", w, r, user)
}
content := html.EscapeString(common.PreparseMessage(r.PostFormValue("reply-content")))
content := common.PreparseMessage(r.PostFormValue("reply-content"))
// TODO: Fully parse the post and store it in the parsed column
_, err = common.Prstore.Create(uid, content, user.ID, user.LastIP)
if err != nil {
return common.InternalError(err, w, r)
@ -726,7 +729,6 @@ func routeAccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user c
if ferr != nil {
return ferr
}
err := r.ParseMultipartForm(int64(common.Megabyte))
if err != nil {
return common.LocalError("Upload failed", w, r, user)
@ -786,8 +788,8 @@ func routeAccountEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user c
return common.InternalError(err, w, r)
}
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext
headerVars.NoticeList = append(headerVars.NoticeList, "Your avatar was successfully updated")
pi := common.Page{"Edit Avatar", user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_account_own_edit_avatar"] != nil {
if common.RunPreRenderHook("pre_render_account_own_edit_avatar", w, r, &user, &pi) {
@ -807,7 +809,7 @@ func routeAccountEditUsername(w http.ResponseWriter, r *http.Request, user commo
return ferr
}
pi := common.Page{"Edit common.Username", user, headerVars, tList, user.Name}
pi := common.Page{"Edit Username", user, headerVars, tList, user.Name}
if common.PreRenderHooks["pre_render_account_own_edit_username"] != nil {
if common.RunPreRenderHook("pre_render_account_own_edit_username", w, r, &user, &pi) {
return nil
@ -834,7 +836,7 @@ func routeAccountEditUsernameSubmit(w http.ResponseWriter, r *http.Request, user
user.Name = newUsername
headerVars.NoticeList = append(headerVars.NoticeList, "Your username was successfully updated")
pi := common.Page{"Edit common.Username", user, headerVars, tList, nil}
pi := common.Page{"Edit Username", user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_account_own_edit_username"] != nil {
if common.RunPreRenderHook("pre_render_account_own_edit_username", w, r, &user, &pi) {
return nil
@ -885,10 +887,10 @@ func routeAccountEditEmail(w http.ResponseWriter, r *http.Request, user common.U
email.Primary = true
emailList = append(emailList, email)
}
if !common.Site.EnableEmails {
headerVars.NoticeList = append(headerVars.NoticeList, "The mail system is currently disabled.")
}
pi := common.Page{"Email Manager", user, headerVars, emailList, nil}
if common.PreRenderHooks["pre_render_account_own_edit_email"] != nil {
if common.RunPreRenderHook("pre_render_account_own_edit_email", w, r, &user, &pi) {

View File

@ -4,7 +4,6 @@ import (
//"log"
//"fmt"
"encoding/json"
"html"
"log"
"net/http"
"strconv"
@ -45,7 +44,8 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) co
}
topicName := r.PostFormValue("topic_name")
topicContent := html.EscapeString(r.PostFormValue("topic_content"))
topicContent := common.PreparseMessage(r.PostFormValue("topic_content"))
// TODO: Fully parse the post and store it in the parsed column
err = topic.Update(topicName, topicContent)
if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)
@ -352,7 +352,7 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user common.Us
return common.NoPermissionsJSQ(w, r, user, isJs)
}
content := html.EscapeString(common.PreparseMessage(r.PostFormValue("edit_item")))
content := common.PreparseMessage(r.PostFormValue("edit_item"))
_, err = stmts.editReply.Exec(content, common.ParseMessage(content, fid, "forums"), rid)
if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)
@ -457,7 +457,7 @@ func routeProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user co
return common.NoPermissionsJSQ(w, r, user, isJs)
}
content := html.EscapeString(common.PreparseMessage(r.PostFormValue("edit_item")))
content := common.PreparseMessage(r.PostFormValue("edit_item"))
_, err = stmts.editProfileReply.Exec(content, common.ParseMessage(content, 0, ""), rid)
if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)

View File

@ -1,6 +1,7 @@
package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
@ -75,36 +76,34 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
}
// TODO: Add a stat store for this?
var postCount int
err = stmts.todaysPostCount.QueryRow().Scan(&postCount)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
var intErr error
var extractStat = func(stmt *sql.Stmt) (stat int) {
err := stmt.QueryRow().Scan(&stat)
if err != nil && err != ErrNoRows {
intErr = err
}
return stat
}
var postCount = extractStat(stmts.todaysPostCount)
var postInterval = "day"
var postColour = greaterThanSwitch(postCount, 5, 25)
var topicCount int
err = stmts.todaysTopicCount.QueryRow().Scan(&topicCount)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
}
var topicCount = extractStat(stmts.todaysTopicCount)
var topicInterval = "day"
var topicColour = greaterThanSwitch(topicCount, 0, 8)
var reportCount int
err = stmts.todaysReportCount.QueryRow().Scan(&reportCount)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
}
var reportCount = extractStat(stmts.todaysReportCount)
var reportInterval = "week"
var newUserCount int
err = stmts.todaysNewUserCount.QueryRow().Scan(&newUserCount)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
}
var newUserCount = extractStat(stmts.todaysNewUserCount)
var newUserInterval = "week"
// Did any of the extractStats fail?
if intErr != nil {
return common.InternalError(intErr, w, r)
}
var gridElements = []common.GridElement{
common.GridElement{"dash-version", "v" + version.String(), 0, "grid_istat stat_green", "", "", "Gosora is up-to-date :)"},
common.GridElement{"dash-cpu", "CPU: " + cpustr, 1, "grid_istat " + cpuColour, "", "", "The global CPU usage of this server"},
@ -115,6 +114,7 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
uonline := wsHub.userCount()
gonline := wsHub.guestCount()
totonline := uonline + gonline
reqCount := 0
var onlineColour = greaterThanSwitch(totonline, 3, 10)
var onlineGuestsColour = greaterThanSwitch(gonline, 1, 10)
@ -127,19 +127,22 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
gridElements = append(gridElements, common.GridElement{"dash-totonline", strconv.Itoa(totonline) + totunit + " online", 3, "grid_stat " + onlineColour, "", "", "The number of people who are currently online"})
gridElements = append(gridElements, common.GridElement{"dash-gonline", strconv.Itoa(gonline) + gunit + " guests online", 4, "grid_stat " + onlineGuestsColour, "", "", "The number of guests who are currently online"})
gridElements = append(gridElements, common.GridElement{"dash-uonline", strconv.Itoa(uonline) + uunit + " users online", 5, "grid_stat " + onlineUsersColour, "", "", "The number of logged-in users who are currently online"})
gridElements = append(gridElements, common.GridElement{"dash-reqs", strconv.Itoa(reqCount) + " reqs / second", 7, "grid_stat grid_end_group " + topicColour, "", "", "The number of requests over the last 24 hours"})
}
gridElements = append(gridElements, common.GridElement{"dash-postsperday", strconv.Itoa(postCount) + " posts / " + postInterval, 6, "grid_stat " + postColour, "", "", "The number of new posts over the last 24 hours"})
gridElements = append(gridElements, common.GridElement{"dash-topicsperday", strconv.Itoa(topicCount) + " topics / " + topicInterval, 7, "grid_stat " + topicColour, "", "", "The number of new topics over the last 24 hours"})
gridElements = append(gridElements, common.GridElement{"dash-totonlineperday", "20 online / day", 8, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The people online over the last 24 hours"*/})
gridElements = append(gridElements, common.GridElement{"dash-totonlineperday", "20 online / day", 8, "grid_stat stat_disabled", "", "", "Coming Soon!" /*, "The people online over the last 24 hours"*/})
gridElements = append(gridElements, common.GridElement{"dash-searches", "8 searches / week", 9, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The number of searches over the last 7 days"*/})
gridElements = append(gridElements, common.GridElement{"dash-newusers", strconv.Itoa(newUserCount) + " new users / " + newUserInterval, 10, "grid_stat", "", "", "The number of new users over the last 7 days"})
gridElements = append(gridElements, common.GridElement{"dash-reports", strconv.Itoa(reportCount) + " reports / " + reportInterval, 11, "grid_stat", "", "", "The number of reports over the last 7 days"})
gridElements = append(gridElements, common.GridElement{"dash-minperuser", "2 minutes / user / week", 12, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The average number of number of minutes spent by each active user over the last 7 days"*/})
gridElements = append(gridElements, common.GridElement{"dash-visitorsperweek", "2 visitors / week", 13, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The number of unique visitors we've had over the last 7 days"*/})
gridElements = append(gridElements, common.GridElement{"dash-postsperuser", "5 posts / user / week", 14, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The average number of posts made by each active user over the past week"*/})
if false {
gridElements = append(gridElements, common.GridElement{"dash-minperuser", "2 minutes / user / week", 12, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The average number of number of minutes spent by each active user over the last 7 days"*/})
gridElements = append(gridElements, common.GridElement{"dash-visitorsperweek", "2 visitors / week", 13, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The number of unique visitors we've had over the last 7 days"*/})
gridElements = append(gridElements, common.GridElement{"dash-postsperuser", "5 posts / user / week", 14, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The average number of posts made by each active user over the past week"*/})
}
pi := common.PanelDashboardPage{common.GetTitlePhrase("panel-dashboard"), user, headerVars, stats, "dashboard", gridElements}
if common.PreRenderHooks["pre_render_panel_dashboard"] != nil {
@ -422,6 +425,25 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use
return 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
}
pi := common.PanelPage{common.GetTitlePhrase("panel-analytics"), user, headerVars, stats, "analytics", tList, nil}
if common.PreRenderHooks["pre_render_panel_analytics"] != nil {
if common.RunPreRenderHook("pre_render_panel_analytics", w, r, &user, &pi) {
return nil
}
}
err := common.Templates.ExecuteTemplate(w, "panel-analytics-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 {

View File

@ -0,0 +1,615 @@
.ct-label {
fill: rgba(0, 0, 0, 0.4);
color: rgba(0, 0, 0, 0.4);
font-size: 0.75rem;
line-height: 1; }
.ct-chart-line .ct-label,
.ct-chart-bar .ct-label {
display: block;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex; }
.ct-chart-pie .ct-label,
.ct-chart-donut .ct-label {
dominant-baseline: central; }
.ct-label.ct-horizontal.ct-start {
-webkit-box-align: flex-end;
-webkit-align-items: flex-end;
-ms-flex-align: flex-end;
align-items: flex-end;
-webkit-box-pack: flex-start;
-webkit-justify-content: flex-start;
-ms-flex-pack: flex-start;
justify-content: flex-start;
text-align: left;
text-anchor: start; }
.ct-label.ct-horizontal.ct-end {
-webkit-box-align: flex-start;
-webkit-align-items: flex-start;
-ms-flex-align: flex-start;
align-items: flex-start;
-webkit-box-pack: flex-start;
-webkit-justify-content: flex-start;
-ms-flex-pack: flex-start;
justify-content: flex-start;
text-align: left;
text-anchor: start; }
.ct-label.ct-vertical.ct-start {
-webkit-box-align: flex-end;
-webkit-align-items: flex-end;
-ms-flex-align: flex-end;
align-items: flex-end;
-webkit-box-pack: flex-end;
-webkit-justify-content: flex-end;
-ms-flex-pack: flex-end;
justify-content: flex-end;
text-align: right;
text-anchor: end; }
.ct-label.ct-vertical.ct-end {
-webkit-box-align: flex-end;
-webkit-align-items: flex-end;
-ms-flex-align: flex-end;
align-items: flex-end;
-webkit-box-pack: flex-start;
-webkit-justify-content: flex-start;
-ms-flex-pack: flex-start;
justify-content: flex-start;
text-align: left;
text-anchor: start; }
.ct-chart-bar .ct-label.ct-horizontal.ct-start {
-webkit-box-align: flex-end;
-webkit-align-items: flex-end;
-ms-flex-align: flex-end;
align-items: flex-end;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
text-anchor: start; }
.ct-chart-bar .ct-label.ct-horizontal.ct-end {
-webkit-box-align: flex-start;
-webkit-align-items: flex-start;
-ms-flex-align: flex-start;
align-items: flex-start;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
text-anchor: start; }
.ct-chart-bar.ct-horizontal-bars .ct-label.ct-horizontal.ct-start {
-webkit-box-align: flex-end;
-webkit-align-items: flex-end;
-ms-flex-align: flex-end;
align-items: flex-end;
-webkit-box-pack: flex-start;
-webkit-justify-content: flex-start;
-ms-flex-pack: flex-start;
justify-content: flex-start;
text-align: left;
text-anchor: start; }
.ct-chart-bar.ct-horizontal-bars .ct-label.ct-horizontal.ct-end {
-webkit-box-align: flex-start;
-webkit-align-items: flex-start;
-ms-flex-align: flex-start;
align-items: flex-start;
-webkit-box-pack: flex-start;
-webkit-justify-content: flex-start;
-ms-flex-pack: flex-start;
justify-content: flex-start;
text-align: left;
text-anchor: start; }
.ct-chart-bar.ct-horizontal-bars .ct-label.ct-vertical.ct-start {
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: flex-end;
-webkit-justify-content: flex-end;
-ms-flex-pack: flex-end;
justify-content: flex-end;
text-align: right;
text-anchor: end; }
.ct-chart-bar.ct-horizontal-bars .ct-label.ct-vertical.ct-end {
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: flex-start;
-webkit-justify-content: flex-start;
-ms-flex-pack: flex-start;
justify-content: flex-start;
text-align: left;
text-anchor: end; }
.ct-grid {
stroke: rgba(0, 0, 0, 0.2);
stroke-width: 1px;
stroke-dasharray: 2px; }
.ct-grid-background {
fill: none; }
.ct-point {
stroke-width: 10px;
stroke-linecap: round; }
.ct-line {
fill: none;
stroke-width: 4px; }
.ct-area {
stroke: none;
fill-opacity: 0.1; }
.ct-bar {
fill: none;
stroke-width: 10px; }
.ct-slice-donut {
fill: none;
stroke-width: 60px; }
.ct-series-a .ct-point, .ct-series-a .ct-line, .ct-series-a .ct-bar, .ct-series-a .ct-slice-donut {
stroke: #d70206; }
.ct-series-a .ct-slice-pie, .ct-series-a .ct-slice-donut-solid, .ct-series-a .ct-area {
fill: #d70206; }
.ct-series-b .ct-point, .ct-series-b .ct-line, .ct-series-b .ct-bar, .ct-series-b .ct-slice-donut {
stroke: #f05b4f; }
.ct-series-b .ct-slice-pie, .ct-series-b .ct-slice-donut-solid, .ct-series-b .ct-area {
fill: #f05b4f; }
.ct-series-c .ct-point, .ct-series-c .ct-line, .ct-series-c .ct-bar, .ct-series-c .ct-slice-donut {
stroke: #f4c63d; }
.ct-series-c .ct-slice-pie, .ct-series-c .ct-slice-donut-solid, .ct-series-c .ct-area {
fill: #f4c63d; }
.ct-series-d .ct-point, .ct-series-d .ct-line, .ct-series-d .ct-bar, .ct-series-d .ct-slice-donut {
stroke: #d17905; }
.ct-series-d .ct-slice-pie, .ct-series-d .ct-slice-donut-solid, .ct-series-d .ct-area {
fill: #d17905; }
.ct-series-e .ct-point, .ct-series-e .ct-line, .ct-series-e .ct-bar, .ct-series-e .ct-slice-donut {
stroke: #453d3f; }
.ct-series-e .ct-slice-pie, .ct-series-e .ct-slice-donut-solid, .ct-series-e .ct-area {
fill: #453d3f; }
.ct-series-f .ct-point, .ct-series-f .ct-line, .ct-series-f .ct-bar, .ct-series-f .ct-slice-donut {
stroke: #59922b; }
.ct-series-f .ct-slice-pie, .ct-series-f .ct-slice-donut-solid, .ct-series-f .ct-area {
fill: #59922b; }
.ct-series-g .ct-point, .ct-series-g .ct-line, .ct-series-g .ct-bar, .ct-series-g .ct-slice-donut {
stroke: #0544d3; }
.ct-series-g .ct-slice-pie, .ct-series-g .ct-slice-donut-solid, .ct-series-g .ct-area {
fill: #0544d3; }
.ct-series-h .ct-point, .ct-series-h .ct-line, .ct-series-h .ct-bar, .ct-series-h .ct-slice-donut {
stroke: #6b0392; }
.ct-series-h .ct-slice-pie, .ct-series-h .ct-slice-donut-solid, .ct-series-h .ct-area {
fill: #6b0392; }
.ct-series-i .ct-point, .ct-series-i .ct-line, .ct-series-i .ct-bar, .ct-series-i .ct-slice-donut {
stroke: #f05b4f; }
.ct-series-i .ct-slice-pie, .ct-series-i .ct-slice-donut-solid, .ct-series-i .ct-area {
fill: #f05b4f; }
.ct-series-j .ct-point, .ct-series-j .ct-line, .ct-series-j .ct-bar, .ct-series-j .ct-slice-donut {
stroke: #dda458; }
.ct-series-j .ct-slice-pie, .ct-series-j .ct-slice-donut-solid, .ct-series-j .ct-area {
fill: #dda458; }
.ct-series-k .ct-point, .ct-series-k .ct-line, .ct-series-k .ct-bar, .ct-series-k .ct-slice-donut {
stroke: #eacf7d; }
.ct-series-k .ct-slice-pie, .ct-series-k .ct-slice-donut-solid, .ct-series-k .ct-area {
fill: #eacf7d; }
.ct-series-l .ct-point, .ct-series-l .ct-line, .ct-series-l .ct-bar, .ct-series-l .ct-slice-donut {
stroke: #86797d; }
.ct-series-l .ct-slice-pie, .ct-series-l .ct-slice-donut-solid, .ct-series-l .ct-area {
fill: #86797d; }
.ct-series-m .ct-point, .ct-series-m .ct-line, .ct-series-m .ct-bar, .ct-series-m .ct-slice-donut {
stroke: #b2c326; }
.ct-series-m .ct-slice-pie, .ct-series-m .ct-slice-donut-solid, .ct-series-m .ct-area {
fill: #b2c326; }
.ct-series-n .ct-point, .ct-series-n .ct-line, .ct-series-n .ct-bar, .ct-series-n .ct-slice-donut {
stroke: #6188e2; }
.ct-series-n .ct-slice-pie, .ct-series-n .ct-slice-donut-solid, .ct-series-n .ct-area {
fill: #6188e2; }
.ct-series-o .ct-point, .ct-series-o .ct-line, .ct-series-o .ct-bar, .ct-series-o .ct-slice-donut {
stroke: #a748ca; }
.ct-series-o .ct-slice-pie, .ct-series-o .ct-slice-donut-solid, .ct-series-o .ct-area {
fill: #a748ca; }
.ct-square {
display: block;
position: relative;
width: 100%; }
.ct-square:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 100%; }
.ct-square:after {
content: "";
display: table;
clear: both; }
.ct-square > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-minor-second {
display: block;
position: relative;
width: 100%; }
.ct-minor-second:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 93.75%; }
.ct-minor-second:after {
content: "";
display: table;
clear: both; }
.ct-minor-second > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-major-second {
display: block;
position: relative;
width: 100%; }
.ct-major-second:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 88.8888888889%; }
.ct-major-second:after {
content: "";
display: table;
clear: both; }
.ct-major-second > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-minor-third {
display: block;
position: relative;
width: 100%; }
.ct-minor-third:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 83.3333333333%; }
.ct-minor-third:after {
content: "";
display: table;
clear: both; }
.ct-minor-third > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-major-third {
display: block;
position: relative;
width: 100%; }
.ct-major-third:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 80%; }
.ct-major-third:after {
content: "";
display: table;
clear: both; }
.ct-major-third > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-perfect-fourth {
display: block;
position: relative;
width: 100%; }
.ct-perfect-fourth:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 75%; }
.ct-perfect-fourth:after {
content: "";
display: table;
clear: both; }
.ct-perfect-fourth > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-perfect-fifth {
display: block;
position: relative;
width: 100%; }
.ct-perfect-fifth:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 66.6666666667%; }
.ct-perfect-fifth:after {
content: "";
display: table;
clear: both; }
.ct-perfect-fifth > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-minor-sixth {
display: block;
position: relative;
width: 100%; }
.ct-minor-sixth:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 62.5%; }
.ct-minor-sixth:after {
content: "";
display: table;
clear: both; }
.ct-minor-sixth > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-golden-section {
display: block;
position: relative;
width: 100%; }
.ct-golden-section:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 61.804697157%; }
.ct-golden-section:after {
content: "";
display: table;
clear: both; }
.ct-golden-section > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-major-sixth {
display: block;
position: relative;
width: 100%; }
.ct-major-sixth:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 60%; }
.ct-major-sixth:after {
content: "";
display: table;
clear: both; }
.ct-major-sixth > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-minor-seventh {
display: block;
position: relative;
width: 100%; }
.ct-minor-seventh:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 56.25%; }
.ct-minor-seventh:after {
content: "";
display: table;
clear: both; }
.ct-minor-seventh > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-major-seventh {
display: block;
position: relative;
width: 100%; }
.ct-major-seventh:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 53.3333333333%; }
.ct-major-seventh:after {
content: "";
display: table;
clear: both; }
.ct-major-seventh > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-octave {
display: block;
position: relative;
width: 100%; }
.ct-octave:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 50%; }
.ct-octave:after {
content: "";
display: table;
clear: both; }
.ct-octave > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-major-tenth {
display: block;
position: relative;
width: 100%; }
.ct-major-tenth:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 40%; }
.ct-major-tenth:after {
content: "";
display: table;
clear: both; }
.ct-major-tenth > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-major-eleventh {
display: block;
position: relative;
width: 100%; }
.ct-major-eleventh:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 37.5%; }
.ct-major-eleventh:after {
content: "";
display: table;
clear: both; }
.ct-major-eleventh > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-major-twelfth {
display: block;
position: relative;
width: 100%; }
.ct-major-twelfth:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 33.3333333333%; }
.ct-major-twelfth:after {
content: "";
display: table;
clear: both; }
.ct-major-twelfth > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
.ct-double-octave {
display: block;
position: relative;
width: 100%; }
.ct-double-octave:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: 25%; }
.ct-double-octave:after {
content: "";
display: table;
clear: both; }
.ct-double-octave > svg {
display: block;
position: absolute;
top: 0;
left: 0; }
/*# sourceMappingURL=chartist.css.map */

File diff suppressed because one or more lines are too long

4488
public/chartist/chartist.js Normal file

File diff suppressed because it is too large Load Diff

1
public/chartist/chartist.min.css vendored Normal file

File diff suppressed because one or more lines are too long

10
public/chartist/chartist.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,241 @@
@import "settings/chartist-settings";
@mixin ct-responsive-svg-container($width: 100%, $ratio: $ct-container-ratio) {
display: block;
position: relative;
width: $width;
&:before {
display: block;
float: left;
content: "";
width: 0;
height: 0;
padding-bottom: $ratio * 100%;
}
&:after {
content: "";
display: table;
clear: both;
}
> svg {
display: block;
position: absolute;
top: 0;
left: 0;
}
}
@mixin ct-align-justify($ct-text-align: $ct-text-align, $ct-text-justify: $ct-text-justify) {
-webkit-box-align: $ct-text-align;
-webkit-align-items: $ct-text-align;
-ms-flex-align: $ct-text-align;
align-items: $ct-text-align;
-webkit-box-pack: $ct-text-justify;
-webkit-justify-content: $ct-text-justify;
-ms-flex-pack: $ct-text-justify;
justify-content: $ct-text-justify;
// Fallback to text-align for non-flex browsers
@if($ct-text-justify == 'flex-start') {
text-align: left;
} @else if ($ct-text-justify == 'flex-end') {
text-align: right;
} @else {
text-align: center;
}
}
@mixin ct-flex() {
// Fallback to block
display: block;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
}
@mixin ct-chart-label($ct-text-color: $ct-text-color, $ct-text-size: $ct-text-size, $ct-text-line-height: $ct-text-line-height) {
fill: $ct-text-color;
color: $ct-text-color;
font-size: $ct-text-size;
line-height: $ct-text-line-height;
}
@mixin ct-chart-grid($ct-grid-color: $ct-grid-color, $ct-grid-width: $ct-grid-width, $ct-grid-dasharray: $ct-grid-dasharray) {
stroke: $ct-grid-color;
stroke-width: $ct-grid-width;
@if ($ct-grid-dasharray) {
stroke-dasharray: $ct-grid-dasharray;
}
}
@mixin ct-chart-point($ct-point-size: $ct-point-size, $ct-point-shape: $ct-point-shape) {
stroke-width: $ct-point-size;
stroke-linecap: $ct-point-shape;
}
@mixin ct-chart-line($ct-line-width: $ct-line-width, $ct-line-dasharray: $ct-line-dasharray) {
fill: none;
stroke-width: $ct-line-width;
@if ($ct-line-dasharray) {
stroke-dasharray: $ct-line-dasharray;
}
}
@mixin ct-chart-area($ct-area-opacity: $ct-area-opacity) {
stroke: none;
fill-opacity: $ct-area-opacity;
}
@mixin ct-chart-bar($ct-bar-width: $ct-bar-width) {
fill: none;
stroke-width: $ct-bar-width;
}
@mixin ct-chart-donut($ct-donut-width: $ct-donut-width) {
fill: none;
stroke-width: $ct-donut-width;
}
@mixin ct-chart-series-color($color) {
.#{$ct-class-point}, .#{$ct-class-line}, .#{$ct-class-bar}, .#{$ct-class-slice-donut} {
stroke: $color;
}
.#{$ct-class-slice-pie}, .#{$ct-class-slice-donut-solid}, .#{$ct-class-area} {
fill: $color;
}
}
@mixin ct-chart($ct-container-ratio: $ct-container-ratio, $ct-text-color: $ct-text-color, $ct-text-size: $ct-text-size, $ct-grid-color: $ct-grid-color, $ct-grid-width: $ct-grid-width, $ct-grid-dasharray: $ct-grid-dasharray, $ct-point-size: $ct-point-size, $ct-point-shape: $ct-point-shape, $ct-line-width: $ct-line-width, $ct-bar-width: $ct-bar-width, $ct-donut-width: $ct-donut-width, $ct-series-names: $ct-series-names, $ct-series-colors: $ct-series-colors) {
.#{$ct-class-label} {
@include ct-chart-label($ct-text-color, $ct-text-size);
}
.#{$ct-class-chart-line} .#{$ct-class-label},
.#{$ct-class-chart-bar} .#{$ct-class-label} {
@include ct-flex();
}
.#{$ct-class-chart-pie} .#{$ct-class-label},
.#{$ct-class-chart-donut} .#{$ct-class-label} {
dominant-baseline: central;
}
.#{$ct-class-label}.#{$ct-class-horizontal}.#{$ct-class-start} {
@include ct-align-justify(flex-end, flex-start);
// Fallback for browsers that don't support foreignObjects
text-anchor: start;
}
.#{$ct-class-label}.#{$ct-class-horizontal}.#{$ct-class-end} {
@include ct-align-justify(flex-start, flex-start);
// Fallback for browsers that don't support foreignObjects
text-anchor: start;
}
.#{$ct-class-label}.#{$ct-class-vertical}.#{$ct-class-start} {
@include ct-align-justify(flex-end, flex-end);
// Fallback for browsers that don't support foreignObjects
text-anchor: end;
}
.#{$ct-class-label}.#{$ct-class-vertical}.#{$ct-class-end} {
@include ct-align-justify(flex-end, flex-start);
// Fallback for browsers that don't support foreignObjects
text-anchor: start;
}
.#{$ct-class-chart-bar} .#{$ct-class-label}.#{$ct-class-horizontal}.#{$ct-class-start} {
@include ct-align-justify(flex-end, center);
// Fallback for browsers that don't support foreignObjects
text-anchor: start;
}
.#{$ct-class-chart-bar} .#{$ct-class-label}.#{$ct-class-horizontal}.#{$ct-class-end} {
@include ct-align-justify(flex-start, center);
// Fallback for browsers that don't support foreignObjects
text-anchor: start;
}
.#{$ct-class-chart-bar}.#{$ct-class-horizontal-bars} .#{$ct-class-label}.#{$ct-class-horizontal}.#{$ct-class-start} {
@include ct-align-justify(flex-end, flex-start);
// Fallback for browsers that don't support foreignObjects
text-anchor: start;
}
.#{$ct-class-chart-bar}.#{$ct-class-horizontal-bars} .#{$ct-class-label}.#{$ct-class-horizontal}.#{$ct-class-end} {
@include ct-align-justify(flex-start, flex-start);
// Fallback for browsers that don't support foreignObjects
text-anchor: start;
}
.#{$ct-class-chart-bar}.#{$ct-class-horizontal-bars} .#{$ct-class-label}.#{$ct-class-vertical}.#{$ct-class-start} {
//@include ct-chart-label($ct-text-color, $ct-text-size, center, $ct-vertical-text-justify);
@include ct-align-justify(center, flex-end);
// Fallback for browsers that don't support foreignObjects
text-anchor: end;
}
.#{$ct-class-chart-bar}.#{$ct-class-horizontal-bars} .#{$ct-class-label}.#{$ct-class-vertical}.#{$ct-class-end} {
@include ct-align-justify(center, flex-start);
// Fallback for browsers that don't support foreignObjects
text-anchor: end;
}
.#{$ct-class-grid} {
@include ct-chart-grid($ct-grid-color, $ct-grid-width, $ct-grid-dasharray);
}
.#{$ct-class-grid-background} {
fill: $ct-grid-background-fill;
}
.#{$ct-class-point} {
@include ct-chart-point($ct-point-size, $ct-point-shape);
}
.#{$ct-class-line} {
@include ct-chart-line($ct-line-width);
}
.#{$ct-class-area} {
@include ct-chart-area();
}
.#{$ct-class-bar} {
@include ct-chart-bar($ct-bar-width);
}
.#{$ct-class-slice-donut} {
@include ct-chart-donut($ct-donut-width);
}
@if $ct-include-colored-series {
@for $i from 0 to length($ct-series-names) {
.#{$ct-class-series}-#{nth($ct-series-names, $i + 1)} {
$color: nth($ct-series-colors, $i + 1);
@include ct-chart-series-color($color);
}
}
}
}
@if $ct-include-classes {
@include ct-chart();
@if $ct-include-alternative-responsive-containers {
@for $i from 0 to length($ct-scales-names) {
.#{nth($ct-scales-names, $i + 1)} {
@include ct-responsive-svg-container($ratio: nth($ct-scales, $i + 1));
}
}
}
}

View File

@ -0,0 +1,88 @@
// Scales for responsive SVG containers
$ct-scales: ((1), (15/16), (8/9), (5/6), (4/5), (3/4), (2/3), (5/8), (1/1.618), (3/5), (9/16), (8/15), (1/2), (2/5), (3/8), (1/3), (1/4)) !default;
$ct-scales-names: (ct-square, ct-minor-second, ct-major-second, ct-minor-third, ct-major-third, ct-perfect-fourth, ct-perfect-fifth, ct-minor-sixth, ct-golden-section, ct-major-sixth, ct-minor-seventh, ct-major-seventh, ct-octave, ct-major-tenth, ct-major-eleventh, ct-major-twelfth, ct-double-octave) !default;
// Class names to be used when generating CSS
$ct-class-chart: ct-chart !default;
$ct-class-chart-line: ct-chart-line !default;
$ct-class-chart-bar: ct-chart-bar !default;
$ct-class-horizontal-bars: ct-horizontal-bars !default;
$ct-class-chart-pie: ct-chart-pie !default;
$ct-class-chart-donut: ct-chart-donut !default;
$ct-class-label: ct-label !default;
$ct-class-series: ct-series !default;
$ct-class-line: ct-line !default;
$ct-class-point: ct-point !default;
$ct-class-area: ct-area !default;
$ct-class-bar: ct-bar !default;
$ct-class-slice-pie: ct-slice-pie !default;
$ct-class-slice-donut: ct-slice-donut !default;
$ct-class-slice-donut-solid: ct-slice-donut-solid !default;
$ct-class-grid: ct-grid !default;
$ct-class-grid-background: ct-grid-background !default;
$ct-class-vertical: ct-vertical !default;
$ct-class-horizontal: ct-horizontal !default;
$ct-class-start: ct-start !default;
$ct-class-end: ct-end !default;
// Container ratio
$ct-container-ratio: (1/1.618) !default;
// Text styles for labels
$ct-text-color: rgba(0, 0, 0, 0.4) !default;
$ct-text-size: 0.75rem !default;
$ct-text-align: flex-start !default;
$ct-text-justify: flex-start !default;
$ct-text-line-height: 1;
// Grid styles
$ct-grid-color: rgba(0, 0, 0, 0.2) !default;
$ct-grid-dasharray: 2px !default;
$ct-grid-width: 1px !default;
$ct-grid-background-fill: none !default;
// Line chart properties
$ct-line-width: 4px !default;
$ct-line-dasharray: false !default;
$ct-point-size: 10px !default;
// Line chart point, can be either round or square
$ct-point-shape: round !default;
// Area fill transparency between 0 and 1
$ct-area-opacity: 0.1 !default;
// Bar chart bar width
$ct-bar-width: 10px !default;
// Donut width (If donut width is to big it can cause issues where the shape gets distorted)
$ct-donut-width: 60px !default;
// If set to true it will include the default classes and generate CSS output. If you're planning to use the mixins you
// should set this property to false
$ct-include-classes: true !default;
// If this is set to true the CSS will contain colored series. You can extend or change the color with the
// properties below
$ct-include-colored-series: $ct-include-classes !default;
// If set to true this will include all responsive container variations using the scales defined at the top of the script
$ct-include-alternative-responsive-containers: $ct-include-classes !default;
// Series names and colors. This can be extended or customized as desired. Just add more series and colors.
$ct-series-names: (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) !default;
$ct-series-colors: (
#d70206,
#f05b4f,
#f4c63d,
#d17905,
#453d3f,
#59922b,
#0544d3,
#6b0392,
#f05b4f,
#dda458,
#eacf7d,
#86797d,
#b2c326,
#6188e2,
#a748ca
) !default;

View File

@ -122,6 +122,9 @@ func (count *accCountBuilder) Limit(limit string) *accCountBuilder {
return count
}
// TODO: Add QueryRow for this and use it in statistics.go
func (count *accCountBuilder) Prepare() *sql.Stmt {
return count.build.SimpleCount(count.table, count.where, count.limit)
}
// TODO: Add a Sum builder for summing viewchunks up into one number for the dashboard?

View File

@ -89,6 +89,8 @@ func buildPanelRoutes() {
View("routePanelUsersEdit", "/panel/users/edit/", "extraData"),
Action("routePanelUsersEditSubmit", "/panel/users/edit/submit/", "extraData"),
View("routePanelAnalyticsViews", "/panel/analytics/views/"),
View("routePanelGroups", "/panel/groups/"),
View("routePanelGroupsEdit", "/panel/groups/edit/", "extraData"),
View("routePanelGroupsEditPerms", "/panel/groups/edit/perms/", "extraData"),

View File

@ -624,6 +624,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm
var replyList []common.ReplyUser
// SEO URLs...
// TODO: Do a 301 if it's the wrong username? Do a canonical too?
halves := strings.Split(r.URL.Path[len("/user/"):], ".")
if len(halves) < 2 {
halves = append(halves, halves[0])

View File

@ -137,111 +137,115 @@ var topic_26 = []byte(`</p>
<textarea name="topic_content" class="show_on_edit topic_content_input">`)
var topic_27 = []byte(`</textarea>
<span class="controls">
<span class="controls" aria-label="Controls and Author Information">
<a href="`)
var topic_28 = []byte(`" class="username real_username" rel="author">`)
var topic_29 = []byte(`</a>&nbsp;&nbsp;
`)
var topic_30 = []byte(`<a href="/topic/like/submit/`)
var topic_31 = []byte(`" class="mod_button" title="Love it" style="color:#202020;">
var topic_31 = []byte(`" class="mod_button" title="Love it" `)
var topic_32 = []byte(`aria-label="Unlike this topic"`)
var topic_33 = []byte(`aria-label="Like this topic"`)
var topic_34 = []byte(` style="color:#202020;">
<button class="username like_label"`)
var topic_32 = []byte(` style="background-color:#D6FFD6;"`)
var topic_33 = []byte(`></button></a>`)
var topic_34 = []byte(`<a href='/topic/edit/`)
var topic_35 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>`)
var topic_36 = []byte(`<a href='/topic/delete/submit/`)
var topic_37 = []byte(`' class="mod_button" style="font-weight:normal;" title="Delete Topic"><button class="username trash_label"></button></a>`)
var topic_38 = []byte(`<a class="mod_button" href='/topic/unlock/submit/`)
var topic_39 = []byte(`' style="font-weight:normal;" title="Unlock Topic"><button class="username unlock_label"></button></a>`)
var topic_40 = []byte(`<a href='/topic/lock/submit/`)
var topic_41 = []byte(`' class="mod_button" style="font-weight:normal;" title="Lock Topic"><button class="username lock_label"></button></a>`)
var topic_42 = []byte(`<a class="mod_button" href='/topic/unstick/submit/`)
var topic_43 = []byte(`' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>`)
var topic_44 = []byte(`<a href='/topic/stick/submit/`)
var topic_45 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>`)
var topic_46 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_47 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_48 = []byte(`
var topic_35 = []byte(` style="background-color:#D6FFD6;"`)
var topic_36 = []byte(`></button></a>`)
var topic_37 = []byte(`<a href='/topic/edit/`)
var topic_38 = []byte(`' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic" aria-label="Edit this topic"><button class="username edit_label"></button></a>`)
var topic_39 = []byte(`<a href='/topic/delete/submit/`)
var topic_40 = []byte(`' class="mod_button" style="font-weight:normal;" title="Delete Topic" aria-label="Delete this topic"><button class="username trash_label"></button></a>`)
var topic_41 = []byte(`<a class="mod_button" href='/topic/unlock/submit/`)
var topic_42 = []byte(`' style="font-weight:normal;" title="Unlock Topic" aria-label="Unlock this topic"><button class="username unlock_label"></button></a>`)
var topic_43 = []byte(`<a href='/topic/lock/submit/`)
var topic_44 = []byte(`' class="mod_button" style="font-weight:normal;" title="Lock Topic" aria-label="Lock this topic"><button class="username lock_label"></button></a>`)
var topic_45 = []byte(`<a class="mod_button" href='/topic/unstick/submit/`)
var topic_46 = []byte(`' style="font-weight:normal;" title="Unpin Topic" aria-label="Unpin this topic"><button class="username unpin_label"></button></a>`)
var topic_47 = []byte(`<a href='/topic/stick/submit/`)
var topic_48 = []byte(`' class="mod_button" style="font-weight:normal;" title="Pin Topic" aria-label="Pin this topic"><button class="username pin_label"></button></a>`)
var topic_49 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_50 = []byte(`' style="font-weight:normal;" title="View IP" aria-label="The poster's IP is `)
var topic_51 = []byte(`"><button class="username ip_label"></button></a>`)
var topic_52 = []byte(`
<a href="/report/submit/`)
var topic_49 = []byte(`?session=`)
var topic_50 = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag Topic"><button class="username flag_label"></button></a>
var topic_53 = []byte(`?session=`)
var topic_54 = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag this topic" aria-label="Flag this topic" rel="nofollow"><button class="username flag_label"></button></a>
`)
var topic_51 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_52 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_53 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_54 = []byte(`</a>`)
var topic_55 = []byte(`<a class="username hide_on_micro level">`)
var topic_56 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_57 = []byte(`
var topic_55 = []byte(`<a class="username hide_on_micro like_count" aria-label="The number of likes on this topic">`)
var topic_56 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_57 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_58 = []byte(`</a>`)
var topic_59 = []byte(`<a class="username hide_on_micro level" aria-label="The poster's level">`)
var topic_60 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_61 = []byte(`
</span>
</div>
</article>
<div class="rowblock post_container" aria-label="The current page for this topic" style="overflow: hidden;">`)
var topic_58 = []byte(`
var topic_62 = []byte(`
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item action_item">
<span class="action_icon" style="font-size: 18px;padding-right: 5px;">`)
var topic_59 = []byte(`</span>
var topic_63 = []byte(`</span>
<span itemprop="text">`)
var topic_60 = []byte(`</span>
var topic_64 = []byte(`</span>
</article>
`)
var topic_61 = []byte(`
var topic_65 = []byte(`
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item `)
var topic_62 = []byte(`" style="background-image: url(`)
var topic_63 = []byte(`), url(/static/`)
var topic_64 = []byte(`/post-avatar-bg.jpg);background-position: 0px `)
var topic_65 = []byte(`-1`)
var topic_66 = []byte(`0px;background-repeat:no-repeat, repeat-y;">
var topic_66 = []byte(`" style="background-image: url(`)
var topic_67 = []byte(`), url(/static/`)
var topic_68 = []byte(`/post-avatar-bg.jpg);background-position: 0px `)
var topic_69 = []byte(`-1`)
var topic_70 = []byte(`0px;background-repeat:no-repeat, repeat-y;">
`)
var topic_67 = []byte(`
var topic_71 = []byte(`
<p class="editable_block user_content" itemprop="text" style="margin:0;padding:0;">`)
var topic_68 = []byte(`</p>
var topic_72 = []byte(`</p>
<span class="controls">
<a href="`)
var topic_69 = []byte(`" class="username real_username" rel="author">`)
var topic_70 = []byte(`</a>&nbsp;&nbsp;
var topic_73 = []byte(`" class="username real_username" rel="author">`)
var topic_74 = []byte(`</a>&nbsp;&nbsp;
`)
var topic_71 = []byte(`<a href="/reply/like/submit/`)
var topic_72 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_73 = []byte(` style="background-color:#D6FFD6;"`)
var topic_74 = []byte(`></button></a>`)
var topic_75 = []byte(`<a href="/reply/edit/submit/`)
var topic_76 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
var topic_77 = []byte(`<a href="/reply/delete/submit/`)
var topic_78 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
var topic_79 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_80 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_81 = []byte(`
var topic_75 = []byte(`<a href="/reply/like/submit/`)
var topic_76 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_77 = []byte(` style="background-color:#D6FFD6;"`)
var topic_78 = []byte(`></button></a>`)
var topic_79 = []byte(`<a href="/reply/edit/submit/`)
var topic_80 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
var topic_81 = []byte(`<a href="/reply/delete/submit/`)
var topic_82 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
var topic_83 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_84 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_85 = []byte(`
<a href="/report/submit/`)
var topic_82 = []byte(`?session=`)
var topic_83 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
var topic_86 = []byte(`?session=`)
var topic_87 = []byte(`&type=reply" class="mod_button report_item" title="Flag this reply" aria-label="Flag this reply" rel="nofollow"><button class="username report_item flag_label"></button></a>
`)
var topic_84 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_85 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_86 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_87 = []byte(`</a>`)
var topic_88 = []byte(`<a class="username hide_on_micro level">`)
var topic_89 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_90 = []byte(`
var topic_88 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_89 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_90 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_91 = []byte(`</a>`)
var topic_92 = []byte(`<a class="username hide_on_micro level">`)
var topic_93 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_94 = []byte(`
</span>
</article>
`)
var topic_91 = []byte(`</div>
var topic_95 = []byte(`</div>
`)
var topic_92 = []byte(`
var topic_96 = []byte(`
<div class="rowblock topic_reply_form quick_create_form">
<form id="reply_form" enctype="multipart/form-data" action="/reply/create/" method="post"></form>
<input form="reply_form" name="tid" value='`)
var topic_93 = []byte(`' type="hidden" />
var topic_97 = []byte(`' type="hidden" />
<div class="formrow real_first_child">
<div class="formitem">
<textarea id="input_content" form="reply_form" name="reply-content" placeholder="Insert reply here" required></textarea>
@ -251,16 +255,16 @@ var topic_93 = []byte(`' type="hidden" />
<div class="formitem">
<button form="reply_form" name="reply-button" class="formbutton">Create Reply</button>
`)
var topic_94 = []byte(`
var topic_98 = []byte(`
<input name="upload_files" form="reply_form" id="upload_files" multiple type="file" style="display: none;" />
<label for="upload_files" class="formbutton add_file_button">Add File</label>
<div id="upload_file_dock"></div>`)
var topic_95 = []byte(`
var topic_99 = []byte(`
</div>
</div>
</div>
`)
var topic_96 = []byte(`
var topic_100 = []byte(`
</main>
@ -719,23 +723,24 @@ window.addEventListener("hashchange", handle_profile_hashbit, false)
`)
var forums_0 = []byte(`
<main>
<main itemscope itemtype="http://schema.org/ItemList">
<div class="rowblock opthead">
<div class="rowitem"><h1>Forums</h1></div>
<div class="rowitem"><h1 itemprop="name">Forums</h1></div>
</div>
<div class="rowblock forum_list">
`)
var forums_1 = []byte(`<div class="rowitem `)
var forums_2 = []byte(`datarow`)
var forums_3 = []byte(`">
var forums_2 = []byte(`datarow `)
var forums_3 = []byte(`"itemprop="itemListElement" itemscope
itemtype="http://schema.org/ListItem">
<span class="forum_left shift_left">
<a href="`)
var forums_4 = []byte(`">`)
var forums_4 = []byte(`" itemprop="item">`)
var forums_5 = []byte(`</a>
`)
var forums_6 = []byte(`
<br /><span class="rowsmall">`)
<br /><span class="rowsmall" itemprop="description">`)
var forums_7 = []byte(`</span>
`)
var forums_8 = []byte(`
@ -772,12 +777,12 @@ var forums_22 = []byte(`
</main>
`)
var topics_0 = []byte(`
<main>
<main itemscope itemtype="http://schema.org/ItemList">
<div class="rowblock rowhead topic_list_title_block">
<div class="rowitem topic_list_title`)
var topics_1 = []byte(` has_opt`)
var topics_2 = []byte(`"><h1>All Topics</h1></div>
var topics_2 = []byte(`"><h1 itemprop="name">All Topics</h1></div>
`)
var topics_3 = []byte(`
<div class="pre_opt auto_hide"></div>
@ -874,7 +879,7 @@ var topics_27 = []byte(`'s Avatar" title="`)
var topics_28 = []byte(`'s Avatar" /></a>
<span class="topic_inner_left">
<a class="rowtopic" href="`)
var topics_29 = []byte(`"><span>`)
var topics_29 = []byte(`" itemprop="itemListElement"><span>`)
var topics_30 = []byte(`</span></a> `)
var topics_31 = []byte(`<a class="rowsmall parent_forum" href="`)
var topics_32 = []byte(`">`)
@ -932,12 +937,12 @@ var forum_6 = []byte(`?page=`)
var forum_7 = []byte(`">&gt;</a></div>`)
var forum_8 = []byte(`
<main>
<main itemscope itemtype="http://schema.org/ItemList">
<div id="forum_head_block" class="rowblock rowhead topic_list_title_block">
<div class="rowitem forum_title`)
var forum_9 = []byte(` has_opt`)
var forum_10 = []byte(`">
<h1>`)
<h1 itemprop="name">`)
var forum_11 = []byte(`</h1>
</div>
`)
@ -1026,7 +1031,7 @@ var forum_32 = []byte(`'s Avatar" title="`)
var forum_33 = []byte(`'s Avatar" /></a>
<span class="topic_inner_left">
<a class="rowtopic" href="`)
var forum_34 = []byte(`"><span>`)
var forum_34 = []byte(`" itemprop="itemListElement"><span>`)
var forum_35 = []byte(`</span></a>
<br /><a class="rowsmall starter" href="`)
var forum_36 = []byte(`">`)

View File

@ -3,9 +3,9 @@
// Code generated by Gosora. More below:
/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */
package main
import "net/http"
import "./common"
import "strconv"
import "net/http"
// nolint
func init() {
@ -149,151 +149,159 @@ w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_31)
if tmpl_topic_vars.Topic.Liked {
w.Write(topic_32)
}
} else {
w.Write(topic_33)
}
if tmpl_topic_vars.CurrentUser.Perms.EditTopic {
w.Write(topic_34)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
if tmpl_topic_vars.Topic.Liked {
w.Write(topic_35)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteTopic {
w.Write(topic_36)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
}
if tmpl_topic_vars.CurrentUser.Perms.EditTopic {
w.Write(topic_37)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_38)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteTopic {
w.Write(topic_39)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_40)
}
if tmpl_topic_vars.CurrentUser.Perms.CloseTopic {
if tmpl_topic_vars.Topic.IsClosed {
w.Write(topic_38)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_39)
} else {
w.Write(topic_40)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_41)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_42)
} else {
w.Write(topic_43)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_44)
}
}
if tmpl_topic_vars.CurrentUser.Perms.PinTopic {
if tmpl_topic_vars.Topic.Sticky {
w.Write(topic_42)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_43)
} else {
w.Write(topic_44)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_45)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_46)
} else {
w.Write(topic_47)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_48)
}
}
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_46)
w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_47)
}
w.Write(topic_48)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_49)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_50)
if tmpl_topic_vars.Topic.LikeCount > 0 {
w.Write([]byte(tmpl_topic_vars.Topic.IPAddress))
w.Write(topic_51)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.LikeCount)))
w.Write(topic_52)
}
if tmpl_topic_vars.Topic.Tag != "" {
w.Write(topic_52)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_53)
w.Write([]byte(tmpl_topic_vars.Topic.Tag))
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_54)
} else {
if tmpl_topic_vars.Topic.LikeCount > 0 {
w.Write(topic_55)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level)))
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.LikeCount)))
w.Write(topic_56)
}
if tmpl_topic_vars.Topic.Tag != "" {
w.Write(topic_57)
w.Write([]byte(tmpl_topic_vars.Topic.Tag))
w.Write(topic_58)
} else {
w.Write(topic_59)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.Level)))
w.Write(topic_60)
}
w.Write(topic_61)
if len(tmpl_topic_vars.ItemList) != 0 {
for _, item := range tmpl_topic_vars.ItemList {
if item.ActionType != "" {
w.Write(topic_58)
w.Write([]byte(item.ActionIcon))
w.Write(topic_59)
w.Write([]byte(item.ActionType))
w.Write(topic_60)
} else {
w.Write(topic_61)
w.Write([]byte(item.ClassName))
w.Write(topic_62)
w.Write([]byte(item.Avatar))
w.Write([]byte(item.ActionIcon))
w.Write(topic_63)
w.Write([]byte(tmpl_topic_vars.Header.Theme.Name))
w.Write([]byte(item.ActionType))
w.Write(topic_64)
if item.ContentLines <= 5 {
} else {
w.Write(topic_65)
}
w.Write([]byte(item.ClassName))
w.Write(topic_66)
w.Write([]byte(item.Avatar))
w.Write(topic_67)
w.Write([]byte(item.ContentHtml))
w.Write([]byte(tmpl_topic_vars.Header.Theme.Name))
w.Write(topic_68)
w.Write([]byte(item.UserLink))
if item.ContentLines <= 5 {
w.Write(topic_69)
w.Write([]byte(item.CreatedByName))
}
w.Write(topic_70)
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_71)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write([]byte(item.ContentHtml))
w.Write(topic_72)
if item.Liked {
w.Write([]byte(item.UserLink))
w.Write(topic_73)
}
w.Write([]byte(item.CreatedByName))
w.Write(topic_74)
}
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_75)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_76)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
if item.Liked {
w.Write(topic_77)
w.Write([]byte(strconv.Itoa(item.ID)))
}
w.Write(topic_78)
}
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
w.Write(topic_79)
w.Write([]byte(item.IPAddress))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_80)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
w.Write(topic_81)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_82)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_83)
if item.LikeCount > 0 {
w.Write(topic_84)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write(topic_85)
}
if item.Tag != "" {
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_83)
w.Write([]byte(item.IPAddress))
w.Write(topic_84)
}
w.Write(topic_85)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_86)
w.Write([]byte(item.Tag))
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_87)
} else {
if item.LikeCount > 0 {
w.Write(topic_88)
w.Write([]byte(strconv.Itoa(item.Level)))
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write(topic_89)
}
if item.Tag != "" {
w.Write(topic_90)
}
}
}
w.Write([]byte(item.Tag))
w.Write(topic_91)
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
} else {
w.Write(topic_92)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write([]byte(strconv.Itoa(item.Level)))
w.Write(topic_93)
if tmpl_topic_vars.CurrentUser.Perms.UploadFiles {
}
w.Write(topic_94)
}
w.Write(topic_95)
}
}
w.Write(topic_95)
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
w.Write(topic_96)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_97)
if tmpl_topic_vars.CurrentUser.Perms.UploadFiles {
w.Write(topic_98)
}
w.Write(topic_99)
}
w.Write(topic_100)
w.Write(footer_0)
w.Write([]byte(common.BuildWidget("footer",tmpl_topic_vars.Header)))
w.Write(footer_1)

View File

@ -0,0 +1,12 @@
{{template "header.html" . }}
<div class="colstack panel_stack">
{{template "panel-menu.html" . }}
<main id="panel_dashboard_right" class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><a>Views</a></div>
</div>
<div id="panel_analytics" class="colstack_graph_holder">
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -27,25 +27,25 @@
<p class="hide_on_edit topic_content user_content" itemprop="text" style="margin:0;padding:0;">{{.Topic.ContentHTML}}</p>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
<span class="controls">
<span class="controls" aria-label="Controls and Author Information">
<a href="{{.Topic.UserLink}}" class="username real_username" rel="author">{{.Topic.CreatedByName}}</a>&nbsp;&nbsp;
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}" class="mod_button" title="Love it" style="color:#202020;">
{{if .CurrentUser.Perms.LikeItem}}<a href="/topic/like/submit/{{.Topic.ID}}" class="mod_button" title="Love it" {{if .Topic.Liked}}aria-label="Unlike this topic"{{else}}aria-label="Like this topic"{{end}} style="color:#202020;">
<button class="username like_label"{{if .Topic.Liked}} style="background-color:#D6FFD6;"{{end}}></button></a>{{end}}
{{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic"><button class="username edit_label"></button></a>{{end}}
{{if .CurrentUser.Perms.EditTopic}}<a href='/topic/edit/{{.Topic.ID}}' class="mod_button open_edit" style="font-weight:normal;" title="Edit Topic" aria-label="Edit this topic"><button class="username edit_label"></button></a>{{end}}
{{if .CurrentUser.Perms.DeleteTopic}}<a href='/topic/delete/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Delete Topic"><button class="username trash_label"></button></a>{{end}}
{{if .CurrentUser.Perms.DeleteTopic}}<a href='/topic/delete/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Delete Topic" aria-label="Delete this topic"><button class="username trash_label"></button></a>{{end}}
{{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unlock Topic"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Lock Topic"><button class="username lock_label"></button></a>{{end}}{{end}}
{{if .CurrentUser.Perms.CloseTopic}}{{if .Topic.IsClosed}}<a class="mod_button" href='/topic/unlock/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unlock Topic" aria-label="Unlock this topic"><button class="username unlock_label"></button></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Lock Topic" aria-label="Lock this topic"><button class="username lock_label"></button></a>{{end}}{{end}}
{{if .CurrentUser.Perms.PinTopic}}{{if .Topic.Sticky}}<a class="mod_button" href='/topic/unstick/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unpin Topic"><button class="username unpin_label"></button></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Pin Topic"><button class="username pin_label"></button></a>{{end}}{{end}}
{{if .CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.Topic.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag Topic"><button class="username flag_label"></button></a>
{{if .CurrentUser.Perms.PinTopic}}{{if .Topic.Sticky}}<a class="mod_button" href='/topic/unstick/submit/{{.Topic.ID}}' style="font-weight:normal;" title="Unpin Topic" aria-label="Unpin this topic"><button class="username unpin_label"></button></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}' class="mod_button" style="font-weight:normal;" title="Pin Topic" aria-label="Pin this topic"><button class="username pin_label"></button></a>{{end}}{{end}}
{{if .CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.Topic.IPAddress}}' style="font-weight:normal;" title="View IP" aria-label="The poster's IP is {{.Topic.IPAddress}}"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.Topic.ID}}?session={{.CurrentUser.Session}}&type=topic" class="mod_button report_item" style="font-weight:normal;" title="Flag this topic" aria-label="Flag this topic" rel="nofollow"><button class="username flag_label"></button></a>
{{if .Topic.LikeCount}}<a class="username hide_on_micro like_count">{{.Topic.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}}
{{if .Topic.LikeCount}}<a class="username hide_on_micro like_count" aria-label="The number of likes on this topic">{{.Topic.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}}
{{if .Topic.Tag}}<a class="username hide_on_micro user_tag">{{.Topic.Tag}}</a>{{else}}<a class="username hide_on_micro level">{{.Topic.Level}}</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>{{end}}
{{if .Topic.Tag}}<a class="username hide_on_micro user_tag">{{.Topic.Tag}}</a>{{else}}<a class="username hide_on_micro level" aria-label="The poster's level">{{.Topic.Level}}</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>{{end}}
</span>
</div>
@ -70,7 +70,7 @@
{{if $.CurrentUser.Perms.DeleteReply}}<a href="/reply/delete/submit/{{.ID}}" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>{{end}}
{{if $.CurrentUser.Perms.ViewIPs}}<a class="mod_button" href='/users/ips/?ip={{.IPAddress}}' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>{{end}}
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
<a href="/report/submit/{{.ID}}?session={{$.CurrentUser.Session}}&type=reply" class="mod_button report_item" title="Flag this reply" aria-label="Flag this reply" rel="nofollow"><button class="username report_item flag_label"></button></a>
{{if .LikeCount}}<a class="username hide_on_micro like_count">{{.LikeCount}}</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>{{end}}

View File

@ -1128,7 +1128,7 @@ select, input, textarea, button {
text-align: center;
}
/* TODO: Move these to panel.css */
#dash-version:before, #dash-cpu:before, #dash-ram:before {
#dash-version:before, #dash-cpu:before, #dash-ram:before, #dash-totonline:before, #dash-gonline:before, #dash-uonline:before, #dash-reqs:before, #dash-postsperday:before, #dash-topicsperday:before {
display: inline-block;
background: var(--tinted-background-color);
font: normal normal normal 14px/1 FontAwesome;
@ -1147,6 +1147,15 @@ select, input, textarea, button {
#dash-ram:before {
content: "\f233";
}
#dash-totonline:before, #dash-gonline:before, #dash-uonline:before {
content: "\f007";
}
#dash-reqs:before {
content: "\f080";
}
#dash-postsperday:before, #dash-topicsperday:before {
content: "\f27b";
}
@media(min-width: 721px) {
.hide_on_big {

View File

@ -6,6 +6,8 @@
}
.colstack_left {
background-color: hsl(0,0%,90%);
margin-top: -0.5px;
border-right: 1px solid var(--element-border-color);
}
.colstack_left .colstack_head {
margin-top: -1px;
@ -21,9 +23,6 @@
.colstack_left .rowmenu .passive:last-child {
border-bottom: 0.5px solid var(--element-border-color) !important;
}
/*.colstack_left > *:not(.colstack_head):last-child {
border-bottom: 0.5px solid var(--element-border-color) !important;
}*/
.submenu {
margin-left: 12px;
}

View File

@ -121,7 +121,6 @@ func (hub *WSHub) pushAlert(targetUser int, asid int, event string, elementType
}
func (hub *WSHub) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error {
//log.Print("In pushAlerts")
var wsUsers []*WSUser
hub.users.RLock()
// We don't want to keep a lock on this for too long, so we'll accept some nil pointers
@ -139,18 +138,15 @@ func (hub *WSHub) pushAlerts(users []int, asid int, event string, elementType st
continue
}
//log.Print("Building alert")
alert, err := buildAlert(asid, event, elementType, actorID, targetUserID, elementID, *wsUser.User)
if err != nil {
errs = append(errs, err)
}
//log.Print("Getting WS Writer")
w, err := wsUser.conn.NextWriter(websocket.TextMessage)
if err != nil {
errs = append(errs, err)
}
w.Write([]byte(alert))
w.Close()
}
@ -294,6 +290,26 @@ func adminStatsTicker() {
var totunit, uunit, gunit string
lessThanSwitch := func(number int, lowerBound int, midBound int) string {
switch {
case number < lowerBound:
return "stat_green"
case number < midBound:
return "stat_orange"
}
return "stat_red"
}
greaterThanSwitch := func(number int, lowerBound int, midBound int) string {
switch {
case number > midBound:
return "stat_green"
case number > lowerBound:
return "stat_orange"
}
return "stat_red"
}
AdminStatLoop:
for {
adminStatsMutex.RLock()
@ -308,6 +324,7 @@ AdminStatLoop:
uonline := wsHub.userCount()
gonline := wsHub.guestCount()
totonline := uonline + gonline
reqCount := 0
// It's far more likely that the CPU Usage will change than the other stats, so we'll optimise them separately...
noStatUpdates = (uonline == lastUonline && gonline == lastGonline && totonline == lastTotonline)
@ -318,29 +335,9 @@ AdminStatLoop:
}
if !noStatUpdates {
if totonline > 10 {
onlineColour = "stat_green"
} else if totonline > 3 {
onlineColour = "stat_orange"
} else {
onlineColour = "stat_red"
}
if gonline > 10 {
onlineGuestsColour = "stat_green"
} else if gonline > 1 {
onlineGuestsColour = "stat_orange"
} else {
onlineGuestsColour = "stat_red"
}
if uonline > 5 {
onlineUsersColour = "stat_green"
} else if uonline > 1 {
onlineUsersColour = "stat_orange"
} else {
onlineUsersColour = "stat_red"
}
onlineColour = greaterThanSwitch(totonline, 3, 10)
onlineGuestsColour = greaterThanSwitch(gonline, 1, 10)
onlineUsersColour = greaterThanSwitch(uonline, 1, 5)
totonline, totunit = common.ConvertFriendlyUnit(totonline)
uonline, uunit = common.ConvertFriendlyUnit(uonline)
@ -384,13 +381,7 @@ AdminStatLoop:
ramstr = fmt.Sprintf("%.1f", usedCount) + " / " + totstr + totalUnit
ramperc := ((memres.Total - memres.Available) * 100) / memres.Total
if ramperc < 50 {
ramColour = "stat_green"
} else if ramperc < 75 {
ramColour = "stat_orange"
} else {
ramColour = "stat_red"
}
ramColour = lessThanSwitch(int(ramperc), 50, 75)
}
}
@ -401,7 +392,6 @@ AdminStatLoop:
for watcher := range watchers {
w, err := watcher.conn.NextWriter(websocket.TextMessage)
if err != nil {
//log.Print(err.Error())
adminStatsMutex.Lock()
delete(adminStatsWatchers, watcher)
adminStatsMutex.Unlock()
@ -413,10 +403,12 @@ AdminStatLoop:
w.Write([]byte("set #dash-totonline <span>" + strconv.Itoa(totonline) + totunit + " online</span>\r"))
w.Write([]byte("set #dash-gonline <span>" + strconv.Itoa(gonline) + gunit + " guests online</span>\r"))
w.Write([]byte("set #dash-uonline <span>" + strconv.Itoa(uonline) + uunit + " users online</span>\r"))
w.Write([]byte("set #dash-reqs <span>" + strconv.Itoa(reqCount) + " reqs / second</span>\r"))
w.Write([]byte("set-class #dash-totonline grid_item grid_stat " + onlineColour + "\r"))
w.Write([]byte("set-class #dash-gonline grid_item grid_stat " + onlineGuestsColour + "\r"))
w.Write([]byte("set-class #dash-uonline grid_item grid_stat " + onlineUsersColour + "\r"))
//w.Write([]byte("set-class #dash-reqs grid_item grid_stat grid_end_group \r"))
}
w.Write([]byte("set #dash-cpu <span>CPU: " + cpustr + "%</span>\r"))