mirror of https://github.com/dirtbags/moth.git
no more state/disabled, better logs for hours.txt
This commit is contained in:
parent
ec2a547637
commit
bfcf325e3b
|
@ -43,6 +43,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
|
||||
|
@ -75,11 +76,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() {
|
||||
|
@ -99,35 +99,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -361,21 +362,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()
|
||||
|
|
|
@ -41,7 +41,6 @@ func TestState(t *testing.T) {
|
|||
}
|
||||
|
||||
mustExist("initialized")
|
||||
mustExist("enabled")
|
||||
mustExist("hours.txt")
|
||||
|
||||
teamIDsBuf, err := afero.ReadFile(s.Fs, "teamids.txt")
|
||||
|
@ -175,6 +174,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[5], "state/hours.txt") {
|
||||
t.Error("Wrong message from event stream:", msg[5])
|
||||
}
|
||||
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo:::0" {
|
||||
t.Error("Wrong message from event stream:", msg)
|
||||
}
|
||||
|
@ -196,19 +198,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, "")
|
||||
|
@ -216,7 +236,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")
|
||||
|
@ -230,7 +250,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 {
|
||||
|
@ -241,14 +261,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 {
|
||||
|
@ -303,11 +315,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`
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
I know you've done some work on this already but I have to get this idea down someplace.
|
||||
|
||||
I would like to, as much as possible, not have the same data in two different places. So here are my proposals for doing this in the filesystem backend.
|
||||
|
||||
We could implement all of these at the same time, although I don't know why we'd want to
|
||||
|
||||
### The obvious way
|
||||
* [ ] Make the `teams` directory full of directories again and not files
|
||||
* [ ] Move team names to `teams/$id/name`
|
||||
* [ ] Define a new file called `teams/$id/map` which contains an ID that this maps to. It will only be followed once.
|
||||
* [ ] When writing anything with team id, resolve any maps first
|
||||
|
||||
### The clever way
|
||||
* [ ] Allow `teams/$id` to be a symbolic link to another team file
|
||||
* [ ] When writing anything with team id, resolve any symlinks first
|
||||
|
||||
### The easy way
|
||||
|
||||
* [ ] Line 2 of a team file, optionally, contains a team id to map to
|
||||
* [ ] When writing anything with a team id, resolve any maps
|
Loading…
Reference in New Issue