The visitors digest: who is here, who came today, how far they got

/usr/local/bin/wizwar-visitors.sh prints one screen — live sockets and
rooms touched in the hour, today's traffic row, the names seated today
with the first-time ones marked, every room opened today with its
state, move counts, span, and last human move, humans' chat verbatim,
and the desk's count — and the wizwar-visitors skill reads it aloud.
It replaces the four hand-written versions of the same report from
announcement day.

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 18:58:53 -04:00
co-authored by Claude Fable 5.1
parent 9d2c66f0bd
commit 2b5c2c4884
3 changed files with 149 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
---
name: wizwar-visitors
description: The real-time Wiz-War visitors report — who is at the table right now, who came today and how far each got, what they said, what they filed, and today's traffic so far. Use when Eric asks about new players, visitors, "anyone playing?", or wants a traffic report for today.
---
# Wiz-War visitors
One command, one screen. The droplet prints the digest; you read it for
Eric like a host glancing over the tavern.
## Gather
ssh root@104.236.96.198 /usr/local/bin/wizwar-visitors.sh [YYYY-MM-DD]
Default is today, UTC. The script rolls today's partial rollup row
first, so the traffic line is current to the minute. Its output:
- **as of / live sockets / rooms touched in the last hour** — the
"right now" line. Two sockets at a quiet hour is Eric plus one.
- **traffic** — today's row of `/var/lib/wizwar/rollup.jsonl`:
addresses of people (bots split out), front-page vs gallery vs
replay views, campaigns (`?ref=` tags on posted links, Facebook's
fbclid) and referrers. Reddit and BGG send no referrer.
- **players today / NEW today** — names seated in rooms created today;
NEW means the name's first room ever is today. Known regulars:
Kestrel, Reuben, Jason, Yohon, and "Claude" (the duel seat).
- **rooms today** — one line each: time, code, humans vs bots, state
(lobby only / started, no moves / in play), move counts, span, and
the last human move with its time. Human chat lines follow the room,
verbatim. Automatons' chat is omitted; humans' is the signal.
- **REPORT lines and the feedback count** — reports filed today, plus
the unanswered total. Unanswered > 0 means run /wizwar-reports.
## Read it
Lead with new names and whether anyone is at the table now. Then, for
each stranger, say how far they got and where they stopped — the last
human move and the span tell it: a room that is "lobby only" stopped
before the start button; a game of under five minutes ending on
pickUpTreasure is the treasure wall the slips now explain; a long span
with many moves is a real session, resumable from their lobby. Quote
human chat verbatim. Then the traffic line as a single sentence with
the day-over-day direction if a previous row exists.
Two reads to resist: a ledger has no "finished" mark (the winner comes
from replaying it), and refused commands never enter ledgers — what a
player TRIED and was refused is invisible; to learn it, fetch the
ledger, replay to its end with the engine, and apply the commands they
plausibly sent next (see [[wizwar-fetch-game-files]]).
## Numbers with their meaning
- Announcement day (2026-09-03): 117 addresses, 9 rooms, 5 strangers;
before it, a good day was 27 addresses and a room or two.
- A new arrival who never presses start, or quits at a pickup, is a
first-impression signal, not a rules dispute — those file reports.
+1
View File
@@ -15,6 +15,7 @@ ssh "root@$HOST" '
# them on every deploy so the live copies are always the repo copies.
install -m 755 /opt/wizwar/deploy/wizwar-backup.sh /usr/local/bin/wizwar-backup.sh
install -m 755 /opt/wizwar/deploy/wizwar-rollup.sh /usr/local/bin/wizwar-rollup.sh
install -m 755 /opt/wizwar/deploy/wizwar-visitors.sh /usr/local/bin/wizwar-visitors.sh
install -m 644 /opt/wizwar/deploy/wizwar-rollup.cron /etc/cron.d/wizwar-rollup
systemctl daemon-reload
systemctl enable --now wizwar
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
# The visitors digest: who is here right now and who came today — the
# rooms opened, how far each got, what humans said, what they filed —
# with today's traffic so far. Read by the wizwar-visitors skill.
# wizwar-visitors.sh [YYYY-MM-DD] (default: today, UTC)
set -u
DAY="${1:-$(date -u +%F)}"
/usr/local/bin/wizwar-rollup.sh "$DAY" >/dev/null 2>&1
python3 - "$DAY" <<'PY'
import json, sys, glob, os, datetime, subprocess
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 = :8787 )' | tail -n +2 | wc -l"),
sh("find /var/lib/wizwar/rooms -name '*.jsonl' -mmin -60 | wc -l")))
# --- traffic so far, from the rollup ---
try:
rows = [json.loads(l) for l in open("/var/lib/wizwar/rollup.jsonl")]
r = [x for x in rows if x["day"] == day][-1]
p = r["paths"]
print("traffic: %d addresses, %d requests from people (%d bot); front %d, gallery %d, replays %d; campaigns %s; referrers %s" % (
r["humanAddresses"], r["human"], r["bot"], p.get("/", 0), p.get("/clips", 0), p.get("/watch", 0),
json.dumps(r["campaigns"]), json.dumps(r["referrers"])))
except Exception as e:
print("traffic: (no rollup row: %s)" % e)
# --- every room, for first sightings; today's rooms in detail ---
first_seen = {}
rooms = []
for f in glob.glob("/var/lib/wizwar/rooms/*.jsonl"):
try:
lines = open(f).read().splitlines()
meta = json.loads(lines[0])
except Exception: continue
ca = meta.get("createdAt", "")
if not ca: continue
humans = [meta["hostId"]]; bots = []; started = False; cmds = 0; chat = []; last = ca; steps = []
for l in lines[1:]:
try: x = json.loads(l)
except Exception: continue
k = x.get("kind")
if k == "join":
(bots if x.get("bot") else humans).append(x["name"])
elif k == "start": started = True
elif k == "command":
cmds += 1; last = x.get("at", last)
steps.append((x.get("playerId"), x["command"].get("type"), x.get("at", "")))
elif k == "chat":
last = x.get("at", last)
if x["player"] in humans: chat.append((x.get("at", "")[11:16], x["player"], x["text"]))
for h in humans:
if h not in first_seen or ca < first_seen[h]: first_seen[h] = ca
rooms.append(dict(id=meta["id"], created=ca, humans=humans, bots=bots, started=started, cmds=cmds, last=last, chat=chat, steps=steps))
today = sorted([r for r in rooms if r["created"].startswith(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].startswith(day)]
print("players today: %s | NEW today: %s" % (", ".join(names) or "none", ", ".join(new) or "none"))
print("rooms today: %d" % len(today))
for r in today:
span = ""
if r["cmds"]:
t0 = datetime.datetime.fromisoformat(r["created"].replace("Z", "+00:00")); t1 = datetime.datetime.fromisoformat(r["last"].replace("Z", "+00:00"))
span = "%d min" % round((t1 - t0).total_seconds() / 60)
human_cmds = [s for s in r["steps"] if s[0] in r["humans"]]
last_human = ("last human move: %s %s at %s" % (human_cmds[-1][0], human_cmds[-1][1], human_cmds[-1][2][11:16])) if human_cmds else "no human move"
state = "lobby only" if not r["started"] else ("started, no moves" if not r["cmds"] else "in play")
print(" %s %s %s%s %s %d moves (%d human) %s %s" % (
r["created"][11:16], r["id"], "+".join(r["humans"]), (" vs %d bot%s" % (len(r["bots"]), "" if len(r["bots"]) == 1 else "s")) if r["bots"] else "",
state, r["cmds"], len(human_cmds), span, last_human))
for at, who, text in r["chat"]: print(" %s %s: %s" % (at, who, text))
# --- the desk ---
reports = 0; unanswered = 0; answered = set()
try:
rows = [json.loads(l) for l in open("/var/lib/wizwar/feedback.jsonl") if l.strip()]
answered = {x["reportId"] for x in rows if "reportId" in x}
for x in rows:
if "happened" not in x: continue
if (x.get("id") or x["at"]) not in answered: unanswered += 1
if x["at"].startswith(day):
reports += 1
print(" REPORT %s %s (%s): %s / expected: %s" % (x["at"][11:16], x["player"], x["roomId"], x["happened"], x["expected"]))
except OSError: pass
print("feedback: %d filed today, %d unanswered overall" % (reports, unanswered))
PY