The credibility pass on Hnefatafl, ported to the template: the ops scripts count traffic through one parser, the reports digest is one program, the plaintext-token fallback and its migration are gone, and the stylesheet holds each shared rule once
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
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6c42e55f59
commit
33ac0ec808
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/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,
|
||||
@@ -24,45 +24,13 @@ checkin() {
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
checkin in_progress
|
||||
if python3 - "$DAY" "$OUT" <<'PY' >> "$LOG" 2>&1
|
||||
import collections, datetime, glob, gzip, json, os, re, shutil, sys
|
||||
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()
|
||||
BOT = re.compile(r"bot|crawl|spider|slurp|facebookexternalhit|slack|discord|twitter|preview|fetch|curl|python|go-http|wget|headless|scan|monitor|uptime", re.I)
|
||||
|
||||
req = human = bot = 0
|
||||
hips, bips = set(), set()
|
||||
pages, joins, refs, campaigns = collections.Counter(), collections.Counter(), collections.Counter(), collections.Counter()
|
||||
seen_campaign = set()
|
||||
for f in sorted(glob.glob("/var/lib/caddy/access*.log*")):
|
||||
if os.path.getmtime(f) < t0: continue
|
||||
opener = gzip.open if f.endswith(".gz") else open
|
||||
with opener(f, "rt", errors="ignore") as fh:
|
||||
for line in fh:
|
||||
try: r = json.loads(line)
|
||||
except ValueError: continue
|
||||
ts = r.get("ts", 0)
|
||||
if not (t0 <= ts < t1): continue
|
||||
q = r.get("request", {}); h = q.get("headers", {})
|
||||
ua = " ".join(h.get("User-Agent", [""]))
|
||||
ip = q.get("client_ip") or q.get("remote_ip") or "?"
|
||||
uri = q.get("uri", ""); path = uri.split("?")[0]
|
||||
req += 1
|
||||
if BOT.search(ua) or not ua:
|
||||
bot += 1; bips.add(ip); continue
|
||||
if path.startswith(("/_app/", "/api/")) or path == "/ws" or path.endswith((".png", ".ico", ".txt")): continue
|
||||
human += 1; hips.add(ip)
|
||||
if path == "/": pages["hall"] += 1
|
||||
elif path == "/rules": pages["rules"] += 1
|
||||
elif path == "/guide": pages["guide"] += 1
|
||||
elif path.startswith("/join/"): joins[path.split("/")[2].upper()] += 1
|
||||
ref = " ".join(h.get("Referer", [""]))
|
||||
if ref and "__DOMAIN__" not in ref:
|
||||
refs[re.sub(r"^https?://", "", ref).split("/")[0]] += 1
|
||||
m = re.search(r"[?&](ref|utm_source|fbclid)=([^&]*)", uri)
|
||||
if m and (ip, m.group(1)) not in seen_campaign:
|
||||
seen_campaign.add((ip, m.group(1)))
|
||||
campaigns[m.group(1) + ("=" + m.group(2)[:24] if m.group(1) != "fbclid" else "")] += 1
|
||||
t = traffic.scan(day)
|
||||
|
||||
created = started = finished = turned = turns = 0
|
||||
humans, bots = set(), 0
|
||||
@@ -80,20 +48,18 @@ for f in glob.glob("/var/lib/__SLUG__/rooms/*.jsonl"):
|
||||
if x["bot"]: bots += 1
|
||||
else: humans.add(x["name"])
|
||||
elif x["t"] == "unseat": bots -= 1
|
||||
day_turns = [x for x in lines if x["t"] == "turn" and t0 <= x["at"] / 1000 < t1]
|
||||
turns += len(day_turns)
|
||||
# A game finished today: its last turn is today's and the engine would say so; approximate by replay-free means:
|
||||
# the ledger's final turn falls today and no turn follows it within the day's end.
|
||||
if day_turns and day_turns[-1] is lines[-1] and False: finished += 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=req, human=human, bot=bot, humanAddresses=len(hips), botAddresses=len(bips),
|
||||
paths=dict(pages), roomLinks=sum(joins.values()), referrers=dict(refs), campaigns=dict(campaigns),
|
||||
rooms=dict(created=created, started=started, withTurns=turned, humanNames=len(humans), botSeats=bots),
|
||||
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"))
|
||||
@@ -105,6 +71,6 @@ 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(hips), human, created))
|
||||
print("%s rolled: %d people, %d requests, %d rooms" % (day, len(t["addresses"]), t["human"], created))
|
||||
PY
|
||||
then checkin ok; else checkin error; fi
|
||||
|
||||
Reference in New Issue
Block a user