A "seats" button beside "transfer seat" opens the host's panel. In a game under way, "give to the clockwork" lets an automaton play a person's seat from the next beat, with their hand, treasures and place untouched; a standIn line on the ledger records it, the chronicle says so, the stand-in never talks under the person's name, and the person's token opens nothing after. "Make a phrase" mints the four-word phrase for any person's seat; claiming it re-seats on a new token, and a seat the clockwork stood in for goes back to its person mid-turn. A rematch holds a stood-in seat for its person. The gallery now has its own claim box, for a player who opens the invite link in a fresh browser. The client's watchdog pinged and then closed a socket quiet for 45 seconds. A background tab's timers run about once a minute, so the last pong was always a minute old when it looked, and idle tabs reconnected every 58 seconds. It now closes only when its own ping went unanswered. The server pings every socket at the protocol level every 30 seconds and drops one that left the last ping unanswered, so a crashed browser's socket no longer lingers until TCP gives up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
99 lines
5.1 KiB
Bash
Executable File
99 lines
5.1 KiB
Bash
Executable File
#!/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, 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 = []
|
|
standing = set(); stood = [] # seats the host gave the clockwork, and who and when
|
|
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 == "standIn":
|
|
standing.add(x["name"]); stood.append((x.get("at", "")[11:16], x["name"], "to the clockwork"))
|
|
elif k == "reseat" and x["name"] in standing:
|
|
standing.discard(x["name"]); stood.append((x.get("at", "")[11:16], x["name"], "back to a person"))
|
|
elif k == "command":
|
|
cmds += 1; last = x.get("at", last)
|
|
steps.append((x.get("playerId"), x["command"].get("type"), x.get("at", ""), x.get("playerId") in standing))
|
|
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, stood=stood))
|
|
|
|
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"] and not s[3]]
|
|
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, what in r["stood"]: print(" %s %s's seat goes %s" % (at, who, what))
|
|
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
|