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:
co-authored by
Claude Fable 5.1
parent
ffbca5bfb9
commit
11e9f950c6
@@ -1517,6 +1517,8 @@
|
||||
let fbHappened = $state("");
|
||||
let fbExpected = $state("");
|
||||
let fbSent = $state(false);
|
||||
/** Drafts of answers under your reports, by report id. */
|
||||
let reportAnswers = $state<Record<string, string>>({});
|
||||
/** A screenshot for the desk, chosen with the report. */
|
||||
let fbImage = $state<File | null>(null);
|
||||
let fbPreview = $state<string | null>(null);
|
||||
@@ -2582,13 +2584,28 @@
|
||||
<div class="report-row">
|
||||
<span class="ledger-code">{r.roomId}</span>
|
||||
<div class="report-body">
|
||||
<div class="report-text">“{r.happened}”</div>
|
||||
{#if r.reply}
|
||||
<div class="report-reply">
|
||||
<span class="report-status">{r.reply.status}</span> — {r.reply.text}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="report-text">“{r.happened}”{#if r.image}<span class="report-pic" title="with your screenshot"> 📷</span>{/if}</div>
|
||||
{#each r.thread ?? [] as line, j (j)}
|
||||
{#if line.from === "player"}
|
||||
<div class="report-reply yours">you — {line.text}</div>
|
||||
{:else}
|
||||
<div class="report-reply"><span class="report-status">{line.status}</span> — {line.text}</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if !(r.thread ?? []).length}
|
||||
<div class="report-reply pending">the wizards are studying the moment…</div>
|
||||
{:else if (r.thread ?? [])[(r.thread ?? []).length - 1]!.from === "player"}
|
||||
<div class="report-reply pending">your word is with the wizards…</div>
|
||||
{/if}
|
||||
{#if (r.thread ?? []).some((l) => l.from === "desk")}
|
||||
<form class="report-answer" onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const t = (reportAnswers[r.id] ?? "").trim();
|
||||
if (t && net.answerReport(r, t)) reportAnswers = { ...reportAnswers, [r.id]: "" };
|
||||
}}>
|
||||
<input bind:value={reportAnswers[r.id]} maxlength="2000" placeholder="answer the wizards…" aria-label="answer the wizards" />
|
||||
<button class="stamp tiny" type="submit" disabled={!(reportAnswers[r.id] ?? "").trim()}>send</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -3681,6 +3698,10 @@
|
||||
resize: vertical;
|
||||
}
|
||||
.fb-note { font-size: 0.8rem; color: #6b5a41; font-style: italic; }
|
||||
.report-reply.yours { color: #43331f; font-style: italic; }
|
||||
.report-pic { color: #8a6d3f; }
|
||||
.report-answer { display: flex; gap: 0.35rem; margin-top: 0.35rem; }
|
||||
.report-answer input { flex: 1; min-width: 0; background: #f4eede; border: 1px solid #b3a687; border-radius: 4px; padding: 0.3rem 0.5rem; font-family: inherit; font-size: 0.85rem; color: #43331f; }
|
||||
.fb-picture input { font-size: 0.85rem; color: #43331f; }
|
||||
.fb-preview { display: block; max-width: 100%; max-height: 9rem; margin-top: 0.4rem; border: 1px solid #b3a687; border-radius: 3px; }
|
||||
.fb-thanks { color: #43331f; padding: 0.6rem 0; }
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user