The rollup counts the tags on posted links, and the pulse has a right-now

Reddit and BoardGameGeek send no referrer, so a visitor from either
looks direct. A ?ref=<name> on the posted link says where they came
from; the rollup counts those (one per address per day, Facebook's
fbclid as its own) beside the referrers browsers do send. The pulse
gains a right-now recipe: live sockets, rooms touched in the last
hour, and today's partial row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-03 12:06:12 -04:00
co-authored by Claude Fable 5.1
parent ac2c3576a7
commit cf7920e159
2 changed files with 22 additions and 3 deletions
+9 -1
View File
@@ -40,7 +40,15 @@ quiet, say so in one line before the details.
serviceStarts, ledgerMB). Report day-over-day direction, not raw serviceStarts, ledgerMB). Report day-over-day direction, not raw
rows. Its Sentry cron monitor is slug `wizwar-rollup` (a missed rows. Its Sentry cron monitor is slug `wizwar-rollup` (a missed
check-in there means the rollup itself is broken). Re-roll a day by check-in there means the rollup itself is broken). Re-roll a day by
hand: `wizwar-rollup.sh YYYY-MM-DD`. hand: `wizwar-rollup.sh YYYY-MM-DD`. `campaigns` counts visitors
(one per address per day) whose link carried `?ref=<name>` — the tags
on Eric's posts (reddit, bgg, …) — or a Facebook fbclid; referrers
alone can't attribute Reddit or BGG, which send none.
**Right now** (on request, or when the announcement is fresh): live
sockets `ss -tn state established '( sport = :8787 )' | tail -n +2 | wc -l`,
rooms touched in the last hour `find /var/lib/wizwar/rooms -name '*.jsonl' -mmin -60 | wc -l`,
and today so far `wizwar-rollup.sh $(date -u +%F)` (a partial row,
replaced again at 00:10; the vitals in it are the moment's).
5. **The table itself**: 5. **The table itself**:
- Room count and growth: `ls /var/lib/wizwar/rooms/*.jsonl | wc -l` - Room count and growth: `ls /var/lib/wizwar/rooms/*.jsonl | wc -l`
- New human names this week: room ledgers' join lines minus bots - New human names this week: room ledgers' join lines minus bots
+13 -2
View File
@@ -3,7 +3,9 @@
# yesterday's traffic (from Caddy's access log), the table's growth (from # yesterday's traffic (from Caddy's access log), the table's growth (from
# the room ledgers, stats.json and feedback.jsonl), and the box's vitals. # the room ledgers, stats.json and feedback.jsonl), and the box's vitals.
# Counts only: no addresses are kept. The pulse reads it for trends the # Counts only: no addresses are kept. The pulse reads it for trends the
# self-rotating access log cannot keep. # self-rotating access log cannot keep. Referrers are what browsers send
# (Reddit and BGG send none); campaigns are the ?ref=<name> tags on links
# Eric posts, one count per address per day, plus Facebook's fbclid.
# Cron: 10 0 * * * (UTC), see /etc/cron.d/wizwar-rollup (deploy.sh installs both). # Cron: 10 0 * * * (UTC), see /etc/cron.d/wizwar-rollup (deploy.sh installs both).
# Sentry Crons check-in: /root/.wizwar-sentry-cron-rollup holds the URL # Sentry Crons check-in: /root/.wizwar-sentry-cron-rollup holds the URL
# (absent = no check-ins). # (absent = no check-ins).
@@ -25,6 +27,9 @@ BOT = ("bot", "crawl", "spider", "facebookexternalhit", "slack", "discord", "twi
# --- traffic: every access log file touched since the day began --- # --- traffic: every access log file touched since the day began ---
req = 0; human = 0; bot = 0; hips = set(); bips = set() req = 0; human = 0; bot = 0; hips = set(); bips = set()
paths = collections.Counter(); status = collections.Counter(); refs = collections.Counter(); ws = 0 paths = collections.Counter(); status = collections.Counter(); refs = collections.Counter(); ws = 0
# Where a visit came from, when the link itself says so: ?ref=<name> on a
# link Eric posted, a Facebook click's fbclid, or a utm_source.
campaigns = collections.Counter(); seen_campaign = set()
for f in sorted(glob.glob("/var/lib/caddy/access*.log*")): for f in sorted(glob.glob("/var/lib/caddy/access*.log*")):
if os.path.getmtime(f) < t0: continue if os.path.getmtime(f) < t0: continue
opener = gzip.open if f.endswith(".gz") else open opener = gzip.open if f.endswith(".gz") else open
@@ -41,8 +46,13 @@ for f in sorted(glob.glob("/var/lib/caddy/access*.log*")):
req += 1 req += 1
if isbot: bot += 1; bips.add(ip) if isbot: bot += 1; bips.add(ip)
else: human += 1; hips.add(ip) else: human += 1; hips.add(ip)
uri = q.get("uri", "").split("?")[0] full = q.get("uri", ""); uri = full.split("?")[0]
if uri == "/ws": ws += 1 if uri == "/ws": ws += 1
if not isbot and "?" in full and ip not in seen_campaign:
qs = dict(kv.split("=", 1) if "=" in kv else (kv, "") for kv in full.split("?", 1)[1].split("&"))
tag = qs.get("ref") or qs.get("utm_source") or ("facebook" if "fbclid" in qs else None)
if tag and re.fullmatch(r"[a-z0-9_-]{1,24}", tag):
campaigns[tag] += 1; seen_campaign.add(ip)
key = ("/clips" if uri.startswith("/clips") else "/watch" if uri.startswith("/watch") key = ("/clips" if uri.startswith("/clips") else "/watch" if uri.startswith("/watch")
else "(assets)" if uri.startswith("/assets") or re.search(r"\.(png|jpg|svg|ico|css|js|webmanifest|mp4)$", uri) else "(assets)" if uri.startswith("/assets") or re.search(r"\.(png|jpg|svg|ico|css|js|webmanifest|mp4)$", uri)
else uri if uri in ("/", "/ws", "/robots.txt") else "(other)") else uri if uri in ("/", "/ws", "/robots.txt") else "(other)")
@@ -99,6 +109,7 @@ row = {
"day": day, "requests": req, "human": human, "bot": bot, "day": day, "requests": req, "human": human, "bot": bot,
"humanAddresses": len(hips), "botAddresses": len(bips), "socketConnects": ws, "humanAddresses": len(hips), "botAddresses": len(bips), "socketConnects": ws,
"paths": dict(paths.most_common()), "status": dict(status), "referrers": dict(refs.most_common(10)), "paths": dict(paths.most_common()), "status": dict(status), "referrers": dict(refs.most_common(10)),
"campaigns": dict(campaigns.most_common(10)),
"newRooms": new_rooms, "humanSeats": sum(seats.values()), "humanNames": sorted(seats), "newRooms": new_rooms, "humanSeats": sum(seats.values()), "humanNames": sorted(seats),
"gamesFinishedTotal": stats.get("gamesFinished"), "gamesCreatedTotal": stats.get("gamesCreated"), "gamesFinishedTotal": stats.get("gamesFinished"), "gamesCreatedTotal": stats.get("gamesCreated"),
"reports": reports, "replies": replies, "reports": reports, "replies": replies,