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 {
// Unix epoch time of this event
When int64
TeamId string
TeamID string
Category string
Points int
}
@ -18,26 +18,25 @@ type AwardList []*Award
// Implement sort.Interface on AwardList
func (awards AwardList) Len() int {
return len(awards)
return len(awards)
}
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) {
tmp := awards[i]
awards[i] = awards[j]
awards[j] = tmp
tmp := awards[i]
awards[i] = awards[j]
awards[j] = tmp
}
func ParseAward(s string) (*Award, error) {
ret := Award{}
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 {
return nil, err
} else if n != 4 {
@ -48,7 +47,7 @@ func ParseAward(s string) (*Award, error) {
}
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) {
@ -57,7 +56,7 @@ func (a *Award) MarshalJSON() ([]byte, error) {
}
ao := []interface{}{
a.When,
a.TeamId,
a.TeamID,
a.Category,
a.Points,
}
@ -67,7 +66,7 @@ func (a *Award) MarshalJSON() ([]byte, error) {
func (a *Award) Same(o *Award) bool {
switch {
case a.TeamId != o.TeamId:
case a.TeamID != o.TeamID:
return false
case a.Category != o.Category:
return false

View File

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

View File

@ -3,16 +3,18 @@ package main
import (
"log"
"net/http"
"strings"
"strconv"
"strings"
)
// HTTPServer is a MOTH HTTP server
type HTTPServer struct {
*http.ServeMux
server *MothServer
base string
server *MothServer
base string
}
// NewHTTPServer creates a MOTH HTTP server, with handler functions registered
func NewHTTPServer(base string, server *MothServer) *HTTPServer {
base = strings.TrimRight(base, "/")
h := &HTTPServer{
@ -28,6 +30,7 @@ func NewHTTPServer(base string, server *MothServer) *HTTPServer {
return h
}
// HandleMothFunc binds a new handler function which creates a new MothServer with every request
func (h *HTTPServer) HandleMothFunc(
pattern string,
mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request),
@ -36,10 +39,10 @@ func (h *HTTPServer) HandleMothFunc(
mh := h.server.NewHandler(req.FormValue("id"))
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) {
w := MothResponseWriter{
statusCode: new(int),
@ -55,27 +58,31 @@ func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
)
}
// MothResponseWriter provides a ResponseWriter that remembers what the status code was
type MothResponseWriter struct {
statusCode *int
http.ResponseWriter
}
// WriteHeader sends an HTTP response header with the provided status code
func (w MothResponseWriter) WriteHeader(statusCode int) {
*w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
// Run binds to the provided bindStr, and serves incoming requests until failure
func (h *HTTPServer) Run(bindStr string) {
log.Printf("Listening on %s", bindStr)
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) {
path := req.URL.Path
if path == "/" {
path = "/index.html"
}
f, mtime, err := mh.ThemeOpen(path)
if err != nil {
http.NotFound(w, req)
@ -85,10 +92,12 @@ func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter,
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) {
JSONWrite(w, mh.ExportState())
}
// RegisterHandler handles attempts to register a team
func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
teamName := req.FormValue("name")
if err := mh.Register(teamName); err != nil {
@ -98,13 +107,14 @@ 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) {
cat := req.FormValue("cat")
pointstr := req.FormValue("points")
answer := req.FormValue("answer")
points, _ := strconv.Atoi(pointstr)
if err := mh.CheckAnswer(cat, points, answer); err != nil {
JSendf(w, JSendFail, "not accepted", err.Error())
} else {
@ -112,22 +122,23 @@ 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) {
trimLen := len(h.base) + len("/content/")
parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3)
if len(parts) < 3 {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
if len(parts) < 3 {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
cat := parts[0]
pointsStr := parts[1]
filename := parts[2]
if (filename == "") {
if filename == "" {
filename = "puzzles.json"
}
points, _ := strconv.Atoi(pointsStr)
mf, mtime, err := mh.PuzzlesOpen(cat, points, filename)
@ -137,5 +148,5 @@ func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter
}
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 (
"flag"
"github.com/spf13/afero"
"log"
"mime"
"time"
"github.com/spf13/afero"
)
func custodian(updateInterval time.Duration, components []Component) {
@ -17,7 +18,7 @@ func custodian(updateInterval time.Duration, components []Component) {
ticker := time.NewTicker(updateInterval)
update()
for _ = range ticker.C {
for range ticker.C {
update()
}
}

View File

@ -1,10 +1,10 @@
package main
import (
"io"
"time"
"fmt"
"io"
"strconv"
"time"
)
type Category struct {
@ -25,7 +25,7 @@ type StateExport struct {
Messages string
TeamNames map[string]string
PointsLog []Award
Puzzles map[string][]int
Puzzles map[string][]int
}
type PuzzleProvider interface {
@ -49,30 +49,28 @@ type StateProvider interface {
Component
}
type Component interface {
Update()
}
type MothServer struct {
Puzzles PuzzleProvider
Theme ThemeProvider
State StateProvider
Theme ThemeProvider
State StateProvider
}
func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer {
return &MothServer{
Puzzles: puzzles,
Theme: theme,
State: state,
Theme: theme,
State: state,
}
}
func (s *MothServer) NewHandler(teamId string) MothRequestHandler {
return MothRequestHandler{
MothServer: s,
teamId: teamId,
teamId: teamId,
}
}
@ -91,7 +89,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
return mh.Puzzles.Open(cat, points, path)
}
}
return nil, time.Time{}, fmt.Errorf("Puzzle locked")
}
@ -112,11 +110,11 @@ func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string)
if err := mh.Puzzles.CheckAnswer(cat, points, answer); err != nil {
return err
}
if err := mh.State.AwardPoints(mh.teamId, cat, points); err != nil {
return err
}
return nil
}
@ -124,7 +122,7 @@ func (mh *MothRequestHandler) ExportAllState() *StateExport {
export := StateExport{}
teamName, _ := mh.State.TeamName(mh.teamId)
export.Messages = mh.State.Messages()
export.TeamNames = map[string]string{"self": teamName}
@ -135,24 +133,23 @@ func (mh *MothRequestHandler) ExportAllState() *StateExport {
export.PointsLog = make([]Award, len(pointsLog))
for logno, award := range pointsLog {
exportAward := *award
if id, ok := exportIds[award.TeamId]; ok {
exportAward.TeamId = id
if id, ok := exportIds[award.TeamID]; ok {
exportAward.TeamID = id
} else {
exportId := strconv.Itoa(logno)
name, _ := mh.State.TeamName(award.TeamId)
exportAward.TeamId = exportId
exportIds[award.TeamId] = exportAward.TeamId
name, _ := mh.State.TeamName(award.TeamID)
exportAward.TeamID = exportId
exportIds[award.TeamID] = exportAward.TeamID
export.TeamNames[exportId] = name
}
export.PointsLog[logno] = exportAward
// Record the highest-value unlocked puzzle in each category
if award.Points > maxSolved[award.Category] {
maxSolved[award.Category] = award.Points
}
}
export.Puzzles = make(map[string][]int)
for _, category := range mh.Puzzles.Inventory() {
// Append sentry (end of puzzles)
@ -175,7 +172,7 @@ func (mh *MothRequestHandler) ExportAllState() *StateExport {
func (mh *MothRequestHandler) ExportState() *StateExport {
export := mh.ExportAllState()
// We don't give this out to just anybody,
// because back when we did,
// we got a bad reputation on some secretive blacklist,
@ -183,6 +180,6 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
if export.TeamNames["self"] == "" {
export.Puzzles = map[string][]int{}
}
return export
}

View File

@ -3,13 +3,14 @@ package main
import (
"bufio"
"fmt"
"github.com/spf13/afero"
"log"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/afero"
)
// 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.
func (s *State) TeamName(teamId string) (string, error) {
func (s *State) TeamName(teamID string) (string, error) {
// XXX: directory traversal
teamFile := filepath.Join("teams", teamId)
teamFile := filepath.Join("teams", teamID)
teamNameBytes, err := afero.ReadFile(s, teamFile)
teamName := strings.TrimSpace(string(teamNameBytes))
if os.IsNotExist(err) {
return "", fmt.Errorf("Unregistered team ID: %s", teamId)
return "", fmt.Errorf("Unregistered team ID: %s", teamID)
} 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
}
// 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 {
return fmt.Errorf("Team IDs file does not exist")
} else {
found := false
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if scanner.Text() == teamId {
if scanner.Text() == teamID {
found = true
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)
if os.IsExist(err) {
return fmt.Errorf("Team ID is already registered")
@ -152,20 +153,20 @@ func (s *State) Messages() string {
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.
// This is not a perfect check, you can trigger a race condition here.
// It's just a courtesy to the user.
// 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{
When: time.Now().Unix(),
TeamId: teamId,
TeamID: teamID,
Category: category,
Points: points,
}
_, err := s.TeamName(teamId)
_, err := s.TeamName(teamID)
if err != nil {
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)
newfn := filepath.Join("points.new", fn)

View File

@ -2,9 +2,10 @@ package main
import (
"bytes"
"github.com/spf13/afero"
"os"
"testing"
"github.com/spf13/afero"
)
func TestState(t *testing.T) {
@ -56,7 +57,7 @@ func TestState(t *testing.T) {
pl = s.PointsLog()
if len(pl) != 1 {
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)
}

View File

@ -1,23 +1,31 @@
package main
import (
"github.com/spf13/afero"
"io/ioutil"
"testing"
"github.com/spf13/afero"
)
func TestTheme(t *testing.T) {
filename := "/index.html"
fs := new(afero.MemMapFs)
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)
if f, err := s.Open("/index.html"); err != nil {
if f, timestamp, err := s.Open("/index.html"); err != nil {
t.Error(err)
} else if buf, err := ioutil.ReadAll(f); err != nil {
t.Error(err)
} else if string(buf) != 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"
"bytes"
"fmt"
"gopkg.in/russross/blackfriday.v2"
"gopkg.in/yaml.v2"
"io"
"log"
"net/mail"
"strings"
"github.com/russross/blackfriday"
"gopkg.in/yaml.v2"
)
type Attachment struct {
@ -55,7 +56,7 @@ func YamlParser(input []byte) (*Puzzle, error) {
return puzzle, nil
}
func AttachmentParser(val []string) ([]Attachment) {
func AttachmentParser(val []string) []Attachment {
ret := make([]Attachment, len(val))
for idx, txt := range val {
parts := strings.SplitN(txt, " ", 3)
@ -150,7 +151,7 @@ func ParseMoth(r io.Reader) (*Puzzle, error) {
if err != nil {
return nil, err
}
// Markdownify the body
bodyB := blackfriday.Run(bodyBuf.Bytes())
if (puzzle.Pre.Body != "") && (len(bodyB) > 0) {

5
go.mod
View File

@ -4,5 +4,10 @@ go 1.13
require (
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
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/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/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/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 teamNames = state.teams
let pointsLog = state.points
let teamNames = state.TeamNames
let pointsLog = state.PointsLog
// 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!
//
// We have been doing some variation on this "everybody backs up the server state" trick since 2009.
// We have needed it 0 times.
let pointsHistory = JSON.parse(localStorage.getItem("pointsHistory")) || []
if (pointsHistory.length >= 20) {
pointsHistory.shift()
let stateHistory = JSON.parse(localStorage.getItem("stateHistory")) || []
if (stateHistory.length >= 20) {
stateHistory.shift()
}
pointsHistory.push(pointsLog)
localStorage.setItem("pointsHistory", JSON.stringify(pointsHistory))
stateHistory.push(state)
localStorage.setItem("stateHistory", JSON.stringify(stateHistory))
let teams = {}
let highestCategoryScore = {} // map[string]int
@ -216,7 +216,7 @@ function scoreboardInit() {
}
function refresh() {
fetch("points.json")
fetch("state")
.then(resp => {
return resp.json()
})