Running again, fix scoreboard.js

This commit is contained in:
Neale Pickett 2020-08-14 20:26:04 -06:00
parent cedc8521ff
commit ab0488ba14
15 changed files with 190 additions and 100 deletions

View File

@ -9,7 +9,7 @@ import (
type Award struct { type Award struct {
// Unix epoch time of this event // Unix epoch time of this event
When int64 When int64
TeamId string TeamID string
Category string Category string
Points int Points int
} }
@ -18,26 +18,25 @@ type AwardList []*Award
// Implement sort.Interface on AwardList // Implement sort.Interface on AwardList
func (awards AwardList) Len() int { func (awards AwardList) Len() int {
return len(awards) return len(awards)
} }
func (awards AwardList) Less(i, j int) bool { func (awards AwardList) Less(i, j int) bool {
return awards[i].When.Before(awards[j].When) return awards[i].When < awards[j].When
} }
func (awards AwardList) Swap(i, j int) { func (awards AwardList) Swap(i, j int) {
tmp := awards[i] tmp := awards[i]
awards[i] = awards[j] awards[i] = awards[j]
awards[j] = tmp awards[j] = tmp
} }
func ParseAward(s string) (*Award, error) { func ParseAward(s string) (*Award, error) {
ret := Award{} ret := Award{}
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamId, &ret.Category, &ret.Points) n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamID, &ret.Category, &ret.Points)
if err != nil { if err != nil {
return nil, err return nil, err
} else if n != 4 { } else if n != 4 {
@ -48,7 +47,7 @@ func ParseAward(s string) (*Award, error) {
} }
func (a *Award) String() string { func (a *Award) String() string {
return fmt.Sprintf("%d %s %s %d", a.When, a.TeamId, a.Category, a.Points) return fmt.Sprintf("%d %s %s %d", a.When, a.TeamID, a.Category, a.Points)
} }
func (a *Award) MarshalJSON() ([]byte, error) { func (a *Award) MarshalJSON() ([]byte, error) {
@ -57,7 +56,7 @@ func (a *Award) MarshalJSON() ([]byte, error) {
} }
ao := []interface{}{ ao := []interface{}{
a.When, a.When,
a.TeamId, a.TeamID,
a.Category, a.Category,
a.Points, a.Points,
} }
@ -67,7 +66,7 @@ func (a *Award) MarshalJSON() ([]byte, error) {
func (a *Award) Same(o *Award) bool { func (a *Award) Same(o *Award) bool {
switch { switch {
case a.TeamId != o.TeamId: case a.TeamID != o.TeamID:
return false return false
case a.Category != o.Category: case a.Category != o.Category:
return false return false

View File

@ -1,8 +1,8 @@
package main package main
import ( import (
"testing"
"sort" "sort"
"testing"
) )
func TestAward(t *testing.T) { func TestAward(t *testing.T) {
@ -12,7 +12,7 @@ func TestAward(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if a.TeamId != "1a2b3c4d" { if a.TeamID != "1a2b3c4d" {
t.Error("TeamID parsed wrong") t.Error("TeamID parsed wrong")
} }
if a.Category != "counting" { if a.Category != "counting" {
@ -41,21 +41,21 @@ func TestAward(t *testing.T) {
} }
func TestAwardList(t *testing.T) { func TestAwardList(t *testing.T) {
a, _ := ParseAward("1536958399 1a2b3c4d counting 1") a, _ := ParseAward("1536958399 1a2b3c4d counting 1")
b, _ := ParseAward("1536958400 1a2b3c4d counting 1") b, _ := ParseAward("1536958400 1a2b3c4d counting 1")
c, _ := ParseAward("1536958300 1a2b3c4d counting 1") c, _ := ParseAward("1536958300 1a2b3c4d counting 1")
list := AwardList{a, b, c} list := AwardList{a, b, c}
if sort.IsSorted(list) { if sort.IsSorted(list) {
t.Error("Unsorted list thinks it's sorted") t.Error("Unsorted list thinks it's sorted")
} }
sort.Stable(list) sort.Stable(list)
if (list[0] != c) || (list[1] != a) || (list[2] != b) { if (list[0] != c) || (list[1] != a) || (list[2] != b) {
t.Error("Sorting didn't") t.Error("Sorting didn't")
} }
if ! sort.IsSorted(list) { if !sort.IsSorted(list) {
t.Error("Sorted list thinks it isn't") t.Error("Sorted list thinks it isn't")
} }
} }

View File

@ -3,16 +3,18 @@ package main
import ( import (
"log" "log"
"net/http" "net/http"
"strings"
"strconv" "strconv"
"strings"
) )
// HTTPServer is a MOTH HTTP server
type HTTPServer struct { type HTTPServer struct {
*http.ServeMux *http.ServeMux
server *MothServer server *MothServer
base string base string
} }
// NewHTTPServer creates a MOTH HTTP server, with handler functions registered
func NewHTTPServer(base string, server *MothServer) *HTTPServer { func NewHTTPServer(base string, server *MothServer) *HTTPServer {
base = strings.TrimRight(base, "/") base = strings.TrimRight(base, "/")
h := &HTTPServer{ h := &HTTPServer{
@ -28,6 +30,7 @@ func NewHTTPServer(base string, server *MothServer) *HTTPServer {
return h return h
} }
// HandleMothFunc binds a new handler function which creates a new MothServer with every request
func (h *HTTPServer) HandleMothFunc( func (h *HTTPServer) HandleMothFunc(
pattern string, pattern string,
mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request), mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request),
@ -36,10 +39,10 @@ func (h *HTTPServer) HandleMothFunc(
mh := h.server.NewHandler(req.FormValue("id")) mh := h.server.NewHandler(req.FormValue("id"))
mothHandler(mh, w, req) mothHandler(mh, w, req)
} }
h.HandleFunc(h.base + pattern, handler) h.HandleFunc(h.base+pattern, handler)
} }
// This gives Instances the signature of http.Handler // ServeHTTP provides the http.Handler interface
func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
w := MothResponseWriter{ w := MothResponseWriter{
statusCode: new(int), statusCode: new(int),
@ -55,21 +58,25 @@ func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
) )
} }
// MothResponseWriter provides a ResponseWriter that remembers what the status code was
type MothResponseWriter struct { type MothResponseWriter struct {
statusCode *int statusCode *int
http.ResponseWriter http.ResponseWriter
} }
// WriteHeader sends an HTTP response header with the provided status code
func (w MothResponseWriter) WriteHeader(statusCode int) { func (w MothResponseWriter) WriteHeader(statusCode int) {
*w.statusCode = statusCode *w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode) w.ResponseWriter.WriteHeader(statusCode)
} }
// Run binds to the provided bindStr, and serves incoming requests until failure
func (h *HTTPServer) Run(bindStr string) { func (h *HTTPServer) Run(bindStr string) {
log.Printf("Listening on %s", bindStr) log.Printf("Listening on %s", bindStr)
log.Fatal(http.ListenAndServe(bindStr, h)) log.Fatal(http.ListenAndServe(bindStr, h))
} }
// ThemeHandler serves up static content from the theme directory
func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
path := req.URL.Path path := req.URL.Path
if path == "/" { if path == "/" {
@ -85,10 +92,12 @@ func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter,
http.ServeContent(w, req, path, mtime, f) http.ServeContent(w, req, path, mtime, f)
} }
// StateHandler returns the full JSON-encoded state of the event
func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
JSONWrite(w, mh.ExportState()) JSONWrite(w, mh.ExportState())
} }
// RegisterHandler handles attempts to register a team
func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
teamName := req.FormValue("name") teamName := req.FormValue("name")
if err := mh.Register(teamName); err != nil { if err := mh.Register(teamName); err != nil {
@ -98,6 +107,7 @@ func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWrite
} }
} }
// AnswerHandler checks answer correctness and awards points
func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
cat := req.FormValue("cat") cat := req.FormValue("cat")
pointstr := req.FormValue("points") pointstr := req.FormValue("points")
@ -112,19 +122,20 @@ func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter,
} }
} }
// ContentHandler returns static content from a given puzzle
func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
trimLen := len(h.base) + len("/content/") trimLen := len(h.base) + len("/content/")
parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3) parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3)
if len(parts) < 3 { if len(parts) < 3 {
http.Error(w, "Not Found", http.StatusNotFound) http.Error(w, "Not Found", http.StatusNotFound)
return return
} }
cat := parts[0] cat := parts[0]
pointsStr := parts[1] pointsStr := parts[1]
filename := parts[2] filename := parts[2]
if (filename == "") { if filename == "" {
filename = "puzzles.json" filename = "puzzles.json"
} }
@ -137,5 +148,5 @@ func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter
} }
defer mf.Close() defer mf.Close()
http.ServeContent(w, req, filename, mtime, mf) http.ServeContent(w, req, filename, mtime, mf)
} }

