Players see their reports answered in the lobby ledger

Reports gain ids; replies live in the same JSONL, folding onto their
report when read (legacy reports answer to their timestamp). The lobby
asks with the same seat-token proof as the games ledger — wake-free —
and lists each report with its reply and status, or 'the wizards are
studying the moment'. deploy/feedback-reply.sh answers one from here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-09-01 09:34:20 -04:00
co-authored by Claude Fable 5
parent eabc10f18c
commit 5abd0ba60e
5 changed files with 153 additions and 3 deletions
+22 -1
View File
@@ -10,6 +10,7 @@
// {type:"catchUp", sinceSeq} replay of moves missed while away
// {type:"chat", text} table talk to the room
// {type:"feedback", happened, expected} a surprise report, pinned to room+seq
// {type:"myFeedback", seats} your reports + the wizards' replies
// {type:"rollDie"} the tabletop D4, published as talk
// {type:"addBot", style?, tier?} host seats an automaton
// {type:"watch", roomId} join the Peanut Gallery: nameless, read-only
@@ -62,7 +63,8 @@ import {
abandonRoom,
} from "./rooms";
import { engagementStats, recordHotseat } from "./stats";
import { appendFeedback } from "./store";
import { appendFeedback, readFeedback } from "./store";
import { randomBytes } from "node:crypto";
import { getShare, loadShares, mintShare } from "./shares";
import { renderSharePng } from "./ogimage";
import { BOT_LINES, type BanterTrigger } from "./banter";
@@ -805,6 +807,7 @@ wss.on("connection", (socket) => {
const happened = clean(msg.happened);
if (!happened) return send(socket, { type: "error", message: "say what happened" });
appendFeedback({
id: randomBytes(4).toString("hex"),
at: new Date().toISOString(),
roomId: room.id,
player: session.playerId ?? "(gallery)",
@@ -817,6 +820,24 @@ wss.on("connection", (socket) => {
send(socket, { type: "feedbackReceived" });
break;
}
case "myFeedback": {
// Reports for every seat this browser can prove — the same token
// check as the games ledger, and just as wake-free.
const seats = Array.isArray(msg.seats) ? msg.seats.slice(0, MAX_MYGAMES_SEATS) : [];
const proven: { roomId: string; name: string }[] = [];
for (const seat of seats) {
if (typeof seat !== "object" || seat === null) continue;
const roomId = String(seat.roomId ?? "").toUpperCase();
const name = String(seat.name ?? "");
const result = peekSummary(roomId, name, typeof seat.token === "string" ? seat.token : null);
if (result !== null && result !== "badToken") proven.push({ roomId, name });
}
const reports = readFeedback()
.filter((r) => proven.some((s) => s.roomId === r.roomId && s.name === r.player))
.map(({ player: _player, ...r }) => r);
send(socket, { type: "feedbackList", reports });
break;
}
case "hotseatReport": {
// A device finishes a handful of games at most; a firehose is abuse.
if (++session.hotseatReports > 20) return;