A report may carry a screenshot

The 🐞 form takes a picture beside its words — PNG, JPEG, or WebP, under
2.5 MB, previewed before sending. The socket's message cap is far too
small for one, so the picture goes through a plain HTTP door after the
report is filed, addressed to the report's id, one per report within
the hour, checked by its own first bytes, and kept in feedback-images/
beside the reports with a line on the report saying so. The desk's
skill knows where to look.

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 11:02:59 -04:00
co-authored by Claude Fable 5.1
parent d76a18d5e6
commit 48350a3686
5 changed files with 106 additions and 8 deletions
+17 -2
View File
@@ -4,6 +4,10 @@ import type { Command, GameEvent, GameView, Side } from "@wizwar/engine";
import { cardDef } from "@wizwar/engine";
import { receiptFor } from "./receipt";
/** The same server over plain HTTP, for the one door a socket cannot carry: a picture. */
const API_URL =
import.meta.env.VITE_WIZWAR_SERVER ? String(import.meta.env.VITE_WIZWAR_SERVER).replace(/^ws/, "http")
: location.port === "5173" ? `http://${location.hostname}:8787` : "";
const SERVER_URL =
import.meta.env.VITE_WIZWAR_SERVER ??
(location.port === "5173" // the vite dev server; anything else serves its own socket
@@ -659,9 +663,17 @@ class Net {
case "feedbackList":
this.feedbackReports = msg.reports ?? [];
break;
case "feedbackReceived":
case "feedbackReceived": {
const image = this.pendingImage;
this.pendingImage = null;
if (image && typeof msg.id === "string") {
fetch(`${API_URL}/api/feedback-image/${msg.id}`, { method: "POST", headers: { "content-type": image.type }, body: image })
.then((r) => { if (!r.ok) this.flash("The words went through; the picture did not — a PNG or JPEG under 2.5 MB, please"); })
.catch(() => this.flash("The words went through; the picture did not"));
}
this.refreshFeedback();
break;
}
case "games": {
this.games = msg.games;
for (const g of msg.games as GameSummary[]) {
@@ -773,7 +785,10 @@ class Net {
}
/** A surprise report: the server pins it to the room and move number. */
sendFeedback(happened: string, expected: string): void {
/** A screenshot waiting for its report's id, then posted beside it. */
private pendingImage: File | null = null;
sendFeedback(happened: string, expected: string, image: File | null = null): void {
this.pendingImage = image;
this.send({ type: "feedback", happened, expected });
}