A visitors digest for the droplet, in wizwar's shape
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
40fcf31faa
commit
098dadf482
@@ -60,6 +60,10 @@ seconds; Caddy holds requests that land in the gap.
|
||||
- Logs: `ssh root@<ip> journalctl -u waving-hands -f` for the duel server,
|
||||
`journalctl -u caddy -f` and /var/lib/caddy/access.log for the web side.
|
||||
- Restart: `ssh root@<ip> systemctl restart waving-hands`
|
||||
- Who has been playing: `ssh root@<ip> waving-hands-visitors.sh [YYYY-MM-DD]`
|
||||
prints today's visitors from Caddy's log (people and bots apart, by page),
|
||||
every room opened today with how far it got, and the names seated, marking
|
||||
those seen for the first time.
|
||||
- Rooms: `/var/lib/waving-hands/rooms/<CODE>.jsonl`, one ledger per duel.
|
||||
Copy that directory to back them up; single-player games are in players'
|
||||
browsers.
|
||||
|
||||
+2
-1
@@ -18,7 +18,7 @@ rsync -az --delete --filter='P _app/immutable/*' \
|
||||
# it needs only its code and the engine. Ledgers live outside this tree.
|
||||
rsync -az --delete --exclude='/server/node_modules' \
|
||||
--include='/server/***' --include='/src/' --include='/src/lib/' --include='/src/lib/game/***' \
|
||||
--include='/deploy/' --include='/deploy/waving-hands.service' \
|
||||
--include='/deploy/' --include='/deploy/waving-hands.service' --include='/deploy/visitors.sh' \
|
||||
--exclude='*' \
|
||||
./ "root@$HOST:/opt/waving-hands/app/"
|
||||
|
||||
@@ -29,6 +29,7 @@ ssh "root@$HOST" '
|
||||
cd /opt/waving-hands/app/server && npm install --no-audit --no-fund
|
||||
chown -R waving-hands:waving-hands /opt/waving-hands/app
|
||||
cp /opt/waving-hands/app/deploy/waving-hands.service /etc/systemd/system/waving-hands.service
|
||||
install -m 755 /opt/waving-hands/app/deploy/visitors.sh /usr/local/bin/waving-hands-visitors.sh
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now waving-hands
|
||||
systemctl restart waving-hands
|
||||
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# The visitors digest: who is at the table right now and who came today,
|
||||
# the rooms opened and how far each got, with today's traffic so far.
|
||||
# Runs ON the droplet: visitors.sh [YYYY-MM-DD] (default: today, UTC)
|
||||
# Single-player duels live in players' browsers, so they show only as
|
||||
# visits to /play; duels between people show in full from their ledgers.
|
||||
set -u
|
||||
DAY="${1:-$(date -u +%F)}"
|
||||
python3 - "$DAY" <<'PY'
|
||||
import datetime, glob, json, re, subprocess, sys
|
||||
from collections import Counter
|
||||
|
||||
day = sys.argv[1]
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
print("as of %s UTC, day %s" % (now.strftime("%H:%M"), day))
|
||||
|
||||
def sh(cmd):
|
||||
try:
|
||||
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=20).stdout.strip()
|
||||
except Exception:
|
||||
return "?"
|
||||
|
||||
print("live sockets: %s | rooms touched in the last hour: %s" % (
|
||||
sh("ss -tn state established '( sport = :8788 )' | tail -n +2 | wc -l"),
|
||||
sh("find /var/lib/waving-hands/rooms -name '*.jsonl' -mmin -60 2>/dev/null | wc -l")))
|
||||
|
||||
# --- traffic: Caddy's access log, people and bots apart ---
|
||||
BOT = re.compile(r"bot|crawl|spider|slurp|facebookexternalhit|preview|fetch|curl|python|go-http|headless|scan|monitor|uptime", re.I)
|
||||
PAGES = {"/": "hall", "/play": "play", "/rules": "rules"}
|
||||
addresses, bots = set(), set()
|
||||
pages, joins, referrers, campaigns, agents = Counter(), Counter(), Counter(), Counter(), Counter()
|
||||
requests = 0
|
||||
for path in sorted(glob.glob("/var/lib/caddy/access.log*")):
|
||||
try:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
try:
|
||||
x = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
ts = datetime.datetime.fromtimestamp(x["ts"], datetime.timezone.utc)
|
||||
if ts.strftime("%F") != day:
|
||||
continue
|
||||
req = x["request"]
|
||||
ua = " ".join(req.get("headers", {}).get("User-Agent", [""]))
|
||||
ip = req.get("remote_ip") or req.get("client_ip", "?")
|
||||
uri = req.get("uri", "")
|
||||
path_only = uri.split("?")[0]
|
||||
if BOT.search(ua) or not ua:
|
||||
bots.add(ip)
|
||||
continue
|
||||
if path_only.startswith("/_app/") or path_only.startswith("/api/") or path_only == "/ws" or path_only.endswith((".png", ".ico", ".txt")):
|
||||
continue
|
||||
requests += 1
|
||||
addresses.add(ip)
|
||||
if path_only in PAGES:
|
||||
pages[PAGES[path_only]] += 1
|
||||
elif path_only.startswith("/join/"):
|
||||
joins[path_only.split("/")[2].upper()] += 1
|
||||
ref = " ".join(req.get("headers", {}).get("Referer", [""]))
|
||||
if ref and "hands.kestrelsnest.social" not in ref:
|
||||
referrers[re.sub(r"^https?://", "", ref).split("/")[0]] += 1
|
||||
m = re.search(r"[?&](ref|utm_source|fbclid)=([^&]*)", uri)
|
||||
if m:
|
||||
campaigns[m.group(1) + "=" + m.group(2)[:24]] += 1
|
||||
agents[re.sub(r"\(.*?\)", "", ua)[:40].strip()] += 1
|
||||
except OSError:
|
||||
continue
|
||||
print("traffic: %d addresses of people, %d page requests (%d bot addresses); hall %d, play %d, rules %d, room links %s; referrers %s; campaigns %s" % (
|
||||
len(addresses), requests, len(bots), pages["hall"], pages["play"], pages["rules"],
|
||||
dict(joins) or "none", dict(referrers) or "none", dict(campaigns) or "none"))
|
||||
|
||||
# --- rooms: every ledger, today's in detail ---
|
||||
first_seen = {}
|
||||
rooms = []
|
||||
for f in glob.glob("/var/lib/waving-hands/rooms/*.jsonl"):
|
||||
try:
|
||||
lines = [json.loads(l) for l in open(f) if l.strip()]
|
||||
meta = lines[0]
|
||||
assert meta["t"] == "room"
|
||||
except Exception:
|
||||
continue
|
||||
created = datetime.datetime.fromtimestamp(meta["createdAt"] / 1000, datetime.timezone.utc)
|
||||
humans, bots_seated, started, turns, last = [], [], False, 0, created
|
||||
for x in lines[1:]:
|
||||
if x["t"] == "seat":
|
||||
(bots_seated if x["bot"] else humans).append(x["name"])
|
||||
elif x["t"] == "start":
|
||||
started = True
|
||||
elif x["t"] == "turn":
|
||||
turns += 1
|
||||
last = datetime.datetime.fromtimestamp(x["at"] / 1000, datetime.timezone.utc)
|
||||
for h in humans:
|
||||
if h not in first_seen or created < first_seen[h]:
|
||||
first_seen[h] = created
|
||||
rooms.append(dict(id=meta["id"], size=meta["seats"], created=created, humans=humans, bots=bots_seated, started=started, turns=turns, last=last))
|
||||
|
||||
today = sorted([r for r in rooms if r["created"].strftime("%F") == day], key=lambda r: r["created"])
|
||||
names = sorted({h for r in today for h in r["humans"]})
|
||||
new = [n for n in names if first_seen[n].strftime("%F") == day]
|
||||
print("players today: %s | NEW today: %s" % (", ".join(names) or "none", ", ".join(new) or "none"))
|
||||
print("rooms today: %d (of %d ever)" % (len(today), len(rooms)))
|
||||
for r in today:
|
||||
state = "lobby only" if not r["started"] else ("started, no turns" if not r["turns"] else "in play")
|
||||
span = ("%d min" % round((r["last"] - r["created"]).total_seconds() / 60)) if r["turns"] else ""
|
||||
seated = "+".join(r["humans"]) + ((" vs %d bot%s" % (len(r["bots"]), "" if len(r["bots"]) == 1 else "s")) if r["bots"] else "")
|
||||
empty = r["size"] - len(r["humans"]) - len(r["bots"])
|
||||
print(" %s %s %s%s %s %d turns %s last turn %s" % (
|
||||
r["created"].strftime("%H:%M"), r["id"], seated, (" (%d seat%s empty)" % (empty, "" if empty == 1 else "s")) if empty else "",
|
||||
state, r["turns"], span, r["last"].strftime("%H:%M") if r["turns"] else "never"))
|
||||
PY
|
||||
Reference in New Issue
Block a user