Reports are threads, and every turn of one rings the desk's bell

A player answers the desk under its reply from their lobby, proven by
the seat the report was filed from; the lobby shows the exchange in
order and says whose word is pending. A new report, a player's answer,
and a desk reply each raise a Sentry issue on the keeper's phone — the
first two from the server, the last from the reply script itself,
straight to Sentry from the droplet. The Desktop digest lists the
answers, and the desk's rule reads a report as reopened when the last
word is the player's.

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-21 12:08:55 -04:00
co-authored by Claude Fable 5.1
parent ffbca5bfb9
commit 11e9f950c6
7 changed files with 114 additions and 16 deletions
+11
View File
@@ -300,7 +300,11 @@ export interface FeedbackReportView {
round: number | null;
happened: string;
expected: string;
player?: string;
reply?: { at: string; text: string; status: string };
/** The exchange in order: the desk's replies and the player's answers. */
thread?: { at: string; text: string; from: "desk" | "player"; status?: string }[];
image?: string;
}
export interface GameSummary {
roomId: string;
@@ -798,6 +802,13 @@ class Net {
if (this.seats.length > 0) this.send({ type: "myFeedback", seats: $state.snapshot(this.seats) });
}
/** A word back to the desk under one of your reports, proven by the seat it was filed from. */
answerReport(report: { id: string; roomId: string; player?: string }, text: string): boolean {
const seat = this.seats.find((s) => s.roomId === report.roomId && (!report.player || s.name === report.player));
if (!seat) { this.flash("That seat is no longer in this browser, so the desk cannot be sure it is you"); return false; }
return this.send({ type: "feedbackAnswer", reportId: report.id, text, seat: $state.snapshot(seat) });
}
requestTransferCode(): void {
this.send({ type: "makeTransfer" });
}