Compare commits

..
5 Commits
Author SHA1 Message Date
Eric WagonerandClaude Opus 5.5 98e61dc756 The hosting line says what it promises: chipping in unlocks nothing, and nothing is held back without it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbtUPJ24P6vc6tE1x6RS3m
2026-10-13 09:23:44 -04:00
Eric WagonerandClaude Fable 5.1 60435fef84 The hall keeps the night on the shared host: one job runs every game's rollup and backup and checks in with Sentry once for each
Eight cron monitors, two per game, watched eight scripts that run on the
same box at the same two minutes. hall-nightly.sh now runs the hall's own
rollup and every game's rollup at 00:12, and every game's backup at 07:23,
finding the games by their scripts in /usr/local/bin, and checks in once
for each job to the gamehall project's hall-rollup and hall-backup
monitors. The template's rollup and backup say by their exit status
whether they managed, the skipped backup included, and its deploy leaves
the game's own cron file off a host the hall schedules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-12 10:07:05 -04:00
Eric WagonerandClaude Fable 5.1 87c2215e5f The hall's cards say who plays on one line and where the game came from on a quieter one, with a word for newcomers under the intro
Each card's players and origin are two lines, the origin a step
smaller. Dip's description is shorter and its players line says "2 to
10, depending on the map". The intro says "start a table" like the list
beneath it, and a line under it points newcomers at Wiz-War. The Waving
Hands picture is re-cropped from the same capture, the grid centered
and the stray edge gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 20:59:53 -04:00
Eric WagonerandClaude Fable 5.1 7301e98802 The hall's Waving Hands card shows the duel in hands, not letters
The lobby's four-turn grid, captured from the game with each letter
replaced by the game's own hand silhouette, mirrored for the right hand
as the picker draws it. The game itself still writes the grid in letters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 20:48:25 -04:00
Eric WagonerandClaude Fable 5.1 c162625d51 The hall's cards name each game's house player and lead with "Play Dip →", Waving Hands shows its gesture grid, and the small text reads at 4.5:1
The intro says how to start before the cards. Each card's metadata line
names the house player, the call to action is "Play [Game] →" with the
game's address beneath, and an empty length line waits, hidden, for real
play data. Waving Hands' card shows the lobby's gesture grid instead of
the share card. "How a table works" says Start a table and how a seat
crosses devices, and the credits run in the cards' order. The faint bone
is gone: the metadata lines and the footer use the dimmed bone, 6.5:1
and 7.2:1 where they were 3.7:1 and 4.0:1, and the ko-fi link no longer
wraps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 20:41:55 -04:00
14 changed files with 148 additions and 40 deletions

No files matched your search

