diff --git a/apps/api/src/inventions-db.ts b/apps/api/src/inventions-db.ts index 5ea4491..75e2037 100644 --- a/apps/api/src/inventions-db.ts +++ b/apps/api/src/inventions-db.ts @@ -352,23 +352,29 @@ async function publicInventions(db: D1Database, featuredOnly = false): Promise JSON.parse(r.data) as SavedInvention) } -/** Midnight UTC today, as the ISO timestamp `acquired_at` is compared against. */ -function startOfUtcDay(): string { - return `${new Date().toISOString().slice(0, 10)}T00:00:00.000Z` +/** Length of the "today" window — a trailing day, not the calendar one. */ +const TOP_TODAY_WINDOW_MS = 24 * 60 * 60 * 1000 + +/** 24 hours ago, as the ISO timestamp `acquired_at` is compared against. */ +function startOfWindow(): string { + return new Date(Date.now() - TOP_TODAY_WINDOW_MS).toISOString() } /** - * The "top today" feed — the inventions other players picked up TODAY, most first. + * The "top today" feed — the inventions other players picked up in the last 24 hours, + * most first. * * Ranked from the acquisitions the `econ` worker records in `inventory_invention` at * purchase time, grouped by invention, rather than from the lifetime counters on the * invention itself: those never reset, so "top today" used to mean "top ever" and the * shelf only changed when something overtook a total built up over months. * - * "Today" is the UTC day, matching the timestamps econ writes. The day therefore rolls - * over at 00:00 UTC wherever the player is, and the feed IS EMPTY until the first - * acquisition of that day — nothing stands in for it, the same way the featured feed - * serves nothing while nothing is curated. + * "Today" is a TRAILING 24 hours, not the calendar UTC day, so the feed doesn't empty + * itself at midnight UTC and slowly refill through the small hours — it always covers a + * full day's worth of activity. It is still genuinely a window: an invention nobody has + * picked up since yesterday falls off, and the feed IS EMPTY when nothing at all was + * acquired in a day. Nothing stands in for it, the same way the featured feed serves + * nothing while nothing is curated. * * An acquired invention that has since been unpublished or hidden drops out: this is a * public feed, so it is filtered like every other one. Paginated via skip/take AFTER @@ -379,7 +385,7 @@ export async function getTopInventions( skip: number, take: number ): Promise { - const counts = await getInventionAcquisitionCounts(db, startOfUtcDay()) + const counts = await getInventionAcquisitionCounts(db, startOfWindow()) if (counts.length === 0) return [] // getInventionsByIds answers in the order it is asked, so the ranking survives the diff --git a/apps/api/src/routes/avatar.ts b/apps/api/src/routes/avatar.ts index b1b1209..a0cfda9 100644 --- a/apps/api/src/routes/avatar.ts +++ b/apps/api/src/routes/avatar.ts @@ -622,7 +622,7 @@ export const avatarRoutes = new Hono({ strict: false }) } ) - // The "top today" invention feed — the inventions most acquired since 00:00 UTC, + // The "top today" invention feed — the inventions most acquired in the last 24 hours, // counted from the purchase rows the `econ` worker writes. A real day window, so an // empty list is a quiet day rather than a bug. Paginated via skip/take (take defaults // to 50, as the client asks for). Bare array. @@ -632,10 +632,11 @@ export const avatarRoutes = new Hono({ strict: false }) tags: ['Inventions'], summary: 'The “top today” feed', description: - 'Published inventions ranked by how many players acquired them TODAY (since ' + - '00:00 UTC), counted from the purchase records — free grants included, one per ' + - 'player per invention. Genuinely a day window: empty until the day’s first ' + - 'acquisition, and it resets at midnight UTC.', + 'Published inventions ranked by how many players acquired them in the last 24 ' + + 'hours, counted from the purchase records — free grants included, one per ' + + 'player per invention. Genuinely a window: an invention nobody has picked up ' + + 'since yesterday falls off, and a day with no acquisitions at all serves an ' + + 'empty list. It trails the clock rather than resetting at midnight.', parameters: pageParams(50), responses: { 200: json(InventionDto.array(), 'The top inventions') }, }), diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index ed1c69e..066967b 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -1184,22 +1184,23 @@ describe('public endpoints', () => { .run() } - // Today's acquisitions, which is what "top today" now counts: 201 picked up by three + // Recent acquisitions, which is what "top today" now counts: 201 picked up by three // players, 203 by one. 204/205 are acquired too — an unpublished and a hidden // invention can still be owned — and must not surface in a public feed. for (const accountId of [7001, 7002, 7003]) await grantInvention(env.DB, accountId, 201) await grantInvention(env.DB, 7001, 203) await grantInvention(env.DB, 7001, 204) await grantInvention(env.DB, 7002, 205) - // 202 was acquired, but not today — the window is the current UTC day, so it is out. + // 202 was acquired 25 hours ago, just past the trailing 24-hour window, so it is out — + // the feed really does forget, rather than accumulating every acquisition ever. await env.DB.prepare( 'INSERT INTO inventory_invention (account_id, invention_id, acquired_at) VALUES (?1, ?2, ?3)' ) - .bind(7004, 202, '2020-01-01T00:00:00.000Z') + .bind(7004, 202, new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString()) .run() - // Top: most acquisitions today first. Download counts no longer rank anything — 202 - // has the biggest of them and is absent entirely. + // Top: most acquisitions in the window first. Download counts no longer rank anything — + // 202 has the biggest of them and is absent entirely. const top = await ids(await exports.default.fetch(`${ORIGIN}/api/inventions/v1/toptoday`)) expect(top).toEqual([201, 203]) diff --git a/packages/domain/src/inventory-invention-db.ts b/packages/domain/src/inventory-invention-db.ts index b2c0ffb..59c928a 100644 --- a/packages/domain/src/inventory-invention-db.ts +++ b/packages/domain/src/inventory-invention-db.ts @@ -65,7 +65,7 @@ export async function ownsInvention( /** * How many times each invention was acquired at or after `since`, most-acquired first * (ties broken by newest invention, so paging is stable). Backs the `api` worker's "top - * today" feed, which passes the start of the current UTC day. + * today" feed, which passes 24 hours ago. * * `acquired_at` holds `toISOString()` output, which is fixed-width UTC, so a lexical * `>=` on the string is a chronological comparison — no date parsing in SQL.