From the hnefatafl repo's pass of the same day, everything that touched a kit-shared file. The visitors digest and the nightly rollup share deploy/traffic.py, installed to /usr/local/lib/<slug>; the rollup writes the finished-games count it computed behind "and False". pull-reports.sh and the reports skill both use deploy/report-digest.ts. The seat line requires its token hash and the start line its rules revision; the migration for ledgers written before hashing goes with them. The route table in server/src/index.ts lists every route; Report, ReportLine and Tally are declared once in view.ts for both sides; exports nobody imported are exports no more. In the client: .small, the × that dismisses, and the frame of the reading pages are in app.css once; the preferences panel shares the report slip's modal shape; the room store gains seatEmpty and seatUnheld, and the lobby and the join page read those instead of three spellings of their own. The room's moved and awaiting fields stay: the demo board reads them, and simultaneous rounds are the contract. The deploy README no longer describes a browser-only game; the visitors skill no longer names a /play route; the reports skill's replay call carries the room's options. The Slack channel id is passed in the environment rather than filled in as a placeholder. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwFKMuQnPAEHJ5yA1q4orh
77 lines
3.9 KiB
Bash
Executable File
77 lines
3.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Nightly rollup: one JSON line per day in /var/lib/__SLUG__/rollup.jsonl:
|
|
# the day's traffic from Caddy's access log (people and bots apart, by page,
|
|
# with referrers and the campaign tags links carry), the rooms opened,
|
|
# started and finished, and the box's vitals. Counts only: no addresses are
|
|
# kept. The pulse reads it for trends the self-rotating access log loses.
|
|
# __SLUG__-rollup.sh [YYYY-MM-DD] (default: yesterday, UTC)
|
|
# Cron: /etc/cron.d/__SLUG__, installed by deploy.sh; SCHEDULE below
|
|
# must match it, because the Sentry monitor is told the same shape.
|
|
# Sentry Crons check-in: /root/.__SLUG__-sentry-cron-rollup holds the
|
|
# check-in URL (absent = no check-ins).
|
|
set -u
|
|
OUT="/var/lib/__SLUG__/rollup.jsonl"
|
|
LOG="/var/log/__SLUG__/rollup.log"
|
|
DAY="${1:-$(date -u -d 'yesterday' +%Y-%m-%d)}"
|
|
SCHEDULE="12 0 * * *"
|
|
CRON_URL=$(cat /root/.__SLUG__-sentry-cron-rollup 2>/dev/null || true)
|
|
# The check-in carries the schedule: Sentry keeps a monitor only once told its shape.
|
|
checkin() {
|
|
[ -n "$CRON_URL" ] && curl -sf -o /dev/null -X POST -H "Content-Type: application/json" \
|
|
-d "{\"status\":\"$1\",\"monitor_config\":{\"schedule\":{\"type\":\"crontab\",\"value\":\"$SCHEDULE\"},\"checkin_margin\":30,\"max_runtime\":10,\"timezone\":\"UTC\"}}" \
|
|
"$CRON_URL" || true
|
|
}
|
|
mkdir -p "$(dirname "$LOG")"
|
|
checkin in_progress
|
|
if python3 - "$DAY" "$OUT" <<'PY' >> "$LOG" 2>&1
|
|
import datetime, glob, json, os, shutil, sys
|
|
sys.path.insert(0, "/usr/local/lib/__SLUG__")
|
|
import traffic
|
|
day, out = sys.argv[1], sys.argv[2]
|
|
d0 = datetime.datetime.fromisoformat(day).replace(tzinfo=datetime.timezone.utc)
|
|
t0, t1 = d0.timestamp(), (d0 + datetime.timedelta(days=1)).timestamp()
|
|
t = traffic.scan(day)
|
|
|
|
created = started = finished = turned = turns = 0
|
|
humans, bots = set(), 0
|
|
for f in glob.glob("/var/lib/__SLUG__/rooms/*.jsonl"):
|
|
try: lines = [json.loads(l) for l in open(f) if l.strip()]
|
|
except Exception: continue
|
|
if not lines or lines[0].get("t") != "room": continue
|
|
made = lines[0]["createdAt"] / 1000
|
|
if t0 <= made < t1:
|
|
created += 1
|
|
if any(x["t"] == "start" for x in lines): started += 1
|
|
if any(x["t"] == "turn" for x in lines): turned += 1
|
|
for x in lines:
|
|
if x["t"] == "seat":
|
|
if x["bot"]: bots += 1
|
|
else: humans.add(x["name"])
|
|
elif x["t"] == "unseat": bots -= 1
|
|
turns += sum(1 for x in lines if x["t"] == "turn" and t0 <= x["at"] / 1000 < t1)
|
|
# The over line is written with the turn that ends the game, so a finish counts on its own day.
|
|
if any(x["t"] == "over" and t0 <= x["at"] / 1000 < t1 for x in lines): finished += 1
|
|
|
|
disk = shutil.disk_usage("/")
|
|
mem = {}
|
|
for line in open("/proc/meminfo"):
|
|
k, v = line.split(":", 1); mem[k] = int(v.split()[0])
|
|
load = os.getloadavg()[1]
|
|
row = dict(day=day, requests=t["requests"], human=t["human"], bot=t["bot"], humanAddresses=len(t["addresses"]), botAddresses=len(t["botAddresses"]),
|
|
paths=dict(t["pages"]), roomLinks=sum(t["joins"].values()), referrers=dict(t["referrers"]), campaigns=dict(t["campaigns"]),
|
|
rooms=dict(created=created, started=started, withTurns=turned, finished=finished, humanNames=len(humans), botSeats=bots),
|
|
turns=turns, ledgers=len(glob.glob("/var/lib/__SLUG__/rooms/*.jsonl")),
|
|
vitals=dict(diskUsedPct=round(disk.used * 100 / disk.total), memAvailableMb=mem.get("MemAvailable", 0) // 1024, load5=round(load, 2)),
|
|
at=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"))
|
|
rows = []
|
|
if os.path.exists(out):
|
|
rows = [json.loads(l) for l in open(out) if l.strip()]
|
|
rows = [r for r in rows if r.get("day") != day] + [row]
|
|
rows.sort(key=lambda r: r["day"])
|
|
with open(out + ".tmp", "w") as fh:
|
|
for r in rows: fh.write(json.dumps(r) + "\n")
|
|
os.replace(out + ".tmp", out)
|
|
print("%s rolled: %d people, %d requests, %d rooms" % (day, len(t["addresses"]), t["human"], created))
|
|
PY
|
|
then checkin ok; else checkin error; fi
|