diff --git a/README.md b/README.md index 4c38414..9123ee5 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,21 @@ then the round resolves; turn-at-a-time games fit as the case where one seat needs input at a time. A game of single commands with out-of-turn interruptions, Wiz-War's shape, does not fit and keeps its own server. +## The hall-wide notice + +Every game on the shared host shows the same notice at the top of its +pages when there is one: maintenance, a new game, anything the whole +family should hear. The keeper sets it on the host, + + hall-announce "Kestrel's Hall is down for five minutes of maintenance" --at "2026-09-30 11:00 UTC" + hall-announce --clear + +which writes /var/lib/hall/announcement.json; each game's site serves it at +`/hall/announcement.json` (a `handle_path` in its Caddy site file), and +`Announcement.svelte`, in the template's layout, shows it with the time in +each reader's own clock. A reader may dismiss one notice; the next still +shows. `hall-announce` is installed by `gamehall/deploy.sh`. + ## The family `family.json` lists every game of Kestrel's Hall: slug, name, address, diff --git a/drift.sh b/drift.sh index 2ce6c28..28b74c5 100755 --- a/drift.sh +++ b/drift.sh @@ -12,7 +12,7 @@ KIT="$(cd "$(dirname "$0")" && pwd)" MAP="$KIT/drift-map/$SLUG.txt" SHARED="server/src/index.ts server/src/rooms.ts server/src/store.ts server/src/reports.ts server/src/ratelimit.ts server/src/tally.ts src/lib/game/spec.ts src/lib/net/client.ts src/lib/net/talk.ts src/lib/net/view.ts src/lib/net/room.svelte.ts -src/lib/components/TableTalk.svelte src/lib/components/ReportSlip.svelte src/lib/components/TabbedDialog.svelte src/lib/components/Tabs.svelte +src/lib/components/TableTalk.svelte src/lib/components/ReportSlip.svelte src/lib/components/TabbedDialog.svelte src/lib/components/Tabs.svelte src/lib/components/Announcement.svelte deploy/deploy.sh deploy/setup-droplet.sh deploy/setup-server.sh deploy/Caddyfile.tmpl deploy/verify-ledgers.sh deploy/replay-ledgers.ts deploy/pulse.sh deploy/visitors.sh deploy/sentry-slack-alert.sh deploy/pull-reports.sh deploy/report-reply.sh deploy/__SLUG__.service deploy/__SLUG__.cron deploy/__SLUG__-backup.sh deploy/__SLUG__-rollup.sh" diff --git a/gamehall/deploy.sh b/gamehall/deploy.sh index 04fe789..b0b800f 100755 --- a/gamehall/deploy.sh +++ b/gamehall/deploy.sh @@ -23,6 +23,9 @@ ssh "root@$HOST" ' install -m 755 /tmp/gamehall-ops/gamehall-rollup.sh /usr/local/bin/gamehall-rollup.sh 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 + # The hall-wide notice every game serves at /hall/announcement.json. + install -m 755 /tmp/gamehall-ops/hall-announce.sh /usr/local/bin/hall-announce + install -d -m 755 /var/lib/hall mkdir -p /var/lib/gamehall /var/log/gamehall rm -r /tmp/gamehall-ops ' diff --git a/gamehall/gamehall.caddy b/gamehall/gamehall.caddy index eb8d1e3..0ef7e7f 100644 --- a/gamehall/gamehall.caddy +++ b/gamehall/gamehall.caddy @@ -1,5 +1,12 @@ # Kestrel's Hall, the front door of the family: a static page, no server. gamehall.kestrelsnest.social, gamehall.159.203.115.252.sslip.io { + # The hall-wide notice (hall-announce on the host): one file, served by every game's site. + handle_path /hall/* { + root * /var/lib/hall + header Cache-Control "no-store" + file_server + } + root * /opt/gamehall encode gzip zstd diff --git a/gamehall/ops/hall-announce.sh b/gamehall/ops/hall-announce.sh new file mode 100644 index 0000000..87644ab --- /dev/null +++ b/gamehall/ops/hall-announce.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# The notice every game of Kestrel's Hall shows at the top of its pages. +# Each site serves /var/lib/hall/announcement.json at /hall/announcement.json; +# a page checks it on load and every few minutes. Run on the host as root: +# hall-announce "Kestrel's Hall is down for five minutes of maintenance" --at "2026-09-30 11:00 UTC" [--until "2026-09-30 11:15 UTC"] +# hall-announce --show +# hall-announce --clear +# --at is when it happens, told in each reader's own time; --until is when the +# notice stops showing (an hour after --at when not given). +set -euo pipefail +FILE=/var/lib/hall/announcement.json + +case "${1:-}" in + --clear) rm -f "$FILE"; echo "cleared."; exit 0 ;; + --show) cat "$FILE" 2>/dev/null || echo "no announcement."; exit 0 ;; + ''|-*) sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;; +esac + +TEXT="$1"; shift +AT=""; UNTIL="" +while [ $# -gt 0 ]; do + case "$1" in + --at) AT="$(date -u -d "$2" +%Y-%m-%dT%H:%M:%SZ)"; shift 2 ;; + --until) UNTIL="$(date -u -d "$2" +%Y-%m-%dT%H:%M:%SZ)"; shift 2 ;; + *) echo "unknown option: $1" >&2; exit 2 ;; + esac +done +[ -n "$AT" ] && [ -z "$UNTIL" ] && UNTIL="$(date -u -d "$AT + 1 hour" +%Y-%m-%dT%H:%M:%SZ)" + +mkdir -p "$(dirname "$FILE")" +# A new id each time, so a reader who dismissed the last notice still sees this one. +TEXT="$TEXT" AT="$AT" UNTIL="$UNTIL" ID="$(date -u +%Y%m%dT%H%M%S)" python3 -c ' +import json, os +a = {"id": os.environ["ID"], "text": os.environ["TEXT"]} +if os.environ["AT"]: a["at"] = os.environ["AT"] +if os.environ["UNTIL"]: a["until"] = os.environ["UNTIL"] +print(json.dumps(a))' > "$FILE.tmp" +chmod 644 "$FILE.tmp" +mv "$FILE.tmp" "$FILE" +cat "$FILE" diff --git a/gamehall/site/index.html b/gamehall/site/index.html index c2c2b1e..a3c00e5 100644 --- a/gamehall/site/index.html +++ b/gamehall/site/index.html @@ -113,6 +113,9 @@ footer { margin-top: 64px; padding: 28px 0 48px; border-top: 1px solid var(--line); color: var(--bone-faint); font-size: 0.9rem; } footer p { margin: 0 0 8px; } +/* 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 button { position: absolute; right: .5rem; background: none; border: 0; color: inherit; font-size: 1.3rem; line-height: 1; padding: .2rem .5rem; cursor: pointer; } @@ -214,5 +217,50 @@ + diff --git a/template/deploy/Caddyfile.tmpl b/template/deploy/Caddyfile.tmpl index 1a39223..9b270fb 100644 --- a/template/deploy/Caddyfile.tmpl +++ b/template/deploy/Caddyfile.tmpl @@ -1,4 +1,11 @@ __HOST__ { + # The hall-wide notice (hall-announce on the host): one file, served by every game's site. + handle_path /hall/* { + root * /var/lib/hall + header Cache-Control "no-store" + file_server + } + root * /opt/__SLUG__/build encode gzip zstd diff --git a/template/src/lib/components/Announcement.svelte b/template/src/lib/components/Announcement.svelte new file mode 100644 index 0000000..18f343f --- /dev/null +++ b/template/src/lib/components/Announcement.svelte @@ -0,0 +1,119 @@ + + +{#if shown} +
+ {shown.text}{when} + +
+{/if} + + diff --git a/template/src/routes/+layout.svelte b/template/src/routes/+layout.svelte index b93e9ba..33c1fca 100644 --- a/template/src/routes/+layout.svelte +++ b/template/src/routes/+layout.svelte @@ -1,7 +1,9 @@ + {@render children()}