From 887e4b3eaf27bdd9d5f7433132bccdf7d09d4304 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 29 Nov 2022 15:48:35 -0700 Subject: [PATCH] Remove disabled, better hours.txt logs --- cmd/mothd/state.go | 55 ++++++++++++++-------------- cmd/mothd/state_test.go | 54 +++++++++++++++++++--------- docs/FAQ.md | 79 +++++++++++++++++++++++++++++++++++++++++ docs/administration.md | 9 ++--- docs/overview.md | 21 +++-------- 5 files changed, 153 insertions(+), 65 deletions(-) create mode 100644 docs/FAQ.md diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index aa5a1f0..76fe13e 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -39,6 +39,7 @@ type State struct { // Enabled tracks whether the current State system is processing updates Enabled bool + enabledWhy string refreshNow chan bool eventStream chan []string eventWriter *csv.Writer @@ -71,11 +72,10 @@ func NewState(fs afero.Fs) *State { // updateEnabled checks a few things to see if this state directory is "enabled". func (s *State) updateEnabled() { nextEnabled := true - why := "`state/enabled` present, `state/hours.txt` missing" + why := "state/hours.txt has no timestamps before now" if untilFile, err := s.Open("hours.txt"); err == nil { defer untilFile.Close() - why = "`state/hours.txt` present" scanner := bufio.NewScanner(untilFile) for scanner.Scan() { @@ -95,35 +95,36 @@ func (s *State) updateEnabled() { case '#': continue default: - log.Println("Misformatted line in hours.txt file") + log.Println("state/hours.txt has bad line:", line) } + line, _, _ = strings.Cut(line, "#") // Remove inline comments line = strings.TrimSpace(line) - until, err := time.Parse(time.RFC3339, line) - if err != nil { - until, err = time.Parse(RFC3339Space, line) - } - if err != nil { - log.Println("Suspended: Unparseable until date:", line) + until := time.Time{} + if len(line) == 0 { + // Let it stay as zero time, so it's always before now + } else if until, err = time.Parse(time.RFC3339, line); err == nil { + // Great, it was RFC 3339 + } else if until, err = time.Parse(RFC3339Space, line); err == nil { + // Great, it was RFC 3339 with a space instead of a 'T' + } else { + log.Println("state/hours.txt has bad timestamp:", line) continue } if until.Before(time.Now()) { nextEnabled = thisEnabled + why = fmt.Sprint("state/hours.txt most recent timestamp:", line) } } } - if _, err := s.Stat("enabled"); os.IsNotExist(err) { - nextEnabled = false - why = "`state/enabled` missing" - } - - if nextEnabled != s.Enabled { + if (nextEnabled != s.Enabled) || (why != s.enabledWhy) { s.Enabled = nextEnabled - log.Printf("Setting enabled=%v: %s", s.Enabled, why) + s.enabledWhy = why + log.Printf("Setting enabled=%v: %s", s.Enabled, s.enabledWhy) if s.Enabled { - s.LogEvent("enabled", "", "", "", 0, why) + s.LogEvent("enabled", "", "", "", 0, s.enabledWhy) } else { - s.LogEvent("disabled", "", "", "", 0, why) + s.LogEvent("disabled", "", "", "", 0, s.enabledWhy) } } } @@ -350,21 +351,19 @@ func (s *State) maybeInitialize() { f.Close() } - if f, err := s.Create("enabled"); err == nil { - fmt.Fprintln(f, "enabled: remove or rename to suspend the contest.") - f.Close() - } - if f, err := s.Create("hours.txt"); err == nil { fmt.Fprintln(f, "# hours.txt: when the contest is enabled") fmt.Fprintln(f, "#") - fmt.Fprintln(f, "# Enable: + timestamp") - fmt.Fprintln(f, "# Disable: - timestamp") + fmt.Fprintln(f, "# Enable: + [timestamp]") + fmt.Fprintln(f, "# Disable: - [timestamp]") fmt.Fprintln(f, "#") - fmt.Fprintln(f, "# You can have multiple start/stop times.") - fmt.Fprintln(f, "# Whatever time is the most recent, wins.") - fmt.Fprintln(f, "# Times in the future are ignored.") + fmt.Fprintln(f, "# This file, and all files in this directory, are re-read periodically.") + fmt.Fprintln(f, "# Default is enabled.") + fmt.Fprintln(f, "# Rules with only '-' or '+' are also allowed.") + fmt.Fprintln(f, "# Rules apply from the top down.") + fmt.Fprintln(f, "# If you put something in out of order, it's going to be bonkers.") fmt.Fprintln(f) + fmt.Fprintln(f, "- 1970-01-01T00:00:00Z") fmt.Fprintln(f, "+", now) fmt.Fprintln(f, "- 2519-10-31T00:00:00Z") f.Close() diff --git a/cmd/mothd/state_test.go b/cmd/mothd/state_test.go index 21f1ea2..0101dec 100644 --- a/cmd/mothd/state_test.go +++ b/cmd/mothd/state_test.go @@ -17,8 +17,16 @@ func NewTestState() *State { return s } +func slurp(c chan bool) { + for range c { + // Nothing + } +} + func TestState(t *testing.T) { s := NewTestState() + defer close(s.refreshNow) + go slurp(s.refreshNow) mustExist := func(path string) { _, err := s.Fs.Stat(path) @@ -33,7 +41,6 @@ func TestState(t *testing.T) { } mustExist("initialized") - mustExist("enabled") mustExist("hours.txt") teamIDsBuf, err := afero.ReadFile(s.Fs, "teamids.txt") @@ -163,6 +170,9 @@ func TestStateEvents(t *testing.T) { if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "init::::0" { t.Error("Wrong message from event stream:", msg) } + if msg := <-s.eventStream; !strings.HasPrefix(msg[6], "state/hours.txt") { + t.Error("Wrong message from event stream:", msg[6]) + } if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo::::0" { t.Error("Wrong message from event stream:", msg) } @@ -184,19 +194,37 @@ func TestStateDisabled(t *testing.T) { t.Error(err) } defer hoursFile.Close() + s.refresh() + if !s.Enabled { + t.Error("Empty hours.txt not enabled") + } fmt.Fprintln(hoursFile, "- 1970-01-01T01:01:01Z") hoursFile.Sync() s.refresh() if s.Enabled { - t.Error("Disabling 1970-01-01") + t.Error("1970-01-01") } - fmt.Fprintln(hoursFile, "+ 1970-01-01 01:01:01+05:00") + fmt.Fprintln(hoursFile, "+ 1970-01-02 01:01:01+05:00") hoursFile.Sync() s.refresh() if !s.Enabled { - t.Error("Enabling 1970-01-02") + t.Error("1970-01-02") + } + + fmt.Fprintln(hoursFile, "-") + hoursFile.Sync() + s.refresh() + if s.Enabled { + t.Error("bare -") + } + + fmt.Fprintln(hoursFile, "+") + hoursFile.Sync() + s.refresh() + if !s.Enabled { + t.Error("bare +") } fmt.Fprintln(hoursFile, "") @@ -204,7 +232,7 @@ func TestStateDisabled(t *testing.T) { hoursFile.Sync() s.refresh() if !s.Enabled { - t.Error("Comments") + t.Error("Comment") } fmt.Fprintln(hoursFile, "intentional parse error") @@ -218,7 +246,7 @@ func TestStateDisabled(t *testing.T) { hoursFile.Sync() s.refresh() if s.Enabled { - t.Error("Disabling 1980-01-01") + t.Error("1980-01-01") } if err := s.Remove("hours.txt"); err != nil { @@ -229,14 +257,6 @@ func TestStateDisabled(t *testing.T) { t.Error("Removing `hours.txt` disabled event") } - if err := s.Remove("enabled"); err != nil { - t.Error(err) - } - s.refresh() - if s.Enabled { - t.Error("Removing `enabled` didn't disable") - } - s.Remove("initialized") s.refresh() if !s.Enabled { @@ -291,11 +311,11 @@ func TestStateMaintainer(t *testing.T) { eventLog, err := afero.ReadFile(s.Fs, "events.csv") if err != nil { t.Error(err) - } else if events := strings.Split(string(eventLog), "\n"); len(events) != 3 { + } else if events := strings.Split(string(eventLog), "\n"); len(events) != 4 { t.Log("Events:", events) t.Error("Wrong event log length:", len(events)) - } else if events[2] != "" { - t.Error("Event log didn't end with newline") + } else if events[3] != "" { + t.Error("Event log didn't end with newline", events) } } diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..4ea7496 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,79 @@ +Frequently Asked Questions +================= + +I should probably move this somewhere else, +since most of it is about + +Main Application Questions +===================== + +## Can we add some instructions to the user interface? It's confusing. + +The lack of instruction was a deliberate design decision made about 9 years ago +when we found in A/B testing that college students are a lot more motivated by +vague instruction and mystery than precise instruction. We've since found that +people who are inclined to "play" our events are similarly motivated by +weirdness and mystery: they enjoy fiddling around with things until they've +worked it out experimentally. + +Oddly, the group who seems to be the most perturbed by the vagueness is +professionals. This may be because many of these folks spend long amounts of +time trying to make things accessible and precise, and this looks like a train +wreck from that perspective. + +Another way to think about it: this is supposed to be a game, like Super Mario +Brothers. We were very careful about designing the puzzles so that you could +learn by playing. The whimsical design is meant to make it feel like trying +things out will not result in a catastrophic failure anywhere, and we've found +that most people figure it out very quickly without any instruction at all, +despite feeling a little confused or disoriented at first. + +## Why can't I choose my team from a list when I log in? + +We actually started this way, but we quickly learned that there were exploitable +attack avenues available when any participant can join any team. One individual +in 2010, having a bad day, decided to enter every answer they had, for every +team in the contest, as a way of sabotaging the event. It worked: everyone's +motivation to try and solve puzzles tanked, and people were angry that they'd +been working on content only to find that they already had the points. + +## Why won't you add this helpful text to the login page? + +It has been our experience that the more words we have on that page, the less +likely any of them will be read. We strive now to have no instruction at all, +and to design the interface in a way that it's obvious what you have to do. + +## Why aren't we providing a link to the scoreboard? + +It's because the scoreboard looks horrible on a mobile phone: +it was designed for a projector. +Once we have a scoreboard that is readable on mobile, +I'll add that link. + +## Why can't we show a list of teams to log in to? + +At a previous event, +we had a participant log in as other teams and solve every puzzle, +because they were upset about something. +This ruined the event for everyone, +because it took away the challenge of scoring points. + + +Scoreboard Questions +================= + +## Why are there no links or title on the scoreboard? + +The scoreboard is supposed to be projected at events, to participants. The current scoreboard isn't something we intend participants to pull up on their mobile devices or laptops. + +Think of the scoreboard as sort of like the menu screens at Burger King. + + +## Will you change the scoreboard color scheme? + +The scoreboard colors and layout were carefully chosen to be distinguishable for +all forms of color blindness, and even accessible by users with total blindness +using screen readers. This is why we decided to put the category name inside the +bar and just deal with it being a little weird. + +I'm open to suggestions, but they need to work for all users. diff --git a/docs/administration.md b/docs/administration.md index 5690373..a95d5cb 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -45,8 +45,8 @@ Scores Pausing/resuming scoring ------------------- - rm /srv/moth/state/enabled # Pause scoring - touch /srv/moth/state/enabled # Resume scoring + echo '-###' >> /srv/moth/state/hours.txt # Suspend scoring + sed -i '/###/d' /srv/moth/state/hours.txt # Resume scoring When scoring is paused, participants can still submit answers, @@ -54,12 +54,13 @@ and the system will tell them whether the answer is correct. As soon as you unpause, all correctly-submitted answers will be scored. + Adjusting scores ------------------ - rm /srv/moth/state/enabled # Suspend scoring + echo '-###' >> /srv/moth/state/hours.txt # Suspend scoring nano /srv/moth/state/points.log # Replace nano with your preferred editor - touch /srv/moth/state/enabled # Resume scoring + sed -i '/###/d' /srv/moth/state/hours.txt # Resume scoring We don't warn participants before we do this: any points scored while scoring is suspended are queued up and processed as soon as scoreing is resumed. diff --git a/docs/overview.md b/docs/overview.md index 5242aed..ec722cb 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -33,24 +33,13 @@ The state directory is also used to communicate actions to mothd. Remove this file to reset the state. This will blow away team assignments and the points log. -`disabled` ----------- - -Create this file to pause collection of points and other maintenance. -Contestants can still submit answers, -but they won't show up on the scoreboard until you remove this file. - -This file does not normally exist. - - -`until` +`hours.txt` ------- -Put an RFC3337 date/time stamp in here to have the server pause itself at a given time. -Remember that time zones exist! -I recommend always using Zulu time. - -This file does not normally exist. +A list of start and stop hours. +If all the hours are in the future, the event defaults to running. +"Stop" here just pertains to scoreboard updates and puzzle unlocking. +People can still submit answers and their awards are queued up for the next start. `teamids.txt`