From c494cf9a266d69e44c3c1dde0f9eeb44003e909c Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 6 Apr 2026 13:57:32 -0700 Subject: [PATCH] fix: gold icon, story banner crop, crafted items persist, community blurb, validation error filtering --- apps/api/src/routes/game.ts | 16 +++++++++----- apps/web/src/api/client.ts | 24 +++++++++++++++++++++ apps/web/src/components/game/aboutPanel.tsx | 9 ++++++++ apps/web/src/components/ui/resourceBar.tsx | 2 +- apps/web/src/styles.css | 1 + apps/web/src/utils/logError.ts | 14 +++++++++--- 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts index 7155011..fda6ccd 100644 --- a/apps/api/src/routes/game.ts +++ b/apps/api/src/routes/game.ts @@ -622,11 +622,17 @@ const validateAndSanitize = ( = Math.min(material.quantity, previousQuantity); return { ...material, quantity: cappedQuantity }; }); - const craftedRecipeIds = incoming.exploration.craftedRecipeIds.filter( - (recipeId) => { - return previousExploration.craftedRecipeIds.includes(recipeId); - }, - ); + + /* + * Merge crafted recipe IDs from both states so the list can only ever grow. + * A stale auto-save arriving after a craft must not silently un-craft items. + */ + const craftedRecipeIds = [ + ...new Set([ + ...previousExploration.craftedRecipeIds, + ...incoming.exploration.craftedRecipeIds, + ]), + ]; explorationSpread = { exploration: { ...incoming.exploration, diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index b4bc783..f26b210 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -38,6 +38,26 @@ import type { const baseUrl = "/api"; +/** + * Represents a 4xx API error so callers can distinguish expected server + * rejections from unexpected failures. ValidationErrors are downgraded to + * console.warn and are not forwarded to the error-email pipeline. + */ +class ValidationError extends Error { + public readonly statusCode: number; + + /** + * Creates a new ValidationError. + * @param message - The error message from the server response. + * @param statusCode - The HTTP status code (4xx) returned by the server. + */ + public constructor(message: string, statusCode: number) { + super(message); + this.name = "ValidationError"; + this.statusCode = statusCode; + } +} + const getToken = (): string | null => { return globalThis.localStorage.getItem("elysium_token"); }; @@ -72,6 +92,9 @@ const fetchJson = async ( = typeof errorBody.error === "string" ? errorBody.error : "Unknown error"; + if (response.status >= 400 && response.status < 500) { + throw new ValidationError(message, response.status); + } throw new Error(message); } @@ -326,6 +349,7 @@ const updateProfile = async( }; export { + ValidationError, achieveApotheosis, buyEchoUpgrade, buyPrestigeUpgrade, diff --git a/apps/web/src/components/game/aboutPanel.tsx b/apps/web/src/components/game/aboutPanel.tsx index 7fc175b..e51b40a 100644 --- a/apps/web/src/components/game/aboutPanel.tsx +++ b/apps/web/src/components/game/aboutPanel.tsx @@ -277,6 +277,15 @@ const howToPlay = [ + " when you first enable them.", title: "🔔 Sounds & Notifications", }, + { + body: + "Have a question, found a bug, or want to suggest a feature? Join the" + + " NHCarrigan community Discord at https://chat.nhcarrigan.com or open" + + " a support ticket at https://support.nhcarrigan.com. You can also" + + " report issues directly on the project repository. We'd love to hear" + + " from you!", + title: "💬 Community & Support", + }, ]; const formatDate = (dateString: string): string => { diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index 3b43a25..acfc0ff 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -162,7 +162,7 @@ const ResourceBar = ({ title="Click to see all resources" type="button" > - {"🪙"} + {"💰"} {formatNumber(gold)} {"Gold"} {goldFull diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 42fe2c5..8738d15 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -4586,6 +4586,7 @@ body::before { height: 220px; margin-bottom: 1rem; object-fit: cover; + object-position: top; width: 100%; } diff --git a/apps/web/src/utils/logError.ts b/apps/web/src/utils/logError.ts index 61b8089..ff295e2 100644 --- a/apps/web/src/utils/logError.ts +++ b/apps/web/src/utils/logError.ts @@ -5,14 +5,22 @@ * @author Naomi Carrigan */ /* eslint-disable no-console -- Errors are forwarded to backend via the overridden console.error */ +import { ValidationError } from "../api/client.js"; /** * Logs an error to the backend telemetry service. - * Accepts the same arguments as console.error — conventionally a context string - * followed by the error value. - * @param logArguments - The values to log, forwarded directly to console.error. + * ValidationErrors (4xx API rejections) are downgraded to console.warn so they + * are not forwarded to the error-email pipeline — they are expected server responses. + * @param logArguments - The values to log, forwarded directly to console.error or console.warn. */ const logError = (...logArguments: Array): void => { + const isValidation = logArguments.some((argument) => { + return argument instanceof ValidationError; + }); + if (isValidation) { + console.warn(...logArguments); + return; + } console.error(...logArguments); };