+1 -1
View File
@@ -23,7 +23,7 @@ name (the sslip test address is left out) and prints:
own digest (/wizwar-visitors) lists gamehall.kestrelsnest.social among own digest (/wizwar-visitors) lists gamehall.kestrelsnest.social among
its referrers. its referrers.
- **last 7 days** from `/var/lib/gamehall/rollup.jsonl`, written nightly - **last 7 days** from `/var/lib/gamehall/rollup.jsonl`, written nightly
at 00:05 UTC by `gamehall-rollup.sh` for the day before (counts only, at 00:12 UTC by `gamehall-rollup.sh` (run by `hall-nightly.sh`) for the day before (counts only,
no addresses). no addresses).
## Read it ## Read it
+5 -4
View File
@@ -86,8 +86,8 @@ README.
- **The hosting line** follows the family list in the about panel: the - **The hosting line** follows the family list in the about panel: the
games are free and will stay free, the servers cost a few dollars a games are free and will stay free, the servers cost a few dollars a
month, and ko-fi.com/kestrelsnest is where anyone may chip in. It says month, and ko-fi.com/kestrelsnest is where anyone may chip in. It says
every game stays whole either way, and it never names a game's rights chipping in unlocks nothing and nothing is held back without it, and it
holder: a tip is for the hosting, not the never names a game's rights holder: a tip is for the hosting, not the
game. game.
- **Preferences** are what a browser remembers about its player: a panel - **Preferences** are what a browser remembers about its player: a panel
@@ -198,8 +198,9 @@ Every game gets the same tools, installed by `deploy.sh` on every deploy:
the rollup's last week, the backup's last word, the box's vitals. the rollup's last week, the backup's last word, the box's vitals.
- `<slug>-rollup.sh` nightly at 00:12 UTC: one JSON line a day of counts, - `<slug>-rollup.sh` nightly at 00:12 UTC: one JSON line a day of counts,
no addresses. `<slug>-backup.sh` nightly at 07:23 UTC: rclone to the no addresses. `<slug>-backup.sh` nightly at 07:23 UTC: rclone to the
shared Space, mirror plus dated snapshots pruned after 90 days. Both check shared Space, mirror plus dated snapshots pruned after 90 days. On the
in with Sentry Crons so a missed night is noticed. shared host the hall's `hall-nightly.sh` runs both for every game and
checks in with Sentry Crons once for all, so a missed night is noticed.
- The reports desk (skill `<slug>-reports`): `pull-reports.sh` mirrors the - The reports desk (skill `<slug>-reports`): `pull-reports.sh` mirrors the
desk to the Desktop with a digest; `report-reply.sh <ip> <id> <status> desk to the Desktop with a digest; `report-reply.sh <ip> <id> <status>
"text"` answers with `resolved`, `by-design` or `open`. A fix goes live "text"` answers with `resolved`, `by-design` or `open`. A fix goes live
+8
View File
@@ -115,3 +115,11 @@ nightly rollup line in /var/lib/gamehall/rollup.jsonl. The skill
`gamehall-visitors` reads them. A new game on the shared host is added to `gamehall-visitors` reads them. A new game on the shared host is added to
`GAMES` in `gamehall/ops/traffic.py` so the digest counts the people the `GAMES` in `gamehall/ops/traffic.py` so the digest counts the people the
hall sends it. hall sends it.
The hall also keeps the night on the shared host: `hall-nightly.sh rollup`
at 00:12 UTC and `hall-nightly.sh backup` at 07:23 UTC (`gamehall.cron`)
run the hall's own rollup and every game's rollup and backup found in
/usr/local/bin, and check in with Sentry Crons once for each job, monitors
`hall-rollup` and `hall-backup` in the `gamehall` project, from the URLs in
/root/.hall-sentry-cron-rollup and /root/.hall-sentry-cron-backup. A game's
own cron file and check-ins are for a box of its own.
+2
View File
@@ -21,6 +21,8 @@ ssh "root@$HOST" '
# The counting tools: installed on every publish so the live copies are the repo copies. # The counting tools: installed on every publish so the live copies are the repo copies.
install -m 755 /tmp/gamehall-ops/gamehall-visitors.sh /usr/local/bin/gamehall-visitors.sh install -m 755 /tmp/gamehall-ops/gamehall-visitors.sh /usr/local/bin/gamehall-visitors.sh
install -m 755 /tmp/gamehall-ops/gamehall-rollup.sh /usr/local/bin/gamehall-rollup.sh install -m 755 /tmp/gamehall-ops/gamehall-rollup.sh /usr/local/bin/gamehall-rollup.sh
# The nightly job that runs the rollup and backup of every game here and checks in once for all.
install -m 755 /tmp/gamehall-ops/hall-nightly.sh /usr/local/bin/hall-nightly.sh
install -m 644 -D /tmp/gamehall-ops/traffic.py /usr/local/lib/gamehall/traffic.py install -m 644 -D /tmp/gamehall-ops/traffic.py /usr/local/lib/gamehall/traffic.py
install -m 644 /tmp/gamehall-ops/gamehall.cron /etc/cron.d/gamehall install -m 644 /tmp/gamehall-ops/gamehall.cron /etc/cron.d/gamehall
# The hall-wide notice every game serves at /hall/announcement.json. # The hall-wide notice every game serves at /hall/announcement.json.
+1 -1
View File
@@ -4,7 +4,7 @@
# and the people sent on to each game here). Counts only: no addresses are # and the people sent on to each game here). Counts only: no addresses are
# kept, so the trend outlives the self-rotating access log. # kept, so the trend outlives the self-rotating access log.
# gamehall-rollup.sh [YYYY-MM-DD] (default: yesterday, UTC) # gamehall-rollup.sh [YYYY-MM-DD] (default: yesterday, UTC)
# Cron: /etc/cron.d/gamehall, installed by gamehall/deploy.sh. # Run nightly by hall-nightly.sh rollup (/etc/cron.d/gamehall).
set -u set -u
OUT="/var/lib/gamehall/rollup.jsonl" OUT="/var/lib/gamehall/rollup.jsonl"
LOG="/var/log/gamehall/rollup.log" LOG="/var/log/gamehall/rollup.log"
+4 -2
View File
@@ -1,2 +1,4 @@
# Kestrel's Hall: nightly rollup of yesterday's counts. UTC. # Kestrel's Hall: the nightly work of every game on this host, and the hall's own count. UTC.
5 0 * * * root /usr/local/bin/gamehall-rollup.sh # The times are also SCHEDULE in hall-nightly.sh, which tells Sentry to expect them; change both.
12 0 * * * root /usr/local/bin/hall-nightly.sh rollup
23 7 * * * root /usr/local/bin/hall-nightly.sh backup
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# The hall's nightly work for every game on this host, with one Sentry Crons
# check-in for the lot: the rollups just after midnight, the backups at
# breakfast. Each game's own script does its work and says by its exit
# status whether it managed; the games are found by their scripts in
# /usr/local/bin, so a game deployed here is covered without being listed.
# hall-nightly.sh rollup|backup
# Cron: /etc/cron.d/gamehall, installed by gamehall/deploy.sh. SCHEDULE
# below must match it, because Sentry is told the same shape.
# Sentry Crons check-in: /root/.hall-sentry-cron-<job> holds the check-in
# URL (absent = no check-ins).
set -u
JOB="${1:-}"
case "$JOB" in
rollup) SCHEDULE="12 0 * * *" ;;
backup) SCHEDULE="23 7 * * *" ;;
*) echo "usage: hall-nightly.sh rollup|backup" >&2; exit 2 ;;
esac
LOG="/var/log/gamehall/nightly.log"
CRON_URL=$(cat "/root/.hall-sentry-cron-$JOB" 2>/dev/null || true)
# One check-in, when the run ends, carrying its duration and the schedule
# (Sentry keeps a monitor only once told its shape). A start and a finish
# sent seconds apart can reach Sentry in either order, and a start that lands
# second is never closed. A run that never ends sends nothing, and Sentry
# marks the night missed.
START=$SECONDS
checkin() {
[ -n "$CRON_URL" ] && curl -sf -o /dev/null -X POST -H "Content-Type: application/json" \
-d "{\"status\":\"$1\",\"duration\":$((SECONDS - START)),\"monitor_config\":{\"schedule\":{\"type\":\"crontab\",\"value\":\"$SCHEDULE\"},\"checkin_margin\":30,\"max_runtime\":30,\"timezone\":\"UTC\"}}" \
"$CRON_URL" || true
}
mkdir -p "$(dirname "$LOG")"
# The hall's own count first, then each game's script, in name order.
scripts=()
[ "$JOB" = rollup ] && scripts+=(/usr/local/bin/gamehall-rollup.sh)
for s in /usr/local/bin/*-"$JOB".sh; do
[ "$s" = /usr/local/bin/gamehall-rollup.sh ] || scripts+=("$s")
done
failed=()
for s in "${scripts[@]}"; do
name=$(basename "$s" "-$JOB.sh")
"$s" || failed+=("$name")
done
if [ ${#failed[@]} -eq 0 ]; then
echo "$(date -Is) $JOB: ${#scripts[@]} done" >> "$LOG"
checkin ok
else
echo "$(date -Is) $JOB: ${#scripts[@]} run, failed: ${failed[*]}" >> "$LOG"
checkin error
exit 1
fi
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 33 KiB

+47 -20
View File
@@ -22,7 +22,6 @@
--line: #2c3236; --line: #2c3236;
--bone: #e6dfcf; --bone: #e6dfcf;
--bone-dim: #a9a494; --bone-dim: #a9a494;
--bone-faint: #7c7868;
--gold: #d2a94a; --gold: #d2a94a;
--gutter: 16px; --gutter: 16px;
} }
@@ -56,6 +55,7 @@
color: var(--bone-dim); color: var(--bone-dim);
} }
header.mast p.lede b { color: var(--bone); font-weight: 500; } header.mast p.lede b { color: var(--bone); font-weight: 500; }
header.mast p.newcomer { margin: 10px auto 0; max-width: 38em; font-size: 0.95rem; color: var(--bone-dim); }
.games { display: grid; gap: 22px; grid-template-columns: repeat(2, 1fr); margin-top: 22px; } .games { display: grid; gap: 22px; grid-template-columns: repeat(2, 1fr); margin-top: 22px; }
@media (max-width: 860px) { .games { grid-template-columns: 1fr; max-width: 520px; margin-left: auto; margin-right: auto; } } @media (max-width: 860px) { .games { grid-template-columns: 1fr; max-width: 520px; margin-left: auto; margin-right: auto; } }
@@ -73,14 +73,18 @@
.game .art { position: relative; aspect-ratio: 5 / 3; background: #222; overflow: hidden; } .game .art { position: relative; aspect-ratio: 5 / 3; background: #222; overflow: hidden; }
.game .art img.fill { width: 100%; height: 100%; object-fit: cover; display: block; } .game .art img.fill { width: 100%; height: 100%; object-fit: cover; display: block; }
.game .art img.tafl { object-position: 50% 50%; } .game .art img.tafl { object-position: 50% 50%; }
.game .art img.hands { object-position: 0% 50%; }
.game .art img.dip { object-position: 50% 80%; } .game .art img.dip { object-position: 50% 80%; }
.game .body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; } .game .body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
.game h2 { margin: 0; font-size: 1.45rem; font-weight: 500; } .game h2 { margin: 0; font-size: 1.45rem; font-weight: 500; }
.game .pitch { margin: 0; color: var(--bone); } .game .pitch { margin: 0; color: var(--bone); }
.game .facts { margin: 0; color: var(--bone-faint); font-size: 0.92rem; } .game .players, .game .length { margin: 0; color: var(--bone-dim); font-size: 0.92rem; }
.game .play { margin-top: auto; padding-top: 10px; color: var(--gold); font-size: 1rem; } .game .origin { margin: 0; color: var(--bone-dim); font-size: 0.85rem; }
.game .length:empty { display: none; }
.game .go { margin-top: auto; padding-top: 10px; display: flex; flex-direction: column; gap: 2px; }
.game .play { color: var(--gold); font-size: 1rem; }
.game .site { color: var(--bone-dim); font-size: 0.85rem; }
.game:hover .play { text-decoration: underline; } .game:hover .play { text-decoration: underline; }
.nowrap { white-space: nowrap; }
/* The maze card: a stone floor in CSS, mortar-coloured walls, and the hall's own token drawings. */ /* The maze card: a stone floor in CSS, mortar-coloured walls, and the hall's own token drawings. */
.maze { .maze {
@@ -111,7 +115,7 @@
.keeper img { width: 84px; height: 84px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; border: 2px solid var(--line); } .keeper img { width: 84px; height: 84px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; border: 2px solid var(--line); }
@media (max-width: 480px) { .keeper { flex-direction: column; } } @media (max-width: 480px) { .keeper { flex-direction: column; } }
footer { margin-top: 64px; padding: 28px 0 48px; border-top: 1px solid var(--line); color: var(--bone-faint); font-size: 0.9rem; } footer { margin-top: 64px; padding: 28px 0 48px; border-top: 1px solid var(--line); color: var(--bone-dim); font-size: 0.9rem; }
footer p { margin: 0 0 8px; } footer p { margin: 0 0 8px; }
/* The hall-wide notice: its own colors, the same in every game of the hall. */ /* The hall-wide notice: its own colors, the same in every game of the hall. */
.hall-announcement { position: sticky; top: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; gap: .8rem; padding: .45rem 2.4rem .45rem 1rem; background: #f2c45a; color: #1d1a12; font: 500 .95rem/1.35 system-ui, -apple-system, 'Segoe UI', sans-serif; text-align: center; box-shadow: 0 1px 4px #0005; } .hall-announcement { position: sticky; top: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; gap: .8rem; padding: .45rem 2.4rem .45rem 1rem; background: #f2c45a; color: #1d1a12; font: 500 .95rem/1.35 system-ui, -apple-system, 'Segoe UI', sans-serif; text-align: center; box-shadow: 0 1px 4px #0005; }
@@ -123,10 +127,13 @@
<header class="mast"> <header class="mast">
<h1>Kestrel's Hall</h1> <h1>Kestrel's Hall</h1>
<p class="lede"><b>Free board games in the browser.</b> Play with friends by sending a link, or alone against the house. No accounts, no adverts, no timers, and nothing sold.</p> <p class="lede"><b>Free board games in the browser.</b> Play alone against the house right away, or start a table and send friends the link. No accounts, no adverts, no timers, and nothing sold.</p>
<!-- A word for newcomers, one line to tune or remove. -->
<p class="newcomer">New here? Wiz-War is the friendliest place to start, and a game can be over in minutes. Dip is the long game.</p>
</header> </header>
<div class="games"> <div class="games">
<!-- Each card's empty "length" line is for a typical game's length, once real play has shown it; it is hidden until filled. -->
<a class="game" href="https://wizwar.kestrelsnest.social"> <a class="game" href="https://wizwar.kestrelsnest.social">
<div class="art"> <div class="art">
@@ -150,18 +157,28 @@
<div class="body"> <div class="body">
<h2>Wiz-War</h2> <h2>Wiz-War</h2>
<p class="pitch">Steal treasure. Blast through walls. Outwit your friends.</p> <p class="pitch">Steal treasure. Blast through walls. Outwit your friends.</p>
<p class="facts">2 to 6 players · an unofficial rebuild of Tom Jolly's 1993 board game</p> <p class="players">2 to 6 players, or solo against the automaton</p>
<span class="play">Play at wizwar.kestrelsnest.social →</span> <p class="origin">An unofficial rebuild of Tom Jolly's 1993 board game</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Wiz-War →</span>
<span class="site">wizwar.kestrelsnest.social</span>
</div>
</div> </div>
</a> </a>
<a class="game" href="https://hands.kestrelsnest.social"> <a class="game" href="https://hands.kestrelsnest.social">
<div class="art"><img class="fill hands" src="hands.png" alt="The Waving Hands share card: a column of gesture letters beside the title."></div> <div class="art"><img class="fill" src="hands.png" alt="Four turns of a Waving Hands duel: each row a turn, each hand drawn in its gesture, and a spell named under the gesture that finished it."></div>
<div class="body"> <div class="body">
<h2>Waving Hands</h2> <h2>Waving Hands</h2>
<p class="pitch">Wizards duel with their hands, and the right run of gestures is a spell.</p> <p class="pitch">Wizards duel with their hands, and the right run of gestures is a spell.</p>
<p class="facts">2 to 12 players · Richard Bartle's pencil-and-paper duel, 1977</p> <p class="players">2 to 12 players, or solo against a rival wizard</p>
<span class="play">Play at hands.kestrelsnest.social →</span> <p class="origin">Richard Bartle's pencil-and-paper duel, 1977</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Waving Hands →</span>
<span class="site">hands.kestrelsnest.social</span>
</div>
</div> </div>
</a> </a>
@@ -170,8 +187,13 @@
<div class="body"> <div class="body">
<h2>Hnefatafl</h2> <h2>Hnefatafl</h2>
<p class="pitch">The Viking board game of a king breaking for safety and a ring of attackers closing in.</p> <p class="pitch">The Viking board game of a king breaking for safety and a ring of attackers closing in.</p>
<p class="facts">2 players · the Norse tafl game, by the Copenhagen rules or Linnaeus's 1732 Tablut</p> <p class="players">2 players, or solo against the housecarl</p>
<span class="play">Play at tafl.kestrelsnest.social →</span> <p class="origin">The Norse tafl game, by the Copenhagen rules or Linnaeus's 1732 Tablut</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Hnefatafl →</span>
<span class="site">tafl.kestrelsnest.social</span>
</div>
</div> </div>
</a> </a>
@@ -179,9 +201,14 @@
<div class="art"><img class="fill dip" src="dip.jpg" alt="Dip's map of Europe at the opening: the seven powers' armies and fleets on their home centers, each power's lands in its color."></div> <div class="art"><img class="fill dip" src="dip.jpg" alt="Dip's map of Europe at the opening: the seven powers' armies and fleets on their home centers, each power's lands in its color."></div>
<div class="body"> <div class="body">
<h2>Dip</h2> <h2>Dip</h2>
<p class="pitch">Seven powers share one map, and none of them has to keep its word.</p> <p class="pitch">Seven powers share one map, and none of them has to keep its word. Other maps seat five or ten.</p>
<p class="facts">2 to 7 players · an unofficial rebuild of Allan B. Calhamer's 1959 Diplomacy</p> <p class="players">2 to 10 players, depending on the map, or solo against the house's envoys</p>
<span class="play">Play at dip.kestrelsnest.social →</span> <p class="origin">An unofficial rebuild of Allan B. Calhamer's 1959 Diplomacy</p>
<p class="length"></p>
<div class="go">
<span class="play">Play Dip →</span>
<span class="site">dip.kestrelsnest.social</span>
</div>
</div> </div>
</a> </a>
@@ -191,9 +218,9 @@
<div> <div>
<h2>How a table works</h2> <h2>How a table works</h2>
<ul class="how"> <ul class="how">
<li><b>Open a table</b> and you get a four-letter code and a link. Send it to a friend, and begin once they sit down.</li> <li><b>Start a table</b> and you get a four-letter code and a link. Send it to a friend, and begin once they sit down.</li>
<li><b>No one home?</b> Every game can seat the house, whether an automaton, a housecarl, a rival wizard or an envoy, so you can play right away on your own.</li> <li><b>No one home?</b> Every game can seat the house, whether an automaton, a housecarl, a rival wizard or an envoy, so you can play right away on your own.</li>
<li><b>Nothing to sign up for.</b> Your browser holds your seat. Close the tab and come back, and the game is where you left it.</li> <li><b>Nothing to sign up for.</b> Your browser holds your seat. Come back in the same browser and the game is where you left it. On another device, use Transfer seat in any game to carry your seat over with four words.</li>
<li><b>Watchers welcome.</b> Anyone with the link can sit in the Peanut Gallery and, if the host allows, join the table talk.</li> <li><b>Watchers welcome.</b> Anyone with the link can sit in the Peanut Gallery and, if the host allows, join the table talk.</li>
<li><b>Something went wrong?</b> Every table has a report button that reaches the keeper with the exact moment attached.</li> <li><b>Something went wrong?</b> Every table has a report button that reaches the keeper with the exact moment attached.</li>
</ul> </ul>
@@ -208,12 +235,12 @@
</div> </div>
</div> </div>
<h2 style="margin-top:28px">The hosting</h2> <h2 style="margin-top:28px">The hosting</h2>
<p>The games are free and will stay free. They run on small servers that cost a few dollars a month, and anyone who wants to chip in toward the hosting can do so at <a href="https://ko-fi.com/kestrelsnest">ko-fi.com/kestrelsnest</a>. Every game stays whole either way.</p> <p>The games are free and will stay free. They run on small servers that cost a few dollars a month, and anyone who wants to chip in toward the hosting can do so at <a class="nowrap" href="https://ko-fi.com/kestrelsnest">ko-fi.com/kestrelsnest</a>. Chipping in unlocks nothing, and nothing is held back without it.</p>
</div> </div>
</section> </section>
<footer> <footer>
<p>Wiz-War is Tom Jolly's design, and the rights rest with Steve Jackson Games. Waving Hands is Richard Bartle's. Diplomacy is Allan B. Calhamer's design, and its name is a trademark of Hasbro. Nothing here is official or endorsed by Steve Jackson Games, Richard Bartle or Hasbro. Hnefatafl belongs to everyone, and is more than a thousand years old.</p> <p>Wiz-War is Tom Jolly's design, and the rights rest with Steve Jackson Games. Waving Hands is Richard Bartle's. Hnefatafl belongs to everyone, and is more than a thousand years old. Diplomacy is Allan B. Calhamer's design, and its name is a trademark of Hasbro; the designers of Dip's Chromatic and Youngstown Redux maps are credited in Dip's How to play. Nothing here is official or endorsed by Steve Jackson Games, Richard Bartle or Hasbro.</p>
</footer> </footer>
</div> </div>
+12 -7
View File
@@ -46,9 +46,11 @@ A fresh host, when one is needed: `doctl compute droplet create hall --region ny
## Operations scripts on the droplet ## Operations scripts on the droplet
`deploy.sh` installs these to /usr/local/bin, the `traffic.py` they share to `deploy.sh` installs these to /usr/local/bin and the `traffic.py` they share
/usr/local/lib/__SLUG__, and the cron file to /etc/cron.d/__SLUG__ on every to /usr/local/lib/__SLUG__ on every deploy, so the live copies are the repo
deploy, so the live copies are the repo copies: copies. The cron file goes to /etc/cron.d/__SLUG__ only on a box of the game's
own: on the hall's shared host, `hall-nightly.sh` (in the kit's `gamehall/ops`)
runs every game's rollup and backup from /etc/cron.d/gamehall.
- `__SLUG__-visitors.sh [day]`: who is here now and who came that day. - `__SLUG__-visitors.sh [day]`: who is here now and who came that day.
- `__SLUG__-pulse.sh`: the weekly health check (service, errors, rollup - `__SLUG__-pulse.sh`: the weekly health check (service, errors, rollup
@@ -62,10 +64,13 @@ deploy, so the live copies are the repo copies:
wizwar droplet; until then it logs "skipped". wizwar droplet; until then it logs "skipped".
Errors from the game server go to Sentry, project `__SLUG__` in the Errors from the game server go to Sentry, project `__SLUG__` in the
locallygrownnet organisation; the DSN is in the unit file. The nightly rollup locallygrownnet organisation; the DSN is in the unit file. On the shared host
and backup check in with Sentry Crons when /root/.__SLUG__-sentry-cron-rollup the hall's nightly job checks in with Sentry Crons once for every game's
and /root/.__SLUG__-sentry-cron hold their check-in URLs (the ingest rollup and once for every backup (monitors `hall-rollup` and `hall-backup` in
URL with the project's cron path and public key), so a missed night is noticed. the `gamehall` project), and each game's script reports by its exit status.
A game on its own box checks in for itself when /root/.__SLUG__-sentry-cron-rollup
and /root/.__SLUG__-sentry-cron hold check-in URLs (the ingest URL with the
project's cron path and public key); absent, it sends nothing.
To have every new issue, regression and reappearance posted to Slack the way To have every new issue, regression and reappearance posted to Slack the way
wizwar's are, run `deploy/sentry-slack-alert.sh [#channel]` once from your own wizwar's are, run `deploy/sentry-slack-alert.sh [#channel]` once from your own
+6 -1
View File
@@ -5,6 +5,8 @@
# Ledgers are append-only JSONL; the game server never needs stopping. # Ledgers are append-only JSONL; the game server never needs stopping.
# Sentry Crons check-in: /root/.__SLUG__-sentry-cron holds the check-in # Sentry Crons check-in: /root/.__SLUG__-sentry-cron holds the check-in
# URL (absent file = no check-ins). SCHEDULE must match /etc/cron.d/__SLUG__. # URL (absent file = no check-ins). SCHEDULE must match /etc/cron.d/__SLUG__.
# On a host the hall shares, hall-nightly.sh runs this and checks in once
# for every game, so the file is absent and the exit status is the word.
set -u set -u
SCHEDULE="23 7 * * *" SCHEDULE="23 7 * * *"
BUCKET="kestrel-wizwar-backups" # the hall's shared Space, named for its first tenant BUCKET="kestrel-wizwar-backups" # the hall's shared Space, named for its first tenant
@@ -27,9 +29,10 @@ checkin() {
if [ ! -s /root/.config/rclone/rclone.conf ]; then if [ ! -s /root/.config/rclone/rclone.conf ]; then
echo "$(date -Is) skipped: rclone is not configured" >> "$LOG" echo "$(date -Is) skipped: rclone is not configured" >> "$LOG"
exit 0 exit 1
fi fi
status=0
{ {
echo "=== $(date -Is) backup start" echo "=== $(date -Is) backup start"
if rclone sync "$SRC" "spaces:$BUCKET/$PREFIX/current" --exclude 'rollup.jsonl' 2>&1 && if rclone sync "$SRC" "spaces:$BUCKET/$PREFIX/current" --exclude 'rollup.jsonl' 2>&1 &&
@@ -37,6 +40,7 @@ fi
checkin ok checkin ok
else else
checkin error checkin error
status=1
fi fi
CUTOFF=$(date -d "90 days ago" +%Y-%m-%d) CUTOFF=$(date -d "90 days ago" +%Y-%m-%d)
rclone lsf "spaces:$BUCKET/$PREFIX/snapshots/" --dirs-only 2>/dev/null | \ rclone lsf "spaces:$BUCKET/$PREFIX/snapshots/" --dirs-only 2>/dev/null | \
@@ -49,3 +53,4 @@ fi
done done
echo "=== $(date -Is) backup done" echo "=== $(date -Is) backup done"
} >> "$LOG" 2>&1 } >> "$LOG" 2>&1
exit $status
+4 -2
View File
@@ -8,7 +8,9 @@
# Cron: /etc/cron.d/__SLUG__, installed by deploy.sh; SCHEDULE below # Cron: /etc/cron.d/__SLUG__, installed by deploy.sh; SCHEDULE below
# must match it, because the Sentry monitor is told the same shape. # must match it, because the Sentry monitor is told the same shape.
# Sentry Crons check-in: /root/.__SLUG__-sentry-cron-rollup holds the # Sentry Crons check-in: /root/.__SLUG__-sentry-cron-rollup holds the
# check-in URL (absent = no check-ins). # check-in URL (absent = no check-ins). On a host the hall shares,
# hall-nightly.sh runs this and checks in once for every game, so the
# file is absent and the exit status is the word.
set -u set -u
OUT="/var/lib/__SLUG__/rollup.jsonl" OUT="/var/lib/__SLUG__/rollup.jsonl"
LOG="/var/log/__SLUG__/rollup.log" LOG="/var/log/__SLUG__/rollup.log"
@@ -78,4 +80,4 @@ with open(out + ".tmp", "w") as fh:
os.replace(out + ".tmp", out) os.replace(out + ".tmp", out)
print("%s rolled: %d people, %d requests, %d rooms" % (day, len(t["addresses"]), t["human"], created)) print("%s rolled: %d people, %d requests, %d rooms" % (day, len(t["addresses"]), t["human"], created))
PY PY
then checkin ok; else checkin error; fi then checkin ok; else checkin error; exit 1; fi
+5 -1
View File
@@ -43,7 +43,11 @@ ssh "root@$HOST" '
install -m 755 /opt/__SLUG__/app/deploy/__SLUG__-rollup.sh /usr/local/bin/__SLUG__-rollup.sh install -m 755 /opt/__SLUG__/app/deploy/__SLUG__-rollup.sh /usr/local/bin/__SLUG__-rollup.sh
install -m 755 /opt/__SLUG__/app/deploy/__SLUG__-backup.sh /usr/local/bin/__SLUG__-backup.sh install -m 755 /opt/__SLUG__/app/deploy/__SLUG__-backup.sh /usr/local/bin/__SLUG__-backup.sh
install -m 644 -D /opt/__SLUG__/app/deploy/traffic.py /usr/local/lib/__SLUG__/traffic.py install -m 644 -D /opt/__SLUG__/app/deploy/traffic.py /usr/local/lib/__SLUG__/traffic.py
install -m 644 /opt/__SLUG__/app/deploy/__SLUG__.cron /etc/cron.d/__SLUG__ # On a host the hall shares, its hall-nightly.sh runs the rollup and backup
# of every game and checks in with Sentry once for all; a game on a box of
# its own schedules its own.
if [ -f /etc/cron.d/gamehall ]; then rm -f /etc/cron.d/__SLUG__
else install -m 644 /opt/__SLUG__/app/deploy/__SLUG__.cron /etc/cron.d/__SLUG__; fi
mkdir -p /var/log/__SLUG__ mkdir -p /var/log/__SLUG__
systemctl daemon-reload systemctl daemon-reload
systemctl enable --now __SLUG__ systemctl enable --now __SLUG__
+1 -1
View File
@@ -330,7 +330,7 @@
{/if} {/if}
<p class="hosting"> <p class="hosting">
The games of Kestrel's Hall are free and will stay free. They run on small servers that cost a few dollars a month, and anyone who wants to chip in toward the hosting can do so at The games of Kestrel's Hall are free and will stay free. They run on small servers that cost a few dollars a month, and anyone who wants to chip in toward the hosting can do so at
<a href="https://ko-fi.com/kestrelsnest" target="_blank" rel="noreferrer">ko-fi.com/kestrelsnest</a>. Every game stays whole either way. <a href="https://ko-fi.com/kestrelsnest" target="_blank" rel="noreferrer">ko-fi.com/kestrelsnest</a>. Chipping in unlocks nothing, and nothing is held back without it.
</p> </p>
<p class="made">__NAME__ is its designer's.</p> <p class="made">__NAME__ is its designer's.</p>
</div> </div>