github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/routes/panel/dashboard.go (about) 1 package panel 2 3 import ( 4 "database/sql" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "runtime" 9 "strconv" 10 "sync" 11 "sync/atomic" 12 "time" 13 14 c "github.com/Azareal/Gosora/common" 15 p "github.com/Azareal/Gosora/common/phrases" 16 qgen "github.com/Azareal/Gosora/query_gen" 17 "github.com/Azareal/gopsutil/mem" 18 "github.com/pkg/errors" 19 ) 20 21 type dashStmts struct { 22 todaysPostCount *sql.Stmt 23 todaysTopicCount *sql.Stmt 24 todaysTopicCountByForum *sql.Stmt 25 todaysNewUserCount *sql.Stmt 26 weeklyTopicCountByForum *sql.Stmt 27 } 28 29 // TODO: Stop hard-coding these queries 30 func dashMySQLStmts() (stmts dashStmts, err error) { 31 db := qgen.Builder.GetConn() 32 prepStmt := func(table, ext, dur string) *sql.Stmt { 33 if err != nil { 34 return nil 35 } 36 stmt, ierr := db.Prepare("select count(*) from " + table + " where createdAt BETWEEN (utc_timestamp() - interval 1 " + dur + ") and utc_timestamp() " + ext) 37 err = errors.WithStack(ierr) 38 return stmt 39 } 40 41 stmts.todaysPostCount = prepStmt("replies", "", "day") 42 stmts.todaysTopicCount = prepStmt("topics", "", "day") 43 stmts.todaysNewUserCount = prepStmt("users", "", "day") 44 stmts.todaysTopicCountByForum = prepStmt("topics", " and parentID=?", "day") 45 stmts.weeklyTopicCountByForum = prepStmt("topics", " and parentID=?", "week") 46 47 return stmts, err 48 } 49 50 // TODO: Stop hard-coding these queries 51 func dashMSSQLStmts() (stmts dashStmts, err error) { 52 db := qgen.Builder.GetConn() 53 prepStmt := func(table, ext, dur string) *sql.Stmt { 54 if err != nil { 55 return nil 56 } 57 stmt, ierr := db.Prepare("select count(*) from " + table + " where createdAt >= DATEADD(" + dur + ", -1, GETUTCDATE())" + ext) 58 err = errors.WithStack(ierr) 59 return stmt 60 } 61 62 stmts.todaysPostCount = prepStmt("replies", "", "DAY") 63 stmts.todaysTopicCount = prepStmt("topics", "", "DAY") 64 stmts.todaysNewUserCount = prepStmt("users", "", "DAY") 65 stmts.todaysTopicCountByForum = prepStmt("topics", " and parentID=?", "DAY") 66 stmts.weeklyTopicCountByForum = prepStmt("topics", " and parentID=?", "WEEK") 67 68 return stmts, err 69 } 70 71 type GE = c.GridElement 72 73 func Dashboard(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 74 basePage, ferr := buildBasePage(w, r, user, "dashboard", "dashboard") 75 if ferr != nil { 76 return ferr 77 } 78 unknown := p.GetTmplPhrase("panel_dashboard_unknown") 79 80 // We won't calculate this on the spot anymore, as the system doesn't seem to like it if we do multiple fetches simultaneously. Should we constantly calculate this on a background thread? Perhaps, the watchdog to scale back heavy features under load? One plus side is that we'd get immediate CPU percentages here instead of waiting it to kick in with WebSockets 81 cpustr := unknown 82 var cpuColour string 83 84 lessThanSwitch := func(number, lowerBound, midBound int) string { 85 switch { 86 case number < lowerBound: 87 return "stat_green" 88 case number < midBound: 89 return "stat_orange" 90 } 91 return "stat_red" 92 } 93 94 var ramstr, ramColour string 95 memres, err := mem.VirtualMemory() 96 if err != nil { 97 ramstr = unknown 98 } else { 99 totalCount, totalUnit := c.ConvertByteUnit(float64(memres.Total)) 100 usedCount := c.ConvertByteInUnit(float64(memres.Total-memres.Available), totalUnit) 101 102 // Round totals with .9s up, it's how most people see it anyway. Floats are notoriously imprecise, so do it off 0.85 103 var totstr string 104 if (totalCount - float64(int(totalCount))) > 0.85 { 105 usedCount += 1.0 - (totalCount - float64(int(totalCount))) 106 totstr = strconv.Itoa(int(totalCount) + 1) 107 } else { 108 totstr = fmt.Sprintf("%.1f", totalCount) 109 } 110 if usedCount > totalCount { 111 usedCount = totalCount 112 } 113 ramstr = fmt.Sprintf("%.1f", usedCount) + " / " + totstr + totalUnit 114 115 ramperc := ((memres.Total - memres.Available) * 100) / memres.Total 116 ramColour = lessThanSwitch(int(ramperc), 50, 75) 117 } 118 119 var m runtime.MemStats 120 runtime.ReadMemStats(&m) 121 memCount, memUnit := c.ConvertByteUnit(float64(m.Sys)) 122 123 greaterThanSwitch := func(number, lowerBound, midBound int) string { 124 switch { 125 case number > midBound: 126 return "stat_green" 127 case number > lowerBound: 128 return "stat_orange" 129 } 130 return "stat_red" 131 } 132 133 // TODO: Add a stat store for this? 134 var intErr error 135 extractStat := func(stmt *sql.Stmt, args ...interface{}) (stat int) { 136 err := stmt.QueryRow(args...).Scan(&stat) 137 if err != nil && err != sql.ErrNoRows { 138 intErr = err 139 } 140 return stat 141 } 142 143 var stmts dashStmts 144 switch qgen.Builder.GetAdapter().GetName() { 145 case "mysql": 146 stmts, err = dashMySQLStmts() 147 case "mssql": 148 stmts, err = dashMSSQLStmts() 149 default: 150 return c.InternalError(errors.New("Unknown database adapter on dashboard"), w, r) 151 } 152 if err != nil { 153 return c.InternalError(err, w, r) 154 } 155 156 // TODO: Allow for more complex phrase structures than just suffixes 157 postCount := extractStat(stmts.todaysPostCount) 158 postInterval := p.GetTmplPhrase("panel_dashboard_day_suffix") 159 postColour := greaterThanSwitch(postCount, 5, 25) 160 161 topicCount := extractStat(stmts.todaysTopicCount) 162 topicInterval := p.GetTmplPhrase("panel_dashboard_day_suffix") 163 topicColour := greaterThanSwitch(topicCount, 0, 8) 164 165 reportCount := extractStat(stmts.weeklyTopicCountByForum, c.ReportForumID) 166 reportInterval := p.GetTmplPhrase("panel_dashboard_week_suffix") 167 168 newUserCount := extractStat(stmts.todaysNewUserCount) 169 newUserInterval := p.GetTmplPhrase("panel_dashboard_week_suffix") 170 171 // Did any of the extractStats fail? 172 if intErr != nil { 173 return c.InternalError(intErr, w, r) 174 } 175 176 grid1 := []GE{} 177 addElem1 := func(id, href, body string, order int, class, back, textColour, tooltip string) { 178 grid1 = append(grid1, GE{id, href, body, order, class, back, textColour, tooltip}) 179 } 180 gridElements := []GE{} 181 addElem := func(id, href, body string, order int, class, back, textColour, tooltip string) { 182 gridElements = append(gridElements, GE{id, href, body, order, class, back, textColour, tooltip}) 183 } 184 185 // TODO: Implement a check for new versions of Gosora 186 // TODO: Localise this 187 //addElem1("dash-version", "", "v" + version.String(), 0, "grid_istat stat_green", "", "", "Gosora is up-to-date :)") 188 addElem1("dash-version", "", "v"+c.SoftwareVersion.String(), 0, "grid_istat", "", "", "") 189 190 addElem1("dash-cpu", "", p.GetTmplPhrasef("panel_dashboard_cpu", cpustr), 1, "grid_istat "+cpuColour, "", "", p.GetTmplPhrase("panel_dashboard_cpu_desc")) 191 addElem1("dash-ram", "", p.GetTmplPhrasef("panel_dashboard_ram", ramstr), 2, "grid_istat "+ramColour, "", "", p.GetTmplPhrase("panel_dashboard_ram_desc")) 192 addElem1("dash-memused", "/panel/analytics/memory/", p.GetTmplPhrasef("panel_dashboard_memused", memCount, memUnit), 2, "grid_istat", "", "", p.GetTmplPhrase("panel_dashboard_memused_desc")) 193 194 /*dirSize := getDirSize() 195 if dirSize.Size != 0 { 196 dirFloat, unit := c.ConvertByteUnit(float64(dirSize.Size)) 197 addElem1("dash-disk","", p.GetTmplPhrasef("panel_dashboard_disk", dirFloat, unit), 2, "grid_istat", "", "", p.GetTmplPhrase("panel_dashboard_disk_desc")) 198 dur := time.Since(dirSize.Time) 199 if dur.Seconds() > 3 { 200 startDirSizeTask() 201 } 202 } else { 203 addElem1("dash-disk","", p.GetTmplPhrase("panel_dashboard_disk_unknown"), 2, "grid_istat", "", "", p.GetTmplPhrase("panel_dashboard_disk_desc")) 204 startDirSizeTask() 205 }*/ 206 207 if c.EnableWebsockets { 208 uonline := c.WsHub.UserCount() 209 gonline := c.WsHub.GuestCount() 210 totonline := uonline + gonline 211 //reqCount := 0 212 213 onlineColour := greaterThanSwitch(totonline, 3, 10) 214 onlineGuestsColour := greaterThanSwitch(gonline, 1, 10) 215 onlineUsersColour := greaterThanSwitch(uonline, 1, 5) 216 217 totonline, totunit := c.ConvertFriendlyUnit(totonline) 218 uonline, uunit := c.ConvertFriendlyUnit(uonline) 219 gonline, gunit := c.ConvertFriendlyUnit(gonline) 220 221 addElem("dash-totonline", "", p.GetTmplPhrasef("panel_dashboard_online", totonline, totunit), 3, "grid_stat "+onlineColour, "", "", p.GetTmplPhrase("panel_dashboard_online_desc")) 222 addElem("dash-gonline", "", p.GetTmplPhrasef("panel_dashboard_guests_online", gonline, gunit), 4, "grid_stat "+onlineGuestsColour, "", "", p.GetTmplPhrase("panel_dashboard_guests_online_desc")) 223 addElem("dash-uonline", "", p.GetTmplPhrasef("panel_dashboard_users_online", uonline, uunit), 5, "grid_stat "+onlineUsersColour, "", "", p.GetTmplPhrase("panel_dashboard_users_online_desc")) 224 //addElem("dash-reqs","", strconv.Itoa(reqCount) + " reqs / second", 7, "grid_stat grid_end_group " + topicColour, "", "", "The number of requests over the last 24 hours") 225 } 226 227 addElem("dash-postsperday", "", p.GetTmplPhrasef("panel_dashboard_posts", postCount, postInterval), 6, "grid_stat "+postColour, "", "", p.GetTmplPhrase("panel_dashboard_posts_desc")) 228 addElem("dash-topicsperday", "", p.GetTmplPhrasef("panel_dashboard_topics", topicCount, topicInterval), 7, "grid_stat "+topicColour, "", "", p.GetTmplPhrase("panel_dashboard_topics_desc")) 229 addElem("dash-totonlineperday", "", p.GetTmplPhrasef("panel_dashboard_online_day"), 8, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*, "The people online over the last 24 hours"*/) 230 231 addElem("dash-searches", "", p.GetTmplPhrasef("panel_dashboard_searches_day"), 9, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*"The number of searches over the last 7 days"*/) 232 addElem("dash-newusers", "", p.GetTmplPhrasef("panel_dashboard_new_users", newUserCount, newUserInterval), 10, "grid_stat", "", "", p.GetTmplPhrasef("panel_dashboard_new_users_desc")) 233 addElem("dash-reports", "", p.GetTmplPhrasef("panel_dashboard_reports", reportCount, reportInterval), 11, "grid_stat", "", "", p.GetTmplPhrasef("panel_dashboard_reports_desc")) 234 235 if false { 236 addElem("dash-minperuser", "", "?? minutes / user / week", 12, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*"The average number of number of minutes spent by each active user over the last 7 days"*/) 237 addElem("dash-visitorsperweek", "", "?? visitors / week", 13, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*"The number of unique visitors we've had over the last 7 days"*/) 238 addElem("dash-postsperuser", "", "?? posts / user / week", 14, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*"The average number of posts made by each active user over the past week"*/) 239 } 240 241 return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_dashboard_right", "", "panel_dashboard", c.DashGrids{grid1, gridElements}}) 242 } 243 244 type dirSize struct { 245 Size int 246 Time time.Time 247 } 248 249 func init() { 250 cachedDirSize.Store(dirSize{0, time.Now()}) 251 } 252 253 var cachedDirSize atomic.Value 254 var dstMu sync.Mutex 255 var dstMuGuess = 0 256 257 func startDirSizeTask() { 258 if dstMuGuess == 1 { 259 return 260 } 261 dstMu.Lock() 262 dstMuGuess = 1 263 go func() { 264 defer func() { 265 dstMuGuess = 0 266 dstMu.Unlock() 267 }() 268 defer c.EatPanics() 269 dDirSize, e := c.DirSize(".") 270 if e != nil { 271 c.LogWarning(e) 272 } 273 cachedDirSize.Store(dirSize{dDirSize, time.Now()}) 274 }() 275 } 276 277 func getDirSize() dirSize { 278 return cachedDirSize.Load().(dirSize) 279 } 280 281 type StatsDiskJson struct { 282 Total string `json:"total"` 283 } 284 285 func StatsDisk(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 286 dirSize := getDirSize() 287 dirFloat, unit := c.ConvertByteUnit(float64(dirSize.Size)) 288 u := p.GetTmplPhrasef("unit", dirFloat, unit) 289 oBytes, err := json.Marshal(StatsDiskJson{u}) 290 if err != nil { 291 return c.InternalErrorJS(err, w, r) 292 } 293 w.Write(oBytes) 294 return nil 295 }