#!/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 # No notice is an empty one, {}, never a missing file: a page's check for a missing file # is a failed request, and the browser reports it in every visitor's console. case "${1:-}" in --clear) mkdir -p "$(dirname "$FILE")"; echo '{}' > "$FILE.tmp"; chmod 644 "$FILE.tmp"; mv "$FILE.tmp" "$FILE"; echo "cleared."; exit 0 ;; --show) if grep -q '"text"' "$FILE" 2>/dev/null; then cat "$FILE"; else echo "no announcement."; fi; 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"