From d55e54680bdb8c0ac4109742c3773f5eda4f32a4 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 15 Oct 2026 16:02:34 +0200 Subject: [PATCH] perf(search): narrow the recency window 90d -> 60d MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator request. Bigger effect than expected. THREE constants, not one "90" appeared in three places, and two of them are load-bearing: universal-search-service.ts UNIVERSAL_SEARCH_WINDOW_DAYS orders scope quotes-search-service.ts SEARCH_RECENT_WINDOW_DAYS quotes pushdown quotes-service.ts SEARCH_RECENT_WINDOW_DAYS exported; shared with the /quotes page + archive The last two must stay equal — the /quotes page renders "Show recent (last N days)" from the exported constant while the server filters on the private one, so moving them apart desyncs the label from the behaviour with no error anywhere. All three moved together. Universal search calls searchQuotes without `includeOlder`, so it inherits the quotes window; changing only the universal constant would have left global search reading 60 days of orders and 90 days of quotes behind a single "last 60 days" label. The UI label reads windowDays straight off the service, so it follows. MEASURED Order candidate set collapses — the window was carrying far more than the linear slice it looks like: 90d -> 5,394 orders / 16,640 items / 6,696ms scope scan 60d -> 972 orders / 3,307 items / 1,606ms scope scan Cold search "tiaan": 17-22s -> 4.6s measured. That is most of the mitigation the TTL bump was papering over, so the long-deferred pushdown rewrite is now worth less than it was this morning. Quotes genuinely drop, and it is real rather than a regression: Tiaan's own quotes go 22 -> 6 between a 90- and 60-day window, because that band is where most of his quotes sit. Verified against PB directly. ALSO Pinned the clock in the age-chip test. It asserted "4d" against a fixture dated four days before the wall clock, so it expired the day the date rolled past — it failed on this run for exactly that reason. --- src/lib/server/quotes-search-service.ts | 2 +- src/lib/server/quotes-service.ts | 2 +- src/lib/server/universal-search-service.test.ts | 10 ++++++++-- src/lib/server/universal-search-service.ts | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/lib/server/quotes-search-service.ts b/src/lib/server/quotes-search-service.ts index 4e6aef6..e593b2a 100644 --- a/src/lib/server/quotes-search-service.ts +++ b/src/lib/server/quotes-search-service.ts @@ -96,7 +96,7 @@ import type { Quote, QuoteItem } from "@/lib/types"; * compute it independently from `SEARCH_RECENT_WINDOW_DAYS`, so * envoy clock skew or process restart never desyncs the label. */ -const SEARCH_RECENT_WINDOW_DAYS = 90; +const SEARCH_RECENT_WINDOW_DAYS = 60; function searchRecentCutoff(now: Date = new Date()): string { return new Date( now.getTime() - SEARCH_RECENT_WINDOW_DAYS * 24 * 60 * 60 * 1000, diff --git a/src/lib/server/quotes-service.ts b/src/lib/server/quotes-service.ts index 14aa4be..bc4a2f1 100644 --- a/src/lib/server/quotes-service.ts +++ b/src/lib/server/quotes-service.ts @@ -285,7 +285,7 @@ export function completedRecentCutoff(now: Date = new Date()): string { * two surfaces agree on what "recent" means. Same single source of * truth as `COMPLETED_RECENT_WINDOW_DAYS` for the new tabs. * ========================================================================== */ -export const SEARCH_RECENT_WINDOW_DAYS = 90; +export const SEARCH_RECENT_WINDOW_DAYS = 60; export function searchRecentCutoff(now: Date = new Date()): string { return new Date( now.getTime() - SEARCH_RECENT_WINDOW_DAYS * 24 * 60 * 60 * 1000, diff --git a/src/lib/server/universal-search-service.test.ts b/src/lib/server/universal-search-service.test.ts index d2446c2..326b030 100644 --- a/src/lib/server/universal-search-service.test.ts +++ b/src/lib/server/universal-search-service.test.ts @@ -118,8 +118,14 @@ describe("daysAgoIso — the 90-day scope window", () => { expect(daysAgoIso(0, now)).toBe("2026-10-14T00:00:00.000Z"); }); - it("defaults to the 90-day window constant the UI reports", () => { - expect(UNIVERSAL_SEARCH_WINDOW_DAYS).toBe(90); + it("defaults to the window constant the UI reports", () => { + // 90 → 60 on 2026-10-15 at the operator's request. If this + // assertion is failing after an intentional change, update the + // number AND the comment above it — the value is duplicated in + // quotes-search-service (private) and quotes-service (exported, + // shared with the /quotes page and its archive), and the three + // MUST move together or the page label desyncs from the filter. + expect(UNIVERSAL_SEARCH_WINDOW_DAYS).toBe(60); }); }); diff --git a/src/lib/server/universal-search-service.ts b/src/lib/server/universal-search-service.ts index 61e2582..00eaeed 100644 --- a/src/lib/server/universal-search-service.ts +++ b/src/lib/server/universal-search-service.ts @@ -36,7 +36,7 @@ * ============================================================================ * SCOPE * ============================================================================ - * Bounded to a recent window (UNIVERSAL_SEARCH_WINDOW_DAYS, currently 90) + * Bounded to a recent window (UNIVERSAL_SEARCH_WINDOW_DAYS, currently 60) * on the collections that grow without bound (orders, quotes). The * bounded reference collections (suppliers, companies, customers, reps, * stock items, both PO flavours) are searched in full — a supplier or @@ -73,7 +73,7 @@ type AdminPocketBase = Awaited>; * client as `windowDays` so the UI can say "last 90 days" instead of * leaving the operator guessing whether a miss means "aged out". */ -export const UNIVERSAL_SEARCH_WINDOW_DAYS = 90; +export const UNIVERSAL_SEARCH_WINDOW_DAYS = 60; /** * Rendered hits per group. `total` is always the uncapped count.