From d433eeee5035d117abdebc864b164d64294fec6d Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 27 Aug 2026 16:13:45 +0200 Subject: [PATCH] Only claim a derived boundary is a source's when boundaryMs treats it that way MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The day-precision note ("The source gave a date but no time of day, so this counts down to that day's server reset...") rendered for any event with endPrecision "day" and a stated end, with no check on where the date came from. For a reader's own event that is false three times over: there is no source (sourceUrl is null, sourceId is "you"), nobody gave a date — for a repeating occurrence with no stated end the app derived it from the interval — and it does not count down to a server reset, since boundaryMs only applies that shift when extractionMethod === "parser". Gated the note on the same condition so the copy and the countdown agree. Co-Authored-By: Claude Opus 5 (1M context) --- src/client/components/EventDetail.tsx | 11 +++- test/custom-ui.test.tsx | 80 +++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/client/components/EventDetail.tsx b/src/client/components/EventDetail.tsx index 3c53703..f1941a7 100644 --- a/src/client/components/EventDetail.tsx +++ b/src/client/components/EventDetail.tsx @@ -281,7 +281,16 @@ export function EventDetail({ )} - {event.endPrecision === "day" && event.endsAt !== null && ( + {/* Gated on the same condition `boundaryMs` uses to apply the reset + shift, so the copy never claims a countdown the clock is not + actually running. A reader's own event is day-precision exactly as + often as a parsed one, but `extractionMethod` is "manual" — there + is no source to have "given" a date, and `readerInstant` already + resolved it to the instant they meant rather than a placeholder + `boundaryMs` has to reinterpret. */} + {event.endPrecision === "day" && + event.endsAt !== null && + event.extractionMethod === "parser" && (

The source gave a date but no time of day, so this counts down to that day's {REGION_LABEL[region]} server reset — where these usually land, but a diff --git a/test/custom-ui.test.tsx b/test/custom-ui.test.tsx index c602dde..93b3cc7 100644 --- a/test/custom-ui.test.tsx +++ b/test/custom-ui.test.tsx @@ -7,6 +7,7 @@ import { } from "../src/client/components/CustomForms.tsx"; import { YourOwn } from "../src/client/components/YourOwn.tsx"; import { EventRow } from "../src/client/components/EventRow.tsx"; +import { EventDetail } from "../src/client/components/EventDetail.tsx"; import { AUTHOR, Colophon, REPO_URL } from "../src/client/components/Colophon.tsx"; import { GameMetaProvider } from "../src/client/state/gameMeta.tsx"; import { @@ -14,6 +15,7 @@ import { CustomEvent, type CustomEvents, type CustomGames, + type DisplayEvent, } from "../src/shared/custom.ts"; import { metaFor } from "../src/shared/games.ts"; import { clockFor } from "../src/shared/time.ts"; @@ -468,3 +470,81 @@ describe("the sheet says how often", () => { expect(cadenceLabel(null)).toBe(null); }); }); + +describe("the derived-boundary note", () => { + const NOW = Date.parse("2026-08-25T12:00:00.000Z"); + + const noop = () => {}; + const detailProps = { + completed: false, + ignored: false, + status: undefined, + effort: undefined, + note: "", + region: "europe" as const, + now: NOW, + daily: false, + detectedDaily: false, + dailyDays: [], + onDaily: noop, + onToggleDay: noop, + onIgnore: noop, + onStatus: noop, + onEffort: noop, + onNote: noop, + onClose: noop, + }; + + // A parser declining to guess a time of day — the case the note was written + // for. `dates.ts` stores the placeholder as 00:00Z, and `boundaryMs` reads + // it against the game's own server reset rather than literally. + const PARSED: DisplayEvent = { + id: "genshin:walpurgisnacht:2026-09-03", + game: "genshin", + title: "Walpurgisnacht", + type: "banner", + summary: null, + startsAt: "2026-08-20T00:00:00.000Z", + startPrecision: "day", + endsAt: "2026-09-03T00:00:00.000Z", + endPrecision: "day", + regionScoped: false, + regionEnds: null, + sourceUrl: "https://example.com/events", + sourceId: "genshin-game8-events", + status: "published", + confidence: 1, + extractionMethod: "parser", + version: 1, + firstSeenAt: AT, + updatedAt: AT, + }; + + test("present for a parser-sourced day-precision event", () => { + const html = renderToStaticMarkup( + metaFor(id, GAMES)}> + + , + ); + expect(html).toContain("server reset"); + }); + + test("absent for a reader's own day-precision event", () => { + // False three times over: no source, nobody "gave" a date, and + // `boundaryMs` only applies the reset shift for `extractionMethod === + // "parser"` — a reader's own event is "manual" even at day precision. + const own = asDisplayEvent(OWN); + const html = renderToStaticMarkup( + metaFor(id, GAMES)}> + + , + ); + expect(html).not.toContain("server reset"); + }); +});