Files
Eric WagonerandClaude Opus 5.5 151d82e747 Traffic counts what matters: rooms opened from the site, where links were shared, scanners and monitors as bots
- A room opened with Play now never loads /join: the API's answer to
  POST /api/rooms is counted as rooms opened here.
- Link previews (Slack, Discord, Facebook, iMessage, WhatsApp and the
  rest) are reported as where the link was shared, since the people who
  click them arrive with no referrer. WhatsApp's fetcher is a bot now.
- An address that asks for a scanner's paths, or keeps coming back
  without opening a game, is a bot's all day; so is a browser that loads
  pages at a steady interval from a new address each time, never
  playing. A declared bot is still judged request by request.
- The site's own addresses, sslip.io included, are not referrers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
2026-09-27 10:00:53 -04:00

84 lines
4.4 KiB
Bash
Executable File

#!/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 ---
sys.path.insert(0, "/usr/local/lib/waving-hands")
import traffic
t = traffic.scan(day)
print("traffic: %d addresses of people, %d page requests (%d bot addresses, of them %d scanners, %d monitors); hall %d, play %d, rules %d; rooms opened here %d, room links %s; shared on %s; referrers %s; campaigns %s" % (
len(t["addresses"]), t["human"], len(t["botAddresses"]), t["scanners"], t["monitors"], t["pages"]["hall"], t["pages"]["play"], t["pages"]["rules"],
t["opened"], dict(t["joins"]) or "none", dict(t["shared"]) or "none", dict(t["referrers"]) or "none", dict(t["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, over, talk = [], [], False, 0, created, None, 0
names = {}
for x in lines[1:]:
if x["t"] == "seat":
(bots_seated if x["bot"] else humans).append(x["name"])
names[x["id"]] = x["name"]
elif x["t"] == "unseat":
bots_seated.remove(names.pop(x["id"], None)) if x["id"] in names else None
elif x["t"] == "start":
started = True
elif x["t"] == "turn":
turns += 1
last = datetime.datetime.fromtimestamp(x["at"] / 1000, datetime.timezone.utc)
elif x["t"] == "chat":
talk += 1
elif x["t"] == "over":
over = "a draw" if x["winner"] is None else "%s won" % names.get(x["winner"], x["winner"])
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, over=over, talk=talk))
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:
idle_h = (now - r["last"]).total_seconds() / 3600
state = "lobby only" if not r["started"] else ("started, no turns" if not r["turns"] else (r["over"] or ("abandoned" if idle_h > 12 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%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",
(" %d line%s of talk" % (r["talk"], "" if r["talk"] == 1 else "s")) if r["talk"] else ""))
PY