Production: the tafl droplet at 209.97.148.244, tafl.kestrelsnest.social, and the Sentry project
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0d2f54fe02
commit
fc62034290
@@ -7,7 +7,7 @@ description: The weekly Hnefatafl operations pulse — game server health, error
|
|||||||
|
|
||||||
One command on the droplet prints everything; read it as a short report.
|
One command on the droplet prints everything; read it as a short report.
|
||||||
|
|
||||||
ssh root@__IP__ hnefatafl-pulse.sh
|
ssh root@209.97.148.244 hnefatafl-pulse.sh
|
||||||
|
|
||||||
- **game server**: whether the service and Caddy are up, restarts and
|
- **game server**: whether the service and Caddy are up, restarts and
|
||||||
journal errors in the last seven days with the last three error lines.
|
journal errors in the last seven days with the last three error lines.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: Work the Hnefatafl reports desk — fetch players' bug reports, sho
|
|||||||
|
|
||||||
Players file reports from the Report button in a room's masthead (seated
|
Players file reports from the Report button in a room's masthead (seated
|
||||||
or from the gallery). Each lands in `/var/lib/hnefatafl/feedback.jsonl`
|
or from the gallery). Each lands in `/var/lib/hnefatafl/feedback.jsonl`
|
||||||
on the droplet (__IP__) pinned with `roomId`, `turn` (the turn
|
on the droplet (209.97.148.244) pinned with `roomId`, `turn` (the turn
|
||||||
being written when it was filed) and `seq` (the ledger's length then),
|
being written when it was filed) and `seq` (the ledger's length then),
|
||||||
enough to replay the game to the moment. Replies live in the same file
|
enough to replay the game to the moment. Replies live in the same file
|
||||||
and appear under the report in the player's hall. This desk fetches,
|
and appear under the report in the player's hall. This desk fetches,
|
||||||
@@ -34,7 +34,7 @@ Every report and every player's answer rings a Sentry issue (project
|
|||||||
hnefatafl, fingerprinted per report or per line); the desk's own
|
hnefatafl, fingerprinted per report or per line); the desk's own
|
||||||
replies ring nothing.
|
replies ring nothing.
|
||||||
|
|
||||||
Fetch: `ssh root@__IP__ 'cat /var/lib/hnefatafl/feedback.jsonl'`
|
Fetch: `ssh root@209.97.148.244 'cat /var/lib/hnefatafl/feedback.jsonl'`
|
||||||
|
|
||||||
## a) Fetch and b) display
|
## a) Fetch and b) display
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ to hear his players' voices; never compress their words into a table.
|
|||||||
|
|
||||||
## c) Process an unanswered report
|
## c) Process an unanswered report
|
||||||
|
|
||||||
1. **Replay to the pin.** `scp root@__IP__:/var/lib/hnefatafl/rooms/<roomId>.jsonl <scratchpad>/`
|
1. **Replay to the pin.** `scp root@209.97.148.244:/var/lib/hnefatafl/rooms/<roomId>.jsonl <scratchpad>/`
|
||||||
and replay it with the engine as `deploy/replay-ledgers.ts` does:
|
and replay it with the engine as `deploy/replay-ledgers.ts` does:
|
||||||
`game.create(names, start.seed, start.rules ?? 1)` then
|
`game.create(names, start.seed, start.rules ?? 1)` then
|
||||||
`game.resolve(state, line.inputs)` per turn line, printing the state
|
`game.resolve(state, line.inputs)` per turn line, printing the state
|
||||||
@@ -70,8 +70,8 @@ to hear his players' voices; never compress their words into a table.
|
|||||||
passing `rules` to `createGame`). Old ledgers then replay as their
|
passing `rules` to `createGame`). Old ledgers then replay as their
|
||||||
players saw them; new games get the fix.
|
players saw them; new games get the fix.
|
||||||
Either way add a test in `src/lib/game/*.test.ts` pinning the
|
Either way add a test in `src/lib/game/*.test.ts` pinning the
|
||||||
corrected behaviour. Deploy with `deploy/deploy.sh __IP__`.
|
corrected behaviour. Deploy with `deploy/deploy.sh 209.97.148.244`.
|
||||||
5. **Reply.** `bash deploy/report-reply.sh __IP__ <reportId> <status> "text"`.
|
5. **Reply.** `bash deploy/report-reply.sh 209.97.148.244 <reportId> <status> "text"`.
|
||||||
Pass the id as ONE clean argument. Write to the PLAYER: name what you
|
Pass the id as ONE clean argument. Write to the PLAYER: name what you
|
||||||
replayed, cite the rule, and say plainly what was wrong or why nothing
|
replayed, cite the rule, and say plainly what was wrong or why nothing
|
||||||
was. The desk's voice is warm and specific.
|
was. The desk's voice is warm and specific.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ host glancing over the hall.
|
|||||||
|
|
||||||
## Gather
|
## Gather
|
||||||
|
|
||||||
ssh root@__IP__ hnefatafl-visitors.sh [YYYY-MM-DD]
|
ssh root@209.97.148.244 hnefatafl-visitors.sh [YYYY-MM-DD]
|
||||||
|
|
||||||
Default is today, UTC; Eric is in US Eastern, so an evening at his desk
|
Default is today, UTC; Eric is in US Eastern, so an evening at his desk
|
||||||
spills into the next UTC day. Run yesterday too when the hour is early.
|
spills into the next UTC day. Run yesterday too when the hour is early.
|
||||||
|
|||||||
+6
-6
@@ -21,13 +21,13 @@ uptime and upgrades independent of anything else.
|
|||||||
|
|
||||||
## Current production
|
## Current production
|
||||||
|
|
||||||
- Droplet: `hnefatafl` (nyc3, s-1vcpu-512mb-10gb, tag `hnefatafl`), IP __IP__
|
- Droplet: `hnefatafl` (nyc3, s-1vcpu-512mb-10gb, tag `hnefatafl`), IP 209.97.148.244
|
||||||
- URLs: https://hnefatafl.kestrelsnest.social (A record at Hover, where kestrelsnest.social's
|
- URLs: https://tafl.kestrelsnest.social (A record at Hover, where kestrelsnest.social's
|
||||||
DNS lives) and https://hnefatafl.__IP__.sslip.io (always works, zero DNS).
|
DNS lives) and https://hnefatafl.209.97.148.244.sslip.io (always works, zero DNS).
|
||||||
- Everyday deploy: `deploy/deploy.sh __IP__`
|
- Everyday deploy: `deploy/deploy.sh 209.97.148.244`
|
||||||
- Players' reports: `deploy/pull-reports.sh` mirrors /var/lib/hnefatafl/feedback.jsonl
|
- Players' reports: `deploy/pull-reports.sh` mirrors /var/lib/hnefatafl/feedback.jsonl
|
||||||
and the screenshots to ~/Desktop/hnefatafl-reports with a digest;
|
and the screenshots to ~/Desktop/hnefatafl-reports with a digest;
|
||||||
`deploy/report-reply.sh __IP__ <id> <status> "text"` answers one.
|
`deploy/report-reply.sh 209.97.148.244 <id> <status> "text"` answers one.
|
||||||
|
|
||||||
## New droplet from scratch
|
## New droplet from scratch
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ uptime and upgrades independent of anything else.
|
|||||||
--size s-1vcpu-512mb-10gb --image ubuntu-24-04-x64 \
|
--size s-1vcpu-512mb-10gb --image ubuntu-24-04-x64 \
|
||||||
--ssh-keys <your-key-ids> --tag-name hnefatafl --wait`
|
--ssh-keys <your-key-ids> --tag-name hnefatafl --wait`
|
||||||
2. `scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> \
|
2. `scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> \
|
||||||
"bash /root/setup-droplet.sh 'hnefatafl.kestrelsnest.social, hnefatafl.<ip>.sslip.io'"`
|
"bash /root/setup-droplet.sh 'tafl.kestrelsnest.social, hnefatafl.<ip>.sslip.io'"`
|
||||||
(point the A record at the new IP first, or leave the real name out until it is).
|
(point the A record at the new IP first, or leave the real name out until it is).
|
||||||
3. `scp deploy/setup-server.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> "bash /root/setup-server.sh"`
|
3. `scp deploy/setup-server.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> "bash /root/setup-server.sh"`
|
||||||
4. `deploy/deploy.sh <ip>`
|
4. `deploy/deploy.sh <ip>`
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ for f in sorted(glob.glob("/var/lib/caddy/access*.log*")):
|
|||||||
elif path == "/guide": pages["guide"] += 1
|
elif path == "/guide": pages["guide"] += 1
|
||||||
elif path.startswith("/join/"): joins[path.split("/")[2].upper()] += 1
|
elif path.startswith("/join/"): joins[path.split("/")[2].upper()] += 1
|
||||||
ref = " ".join(h.get("Referer", [""]))
|
ref = " ".join(h.get("Referer", [""]))
|
||||||
if ref and "hnefatafl.kestrelsnest.social" not in ref:
|
if ref and "tafl.kestrelsnest.social" not in ref:
|
||||||
refs[re.sub(r"^https?://", "", ref).split("/")[0]] += 1
|
refs[re.sub(r"^https?://", "", ref).split("/")[0]] += 1
|
||||||
m = re.search(r"[?&](ref|utm_source|fbclid)=([^&]*)", uri)
|
m = re.search(r"[?&](ref|utm_source|fbclid)=([^&]*)", uri)
|
||||||
if m and (ip, m.group(1)) not in seen_campaign:
|
if m and (ip, m.group(1)) not in seen_campaign:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Environment=KEEPER_TZ=America/New_York
|
|||||||
Environment=HOST=127.0.0.1
|
Environment=HOST=127.0.0.1
|
||||||
Environment=DATA_DIR=/var/lib/hnefatafl/rooms
|
Environment=DATA_DIR=/var/lib/hnefatafl/rooms
|
||||||
# Create the Sentry project (the Sentry MCP can) and paste its DSN here; empty means no error reporting.
|
# Create the Sentry project (the Sentry MCP can) and paste its DSN here; empty means no error reporting.
|
||||||
Environment=SENTRY_DSN=__SENTRY_DSN__
|
Environment=SENTRY_DSN=https://2e3589c41ea45a58049740d3af64face@o4509525984149504.ingest.us.sentry.io/4512136791130112
|
||||||
ExecStart=/opt/hnefatafl/app/server/node_modules/.bin/tsx src/index.ts
|
ExecStart=/opt/hnefatafl/app/server/node_modules/.bin/tsx src/index.ts
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# to a local folder, and write a digest beside them for reading.
|
# to a local folder, and write a digest beside them for reading.
|
||||||
# deploy/pull-reports.sh [host] [folder] (default ~/Desktop/hnefatafl-reports)
|
# deploy/pull-reports.sh [host] [folder] (default ~/Desktop/hnefatafl-reports)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
HOST="${1:-__IP__}"
|
HOST="${1:-209.97.148.244}"
|
||||||
OUT="${2:-$HOME/Desktop/hnefatafl-reports}"
|
OUT="${2:-$HOME/Desktop/hnefatafl-reports}"
|
||||||
mkdir -p "$OUT/images"
|
mkdir -p "$OUT/images"
|
||||||
scp -q "root@$HOST:/var/lib/hnefatafl/feedback.jsonl" "$OUT/feedback.jsonl" 2>/dev/null || : > "$OUT/feedback.jsonl"
|
scp -q "root@$HOST:/var/lib/hnefatafl/feedback.jsonl" "$OUT/feedback.jsonl" 2>/dev/null || : > "$OUT/feedback.jsonl"
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ AUTH="Authorization: Bearer $SENTRY_TOKEN"
|
|||||||
# The Slack action takes the channel's ID as well as its name; the
|
# The Slack action takes the channel's ID as well as its name; the
|
||||||
# project's error detector is looked up by project id; a workflow is bound
|
# project's error detector is looked up by project id; a workflow is bound
|
||||||
# to its detectors by PUT after creation.
|
# to its detectors by PUT after creation.
|
||||||
SLACK_ID="${SENTRY_SLACK_INTEGRATION:-__SENTRY_SLACK_INTEGRATION__}"
|
SLACK_ID="${SENTRY_SLACK_INTEGRATION:-345334}"
|
||||||
CHANNEL_ID="${SLACK_CHANNEL_ID:-__SLACK_CHANNEL_ID__}"
|
CHANNEL_ID="${SLACK_CHANNEL_ID:-__SLACK_CHANNEL_ID__}"
|
||||||
PROJECT_ID="${SENTRY_PROJECT_ID:-__SENTRY_PROJECT_ID__}"
|
PROJECT_ID="${SENTRY_PROJECT_ID:-4512136791130112}"
|
||||||
echo "Slack integration $SLACK_ID; project $PROJECT_ID; channel $CHANNEL ($CHANNEL_ID)"
|
echo "Slack integration $SLACK_ID; project $PROJECT_ID; channel $CHANNEL ($CHANNEL_ID)"
|
||||||
|
|
||||||
DETECTOR_ID=$(curl -s "$API/detectors/?project=$PROJECT_ID" -H "$AUTH" | python3 -c '
|
DETECTOR_ID=$(curl -s "$API/detectors/?project=$PROJECT_ID" -H "$AUTH" | python3 -c '
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# One-time droplet setup. Copy this script and Caddyfile.tmpl to the droplet
|
# One-time droplet setup. Copy this script and Caddyfile.tmpl to the droplet
|
||||||
# and run ON the droplet as root:
|
# and run ON the droplet as root:
|
||||||
# bash setup-droplet.sh 'hnefatafl.kestrelsnest.social, hnefatafl.<droplet-ip>.sslip.io'
|
# bash setup-droplet.sh 'tafl.kestrelsnest.social, hnefatafl.<droplet-ip>.sslip.io'
|
||||||
# The argument is the Caddy site address line: one name, or several
|
# The argument is the Caddy site address line: one name, or several
|
||||||
# separated by commas. Every name must already resolve to this droplet.
|
# separated by commas. Every name must already resolve to this droplet.
|
||||||
# Hnefatafl is a static site: Caddy serves the built files and terminates
|
# Hnefatafl is a static site: Caddy serves the built files and terminates
|
||||||
@@ -28,7 +28,9 @@ chmod -R g+rX /opt/hnefatafl
|
|||||||
# Caddy: the site and the game server's paths, from the template beside this script.
|
# Caddy: the site and the game server's paths, from the template beside this script.
|
||||||
sed "s|__HOST__|$HOST|" "$(dirname "$0")/Caddyfile.tmpl" > /etc/caddy/Caddyfile
|
sed "s|__HOST__|$HOST|" "$(dirname "$0")/Caddyfile.tmpl" > /etc/caddy/Caddyfile
|
||||||
caddy validate --config /etc/caddy/Caddyfile
|
caddy validate --config /etc/caddy/Caddyfile
|
||||||
systemctl reload caddy
|
# Validation as root touches the access log; Caddy runs as caddy and must own it.
|
||||||
|
chown -R caddy:caddy /var/lib/caddy
|
||||||
|
systemctl restart caddy
|
||||||
|
|
||||||
# Firewall: ssh + web only.
|
# Firewall: ssh + web only.
|
||||||
ufw allow OpenSSH
|
ufw allow OpenSSH
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# deploy/verify-ledgers.sh <droplet-ip-or-host> [https://site]
|
# deploy/verify-ledgers.sh <droplet-ip-or-host> [https://site]
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
HOST="${1:?usage: verify-ledgers.sh <droplet-ip-or-host> [https://site]}"
|
HOST="${1:?usage: verify-ledgers.sh <droplet-ip-or-host> [https://site]}"
|
||||||
SITE="${2:-https://hnefatafl.kestrelsnest.social}"
|
SITE="${2:-https://tafl.kestrelsnest.social}"
|
||||||
DIR="$(mktemp -d)"
|
DIR="$(mktemp -d)"
|
||||||
trap 'rm -rf "$DIR"' EXIT
|
trap 'rm -rf "$DIR"' EXIT
|
||||||
rsync -az "root@$HOST:/var/lib/hnefatafl/rooms/" "$DIR/"
|
rsync -az "root@$HOST:/var/lib/hnefatafl/rooms/" "$DIR/"
|
||||||
|
|||||||
+1
-1
@@ -57,7 +57,7 @@ for path in sorted(glob.glob("/var/lib/caddy/access.log*")):
|
|||||||
elif path_only.startswith("/join/"):
|
elif path_only.startswith("/join/"):
|
||||||
joins[path_only.split("/")[2].upper()] += 1
|
joins[path_only.split("/")[2].upper()] += 1
|
||||||
ref = " ".join(req.get("headers", {}).get("Referer", [""]))
|
ref = " ".join(req.get("headers", {}).get("Referer", [""]))
|
||||||
if ref and "hnefatafl.kestrelsnest.social" not in ref:
|
if ref and "tafl.kestrelsnest.social" not in ref:
|
||||||
referrers[re.sub(r"^https?://", "", ref).split("/")[0]] += 1
|
referrers[re.sub(r"^https?://", "", ref).split("/")[0]] += 1
|
||||||
m = re.search(r"[?&](ref|utm_source|fbclid)=([^&]*)", uri)
|
m = re.search(r"[?&](ref|utm_source|fbclid)=([^&]*)", uri)
|
||||||
if m:
|
if m:
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ import { Store } from './store';
|
|||||||
const PORT = Number(process.env.PORT ?? '8789');
|
const PORT = Number(process.env.PORT ?? '8789');
|
||||||
const HOST = process.env.HOST ?? '127.0.0.1';
|
const HOST = process.env.HOST ?? '127.0.0.1';
|
||||||
const DATA_DIR = process.env.DATA_DIR ?? '../data/rooms';
|
const DATA_DIR = process.env.DATA_DIR ?? '../data/rooms';
|
||||||
const PUBLIC_URL = (process.env.PUBLIC_URL ?? 'https://hnefatafl.kestrelsnest.social').replace(/\/$/, '');
|
const PUBLIC_URL = (process.env.PUBLIC_URL ?? 'https://tafl.kestrelsnest.social').replace(/\/$/, '');
|
||||||
/** The keeper of the site, whom a table may call to a seat, and where they sleep. */
|
/** The keeper of the site, whom a table may call to a seat, and where they sleep. */
|
||||||
const KEEPER = process.env.KEEPER ?? 'the keeper';
|
const KEEPER = process.env.KEEPER ?? 'the keeper';
|
||||||
const KEEPER_TZ = process.env.KEEPER_TZ ?? 'America/New_York';
|
const KEEPER_TZ = process.env.KEEPER_TZ ?? 'America/New_York';
|
||||||
|
|||||||
+4
-4
@@ -7,20 +7,20 @@
|
|||||||
<meta name="theme-color" content="#121a20" />
|
<meta name="theme-color" content="#121a20" />
|
||||||
<title>Hnefatafl</title>
|
<title>Hnefatafl</title>
|
||||||
<meta name="description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." />
|
<meta name="description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." />
|
||||||
<link rel="canonical" href="https://hnefatafl.kestrelsnest.social/" />
|
<link rel="canonical" href="https://tafl.kestrelsnest.social/" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:site_name" content="Hnefatafl" />
|
<meta property="og:site_name" content="Hnefatafl" />
|
||||||
<meta property="og:title" content="Hnefatafl" />
|
<meta property="og:title" content="Hnefatafl" />
|
||||||
<meta property="og:description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." />
|
<meta property="og:description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." />
|
||||||
<meta property="og:url" content="https://hnefatafl.kestrelsnest.social/" />
|
<meta property="og:url" content="https://tafl.kestrelsnest.social/" />
|
||||||
<meta property="og:image" content="https://hnefatafl.kestrelsnest.social/og.png" />
|
<meta property="og:image" content="https://tafl.kestrelsnest.social/og.png" />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="Hnefatafl" />
|
<meta name="twitter:title" content="Hnefatafl" />
|
||||||
<meta name="twitter:description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." />
|
<meta name="twitter:description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." />
|
||||||
<meta name="twitter:image" content="https://hnefatafl.kestrelsnest.social/og.png" />
|
<meta name="twitter:image" content="https://tafl.kestrelsnest.social/og.png" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
Reference in New Issue
Block a user