The reports desk: a slip for surprises, replies in the hall
A Report button in a room's masthead files what happened and what was expected, with a screenshot if there is one, pinned to the room and the turn and rung through Sentry. Reports and the keeper's replies live in feedback.jsonl beside the ledgers; the hall shows each seat's reports with the thread under them and a line to answer. pull-reports.sh mirrors the desk locally, report-reply.sh answers, and a skill works the desk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c49703ee4f
commit
8eca078043
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { allSeats, api, forgetSeat } from '$lib/game/client';
|
||||
import { allSeats, api, forgetSeat, type Report } from '$lib/game/client';
|
||||
import { Duel, duel, NAME_MAX, playerName, rememberName } from '$lib/game/duel.svelte';
|
||||
import { unreadTalk } from '$lib/game/talk';
|
||||
import type { RoomView } from '$lib/game/view';
|
||||
@@ -12,6 +12,33 @@
|
||||
let seats = $state(allSeats());
|
||||
let games = $state<Record<string, RoomView | null>>({});
|
||||
let aboutOpen = $state(false);
|
||||
/** Reports this browser's seats have filed, with the keeper's replies. */
|
||||
let reports = $state<Report[]>([]);
|
||||
let answers = $state<Record<string, string>>({});
|
||||
let answerError = $state('');
|
||||
|
||||
// The desk is asked once per visit for every seat this browser holds.
|
||||
$effect(() => {
|
||||
const held = seats;
|
||||
if (!held.length) return;
|
||||
api.myReports(held.map((h) => ({ roomId: h.roomId, token: h.token })))
|
||||
.then((r) => (reports = r.reports.sort((a, b) => (a.at < b.at ? 1 : -1))))
|
||||
.catch(() => (reports = []));
|
||||
});
|
||||
|
||||
async function answer(r: Report) {
|
||||
const text = (answers[r.id] ?? '').trim();
|
||||
const held = seats.find((h) => h.roomId === r.roomId);
|
||||
if (!text || !held) return;
|
||||
answerError = '';
|
||||
try {
|
||||
await api.answerReport(r.id, r.roomId, held.token, text);
|
||||
r.thread.push({ at: new Date().toISOString(), text, from: 'player' });
|
||||
answers[r.id] = '';
|
||||
} catch (e) {
|
||||
answerError = e instanceof Error ? e.message : 'The answer did not reach the keeper.';
|
||||
}
|
||||
}
|
||||
|
||||
/** The single-player duel saved in this browser, if one is under way. */
|
||||
const local = $derived(duel.inProgress ? duel : null);
|
||||
@@ -187,6 +214,37 @@
|
||||
<button type="button" class="ledger-forget" title="forget this duel" onclick={() => forget(held.roomId)}>×</button>
|
||||
</div>
|
||||
{/each}
|
||||
{#if reports.length}
|
||||
<div class="ledger-head reports-head">your reports to the keeper</div>
|
||||
{#each reports as r (r.id)}
|
||||
{@const last = r.thread[r.thread.length - 1]}
|
||||
<div class="report-row">
|
||||
<span class="ledger-code">{r.roomId}</span>
|
||||
<div class="report-body">
|
||||
<p class="report-text">“{r.happened}”{#if r.image}<span class="muted" title="with your screenshot"> (with a picture)</span>{/if}</p>
|
||||
{#each r.thread as line, j (j)}
|
||||
{#if line.from === 'player'}
|
||||
<p class="report-reply yours">you: {line.text}</p>
|
||||
{:else}
|
||||
<p class="report-reply"><span class="report-status">{line.status}</span> {line.text}</p>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if !last}
|
||||
<p class="report-reply pending">the keeper is studying the moment</p>
|
||||
{:else if last.from === 'player'}
|
||||
<p class="report-reply pending">your word is with the keeper</p>
|
||||
{/if}
|
||||
{#if r.thread.some((l) => l.from === 'desk')}
|
||||
<form class="report-answer" onsubmit={(e) => { e.preventDefault(); void answer(r); }}>
|
||||
<input type="text" bind:value={answers[r.id]} maxlength="2000" placeholder="answer the keeper" aria-label="answer the keeper" />
|
||||
<button type="submit" class="quiet" disabled={!(answers[r.id] ?? '').trim()}>Send</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if answerError}<p class="warning">{answerError}</p>{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -443,6 +501,58 @@
|
||||
color: var(--frost);
|
||||
}
|
||||
|
||||
.reports-head {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.report-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.45rem 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.report-body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.report-text {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.report-reply {
|
||||
font-size: 0.85rem;
|
||||
color: var(--bone-dim);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.report-reply.yours {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.report-reply.pending {
|
||||
font-style: italic;
|
||||
color: var(--bone-faint);
|
||||
}
|
||||
|
||||
.report-status {
|
||||
color: var(--ember);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.report-answer {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.report-answer input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ledger-code {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
|
||||
Reference in New Issue
Block a user