View File

@ -2,10 +2,11 @@ package main
import ( import (
"flag" "flag"
"github.com/spf13/afero"
"log" "log"
"mime" "mime"
"time" "time"
"github.com/spf13/afero"
) )
func custodian(updateInterval time.Duration, components []Component) { func custodian(updateInterval time.Duration, components []Component) {
@ -17,7 +18,7 @@ func custodian(updateInterval time.Duration, components []Component) {
ticker := time.NewTicker(updateInterval) ticker := time.NewTicker(updateInterval)
update() update()
for _ = range ticker.C { for range ticker.C {
update() update()
} }
} }

View File

@ -1,10 +1,10 @@
package main package main
import ( import (
"io"
"time"
"fmt" "fmt"
"io"
"strconv" "strconv"
"time"
) )
type Category struct { type Category struct {
@ -25,7 +25,7 @@ type StateExport struct {
Messages string Messages string
TeamNames map[string]string TeamNames map[string]string
PointsLog []Award PointsLog []Award
Puzzles map[string][]int Puzzles map[string][]int
} }
type PuzzleProvider interface { type PuzzleProvider interface {
@ -49,30 +49,28 @@ type StateProvider interface {
Component Component
} }
type Component interface { type Component interface {
Update() Update()
} }
type MothServer struct { type MothServer struct {
Puzzles PuzzleProvider Puzzles PuzzleProvider
Theme ThemeProvider Theme ThemeProvider
State StateProvider State StateProvider
} }
func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer { func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer {
return &MothServer{ return &MothServer{
Puzzles: puzzles, Puzzles: puzzles,
Theme: theme, Theme: theme,
State: state, State: state,
} }
} }
func (s *MothServer) NewHandler(teamId string) MothRequestHandler { func (s *MothServer) NewHandler(teamId string) MothRequestHandler {
return MothRequestHandler{ return MothRequestHandler{
MothServer: s, MothServer: s,
teamId: teamId, teamId: teamId,
} }
} }
@ -135,13 +133,13 @@ func (mh *MothRequestHandler) ExportAllState() *StateExport {
export.PointsLog = make([]Award, len(pointsLog)) export.PointsLog = make([]Award, len(pointsLog))
for logno, award := range pointsLog { for logno, award := range pointsLog {
exportAward := *award exportAward := *award
if id, ok := exportIds[award.TeamId]; ok { if id, ok := exportIds[award.TeamID]; ok {
exportAward.TeamId = id exportAward.TeamID = id
} else { } else {
exportId := strconv.Itoa(logno) exportId := strconv.Itoa(logno)
name, _ := mh.State.TeamName(award.TeamId) name, _ := mh.State.TeamName(award.TeamID)
exportAward.TeamId = exportId exportAward.TeamID = exportId
exportIds[award.TeamId] = exportAward.TeamId exportIds[award.TeamID] = exportAward.TeamID
export.TeamNames[exportId] = name export.TeamNames[exportId] = name
} }
export.PointsLog[logno] = exportAward export.PointsLog[logno] = exportAward
@ -152,7 +150,6 @@ func (mh *MothRequestHandler) ExportAllState() *StateExport {
} }
} }
export.Puzzles = make(map[string][]int) export.Puzzles = make(map[string][]int)
for _, category := range mh.Puzzles.Inventory() { for _, category := range mh.Puzzles.Inventory() {
// Append sentry (end of puzzles) // Append sentry (end of puzzles)

View File

@ -3,13 +3,14 @@ package main
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"github.com/spf13/afero"
"log" "log"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/spf13/afero"
) )
// Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift // Stuff people with mediocre handwriting could write down unambiguously, and can be entered without holding down shift
@ -81,30 +82,30 @@ func (s *State) UpdateEnabled() {
} }
// Returns team name given a team ID. // Returns team name given a team ID.
func (s *State) TeamName(teamId string) (string, error) { func (s *State) TeamName(teamID string) (string, error) {
// XXX: directory traversal // XXX: directory traversal
teamFile := filepath.Join("teams", teamId) teamFile := filepath.Join("teams", teamID)
teamNameBytes, err := afero.ReadFile(s, teamFile) teamNameBytes, err := afero.ReadFile(s, teamFile)
teamName := strings.TrimSpace(string(teamNameBytes)) teamName := strings.TrimSpace(string(teamNameBytes))
if os.IsNotExist(err) { if os.IsNotExist(err) {
return "", fmt.Errorf("Unregistered team ID: %s", teamId) return "", fmt.Errorf("Unregistered team ID: %s", teamID)
} else if err != nil { } else if err != nil {
return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamId, err) return "", fmt.Errorf("Unregistered team ID: %s (%s)", teamID, err)
} }
return teamName, nil return teamName, nil
} }
// Write out team name. This can only be done once. // Write out team name. This can only be done once.
func (s *State) SetTeamName(teamId, teamName string) error { func (s *State) SetTeamName(teamID, teamName string) error {
if f, err := s.Open("teamids.txt"); err != nil { if f, err := s.Open("teamids.txt"); err != nil {
return fmt.Errorf("Team IDs file does not exist") return fmt.Errorf("Team IDs file does not exist")
} else { } else {
found := false found := false
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
for scanner.Scan() { for scanner.Scan() {
if scanner.Text() == teamId { if scanner.Text() == teamID {
found = true found = true
break break
} }
@ -115,7 +116,7 @@ func (s *State) SetTeamName(teamId, teamName string) error {
} }
} }
teamFile := filepath.Join("teams", teamId) teamFile := filepath.Join("teams", teamID)
err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644) err := afero.WriteFile(s, teamFile, []byte(teamName), os.ModeExclusive|0644)
if os.IsExist(err) { if os.IsExist(err) {
return fmt.Errorf("Team ID is already registered") return fmt.Errorf("Team ID is already registered")
@ -152,20 +153,20 @@ func (s *State) Messages() string {
return string(bMessages) return string(bMessages)
} }
// AwardPoints gives points to teamId in category. // AwardPoints gives points to teamID in category.
// It first checks to make sure these are not duplicate points. // It first checks to make sure these are not duplicate points.
// This is not a perfect check, you can trigger a race condition here. // This is not a perfect check, you can trigger a race condition here.
// It's just a courtesy to the user. // It's just a courtesy to the user.
// The update task makes sure we never have duplicate points in the log. // The update task makes sure we never have duplicate points in the log.
func (s *State) AwardPoints(teamId, category string, points int) error { func (s *State) AwardPoints(teamID, category string, points int) error {
a := Award{ a := Award{
When: time.Now().Unix(), When: time.Now().Unix(),
TeamId: teamId, TeamID: teamID,
Category: category, Category: category,
Points: points, Points: points,
} }
_, err := s.TeamName(teamId) _, err := s.TeamName(teamID)
if err != nil { if err != nil {
return err return err
} }
@ -176,7 +177,7 @@ func (s *State) AwardPoints(teamId, category string, points int) error {
} }
} }
fn := fmt.Sprintf("%s-%s-%d", teamId, category, points) fn := fmt.Sprintf("%s-%s-%d", teamID, category, points)
tmpfn := filepath.Join("points.tmp", fn) tmpfn := filepath.Join("points.tmp", fn)
newfn := filepath.Join("points.new", fn) newfn := filepath.Join("points.new", fn)

View File

@ -2,9 +2,10 @@ package main
import ( import (
"bytes" "bytes"
"github.com/spf13/afero"
"os" "os"
"testing" "testing"
"github.com/spf13/afero"
) )
func TestState(t *testing.T) { func TestState(t *testing.T) {
@ -56,7 +57,7 @@ func TestState(t *testing.T) {
pl = s.PointsLog() pl = s.PointsLog()
if len(pl) != 1 { if len(pl) != 1 {
t.Errorf("After awarding points, points log has length %d", len(pl)) t.Errorf("After awarding points, points log has length %d", len(pl))
} else if (pl[0].TeamId != teamId) || (pl[0].Category != category) || (pl[0].Points != points) { } else if (pl[0].TeamID != teamId) || (pl[0].Category != category) || (pl[0].Points != points) {
t.Errorf("Incorrect logged award %v", pl) t.Errorf("Incorrect logged award %v", pl)
} }

View File

@ -1,23 +1,31 @@
package main package main
import ( import (
"github.com/spf13/afero"
"io/ioutil" "io/ioutil"
"testing" "testing"
"github.com/spf13/afero"
) )
func TestTheme(t *testing.T) { func TestTheme(t *testing.T) {
filename := "/index.html"
fs := new(afero.MemMapFs) fs := new(afero.MemMapFs)
index := "this is the index" index := "this is the index"
afero.WriteFile(fs, "/index.html", []byte(index), 0644) afero.WriteFile(fs, filename, []byte(index), 0644)
fileInfo, err := fs.Stat(filename)
if err != nil {
t.Error(err)
}
s := NewTheme(fs) s := NewTheme(fs)
if f, err := s.Open("/index.html"); err != nil { if f, timestamp, err := s.Open("/index.html"); err != nil {
t.Error(err) t.Error(err)
} else if buf, err := ioutil.ReadAll(f); err != nil { } else if buf, err := ioutil.ReadAll(f); err != nil {
t.Error(err) t.Error(err)
} else if string(buf) != index { } else if string(buf) != index {
t.Error("Read wrong value from index") t.Error("Read wrong value from index")
} else if !timestamp.Equal(fileInfo.ModTime()) {
t.Error("Timestamp compared wrong")
} }
} }

9
cmd/transpile/go.mod Normal file
View File

@ -0,0 +1,9 @@
module github.com/russross/blackfriday/v2
go 1.13
require (
github.com/russross/blackfriday v2.0.0+incompatible
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
gopkg.in/yaml.v2 v2.3.0
)

7
cmd/transpile/go.sum Normal file
View File

@ -0,0 +1,7 @@
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -4,12 +4,13 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"gopkg.in/russross/blackfriday.v2"
"gopkg.in/yaml.v2"
"io" "io"
"log" "log"
"net/mail" "net/mail"
"strings" "strings"
"github.com/russross/blackfriday"
"gopkg.in/yaml.v2"
) )
type Attachment struct { type Attachment struct {
@ -55,7 +56,7 @@ func YamlParser(input []byte) (*Puzzle, error) {
return puzzle, nil return puzzle, nil
} }
func AttachmentParser(val []string) ([]Attachment) { func AttachmentParser(val []string) []Attachment {
ret := make([]Attachment, len(val)) ret := make([]Attachment, len(val))
for idx, txt := range val { for idx, txt := range val {
parts := strings.SplitN(txt, " ", 3) parts := strings.SplitN(txt, " ", 3)

5
go.mod
View File

@ -4,5 +4,10 @@ go 1.13
require ( require (
github.com/namsral/flag v1.7.4-pre github.com/namsral/flag v1.7.4-pre
github.com/pkg/sftp v1.11.0 // indirect
github.com/russross/blackfriday v2.0.0+incompatible // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spf13/afero v1.2.2 github.com/spf13/afero v1.2.2
golang.org/x/tools v0.0.0-20200814172026-c4923e618c08 // indirect
gopkg.in/yaml.v2 v2.3.0
) )

50
go.sum
View File

@ -1,6 +1,56 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs= github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs=
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo= github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=
github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yuin/goldmark v1.1.32 h1:5tjfNdR2ki3yYQ842+eX2sQHeiwpKJ0RnHO4IYOc4V8=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200814172026-c4923e618c08 h1:sfBQLM20fzeXhOixVQirwEbuW4PGStP773EXQpsBB6E=
golang.org/x/tools v0.0.0-20200814172026-c4923e618c08/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

BIN
mothballs/counting.mb Normal file

Binary file not shown.

View File

@ -18,20 +18,20 @@ function scoreboardInit() {
} }
let element = document.getElementById("rankings") let element = document.getElementById("rankings")
let teamNames = state.teams let teamNames = state.TeamNames
let pointsLog = state.points let pointsLog = state.PointsLog
// Every machine that's displaying the scoreboard helpfully stores the last 20 values of // Every machine that's displaying the scoreboard helpfully stores the last 20 values of
// points.json for us, in case of catastrophe. Thanks, y'all! // points.json for us, in case of catastrophe. Thanks, y'all!
// //
// We have been doing some variation on this "everybody backs up the server state" trick since 2009. // We have been doing some variation on this "everybody backs up the server state" trick since 2009.
// We have needed it 0 times. // We have needed it 0 times.
let pointsHistory = JSON.parse(localStorage.getItem("pointsHistory")) || [] let stateHistory = JSON.parse(localStorage.getItem("stateHistory")) || []
if (pointsHistory.length >= 20) { if (stateHistory.length >= 20) {
pointsHistory.shift() stateHistory.shift()
} }
pointsHistory.push(pointsLog) stateHistory.push(state)
localStorage.setItem("pointsHistory", JSON.stringify(pointsHistory)) localStorage.setItem("stateHistory", JSON.stringify(stateHistory))
let teams = {} let teams = {}
let highestCategoryScore = {} // map[string]int let highestCategoryScore = {} // map[string]int
@ -216,7 +216,7 @@ function scoreboardInit() {
} }
function refresh() { function refresh() {
fetch("points.json") fetch("state")
.then(resp => { .then(resp => {
return resp.json() return resp.json()
}) })