diff --git a/socketserver/cmd/statsweb/html.go b/socketserver/cmd/statsweb/html.go new file mode 100644 index 00000000..5634c1f1 --- /dev/null +++ b/socketserver/cmd/statsweb/html.go @@ -0,0 +1,59 @@ +package main + +import ( + "html/template" + "net/http" + "time" + "bitbucket.org/stendec/frankerfacez/socketserver/server" +) + +type CalendarData struct { + Weeks []CalWeekData +} +type CalWeekData struct { + Days []CalDayData +} +type CalDayData struct { + NoData bool + Date int + UniqUsers int +} + +type CalendarMonthInfo struct { + Year int + Month time.Month + // Ranges from -5 to +1. + // A value of +1 means the 1st of the month is a Sunday. + // A value of 0 means the 1st of the month is a Monday. + // A value of -5 means the 1st of the month is a Saturday. + FirstSundayOffset int + // True if the calendar for this month needs six sundays. + NeedSixSundays bool +} + +func GetMonthInfo(at time.Time) CalendarMonthInfo { + year, month, _ := at.Date() + // 1 (start of month) - weekday of start of month = day offset of start of week at start of month + monthWeekStartDay := 1 - time.Date(year, month, 1, 0, 0, 0, 0, server.CounterLocation).Weekday() + // first day on calendar + 6 weeks < end of month? + sixthSundayDay := monthWeekStartDay + 5*7 + sixthSundayDate := time.Date(year, month, sixthSundayDay, 0, 0, 0, 0, server.CounterLocation) + var needSixSundays bool = false + if sixthSundayDate.Month() == month { + needSixSundays = true + } + + return CalendarMonthInfo{ + Year: year, + Month: month, + FirstSundayOffset: monthWeekStartDay, + NeedSixSundays: needSixSundays, + } +} + +func renderCalendar(w http.ResponseWriter, at time.Time) { + layout, err := template.ParseFiles("./webroot/layout.template.html", "./webroot/cal_entry.hbs", "./webroot/calendar.hbs") + data := CalendarData{} + data.Weeks = make([]CalWeekData, 6) + +} diff --git a/socketserver/cmd/statsweb/statsweb.go b/socketserver/cmd/statsweb/statsweb.go index 683754a4..6944be6f 100644 --- a/socketserver/cmd/statsweb/statsweb.go +++ b/socketserver/cmd/statsweb/statsweb.go @@ -6,6 +6,11 @@ import ( "github.com/clarkduvall/hyperloglog" "time" "bitbucket.org/stendec/frankerfacez/socketserver/server" + "net/url" + "fmt" + "strings" + "errors" + "github.com/dustin/gojson" ) var configLocation = flag.String("config", "./config.json", "Location of the configuration file. Defaults to ./config.json") @@ -25,9 +30,160 @@ func main() { loadConfig() + http.HandleFunc("/api", ServeAPI) http.ListenAndServe(config.ListenAddr, http.DefaultServeMux) } +const RequestURIName = "q" +const separatorRange = "~" +const separatorAdd = " " +const jsonErrMalformedRequest = `{"status":"error","error":"malformed request uri"}` +const jsonErrBlankRequest = `{"status":"error","error":"no queries given"}` +const statusError = "error" +const statusPartial = "partial" +const statusOk = "ok" +type apiResponse struct { + Status string `json:"status"` + Responses []requestResponse `json:"resp"` +} +type requestResponse struct { + Status string `json:"status"` + Request string `json:"req"` + Error string `json:"error,omitempty"` + Count uint64 `json:"count,omitempty"` +} + +type serverFilter struct { + // TODO +} +func (sf *serverFilter) IsServerAllowed(server string) { + return true +} +const serverFilterAll serverFilter + +func ServeAPI(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + u, err := url.ParseRequestURI(r.RequestURI) + if err != nil { + w.WriteHeader(400) + fmt.Fprint(w, jsonErrMalformedRequest) + return + } + + query := u.Query() + reqCount := len(query[RequestURIName]) + if reqCount == 0 { + w.WriteHeader(400) + fmt.Fprint(w, jsonErrBlankRequest) + return + } + + resp := apiResponse{Status: statusOk} + resp.Responses = make([]requestResponse, reqCount) + for i, v := range query[RequestURIName] { + resp.Responses[i] = processSingleRequest(v) + } + for _, v := range resp.Responses { + if v.Status == statusError { + resp.Status = statusPartial + break + } + } + + w.WriteHeader(200) + enc := json.NewEncoder(w) + enc.Encode(resp) +} + +const errRangeFormatIncorrect = "incorrect range format, must be yyyy-mm-dd~yyyy-mm-dd" + +func processSingleRequest(req string) (result requestResponse) { + // Forms: + // Single: 2016-01-02 + // Range: 2016-01-03~2016-01-09 + // Add disparate: 2016-01-02 2016-01-03 2016-01-09 2016-01-10 + // NOTE: Spaces are uri-encoded as + + // Add ranges: 2016-01-04~2016-01-08 2016-01-11~2016-01-15 + var hll hyperloglog.HyperLogLogPlus, _ = hyperloglog.NewPlus(server.CounterPrecision) + addSplit := strings.Split(req, separatorAdd) + + result.Request = req + result.Status = statusOk + + outerLoop: + for _, split1 := range addSplit { + if len(split1) == 0 { + continue + } + + rangeSplit := strings.Split(split1, separatorRange) + if len(rangeSplit) == 1 { + at, err := parseDate(rangeSplit[0]) + if err != nil { + result.Status = statusError + result.Error = err.Error() + break outerLoop + } + err = addSingleDate(at, serverFilterAll, &hll) + if err != nil { + result.Status = statusError + result.Error = err.Error() + break outerLoop + } + } else if len(rangeSplit) == 2 { + from, err := parseDate(rangeSplit[0]) + if err != nil { + result.Status = statusError + result.Error = err.Error() + break outerLoop + } + to, err := parseDate(rangeSplit[1]) + if err != nil { + result.Status = statusError + result.Error = err.Error() + break outerLoop + } + err = addRange(from, to, serverFilterAll, &hll) + if err != nil { + result.Status = statusError + result.Error = err.Error() + break outerLoop + } + } else { + result.Status = statusError + result.Error = errRangeFormatIncorrect + break outerLoop + } + } + + if result.Status == statusOk { + result.Count = hll.Count() + } + return result +} + +var errBadDate = errors.New("bad date format, must be yyyy-mm-dd") +var zeroTime = time.Unix(0, 0) + +func parseDate(dateStr string) (time.Time, error) { + var year, month, day int + n, err := fmt.Sscanf(dateStr, "%d-%d-%d", &year, &month, &day) + if err != nil || n != 3 { + return zeroTime, errBadDate + } + return time.Date(year, month, day, 0, 0, 0, 0, server.CounterLocation) +} + +func addSingleDate(at time.Time, filter serverFilter, dest *hyperloglog.HyperLogLogPlus) error { + // TODO + return nil +} + +func addRange(start time.Time, end time.Time, filter serverFilter, dest *hyperloglog.HyperLogLogPlus) error { + +} + func combineDateRange(from time.Time, to time.Time, dest *hyperloglog.HyperLogLogPlus) error { from = server.TruncateToMidnight(from) to = server.TruncateToMidnight(to) diff --git a/socketserver/cmd/statsweb/webroot/cal_entry.hbs b/socketserver/cmd/statsweb/webroot/cal_entry.hbs new file mode 100644 index 00000000..cf178cf3 --- /dev/null +++ b/socketserver/cmd/statsweb/webroot/cal_entry.hbs @@ -0,0 +1,6 @@ + + {{.Date}} + {{if not .NoData}} + {{.UniqUsers}} + {{end}} + diff --git a/socketserver/cmd/statsweb/webroot/calendar.hbs b/socketserver/cmd/statsweb/webroot/calendar.hbs new file mode 100644 index 00000000..1a8070ce --- /dev/null +++ b/socketserver/cmd/statsweb/webroot/calendar.hbs @@ -0,0 +1,18 @@ + + + + + + + + + + + + {{range .Weeks}} + {{range .Days}} + {{template "cal_entry"}} + {{end}} + {{end}} + +
SundayMondayTuesdayWednesdayThursdayFridaySaturday
\ No newline at end of file diff --git a/socketserver/cmd/statsweb/webroot/layout.template.html b/socketserver/cmd/statsweb/webroot/layout.template.html new file mode 100644 index 00000000..09c24acf --- /dev/null +++ b/socketserver/cmd/statsweb/webroot/layout.template.html @@ -0,0 +1,15 @@ + + + + + Socket Server Stats Dashboard + + + +
+ {{template "content"}} +
+ +