mirror of https://github.com/dirtbags/moth.git
Remove disabled, better hours.txt logs
This commit is contained in:
parent
c3a7ee0d4f
commit
887e4b3eaf
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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`
|
||||
|
|
Loading…
Reference in New Issue