Each row spends up to 3 s on the lookupIpLocation online fallback;
5000 rows × 3 s let a single backfill burst block for ~4 h and starve
the next hourly cron. Tighten the cap to 500 so the worst-case pass
fits well inside the hour-long schedule slot. The helper is
idempotent, so a tenant with a large null-location backlog drains
over multiple hourly passes instead of one mega-pass.
Marc directive: when the operator turns the global Coach flag off,
every Coach affordance on every surface must vanish — no grey-out,
no error, no inert button, no DOM trace. The recent v1.4.37 W5
hotfixes closed three holes the UX audit caught (HeroStrip action
button, SuggestedPrompts chip strip, /targets CoachDrawer mount); a
follow-up audit item asked for an automated invariant so the gate
contract can't regress.
Walk every Coach-bearing web surface (HeroStrip action row,
SuggestedPrompts chips, CoachLaunchButton inline pill, LayoutCoachFab
mobile sticky, LayoutCoachMount drawer subtree) under a QueryClient
pre-seeded with `coach: false` and assert each one paints nothing
measurable in the SSR output. The fixture maintains a known surface
list; a trailing count check forces contributors to revisit the
fixture when a new Coach-bearing component lands.
`buildCoachSnapshot()` reads only persisted data — ~10 measurement
findManys plus the GLP-1 / mood / intake side-fetches — yet a single
chat conversation sends 2-4 turns within a minute and rebuilds the
same snapshot every time. Wrap the existing builder (renamed to
`buildCoachSnapshotImpl`) with an in-memory cache keyed on
`(userId, scope.window, sorted scope.sources)` with a 60 s TTL and a
64-entry LRU cap. Different scopes recompute; the cache evicts on the
next eligible read.
`.planning/round-v1433-audit-perf.md` §3.3 estimates 200-800 ms of
server-side tail saved on the second+ turn — the bulk of the
"blockt komplett" complaint between user-clicks-send and first SSE
`token` frame. The `__resetCoachSnapshotCacheForTests` export gives
the existing two snapshot-test suites a hook so each fixture is read
fresh; both add a `beforeEach` reset alongside the existing
`vi.clearAllMocks()` call. Two new tests pin the cache-hit (one
Prisma findMany call across two `buildCoachSnapshot` calls with the
same scope) and the scope-changed miss (different `window` → two
findManys). Feature-flag tests gain a parallel "per-request
memoisation" describe-block that pins the same behaviour for
`getAssistantFlags()` against a stable mocked event reference.
Two server-side latency cuts on the Coach drawer open / first turn path.
1. **Per-request memo on `getAssistantFlags()`.** Opening the Coach
drawer fires five gated fetches in parallel — `/api/insights/chat`,
the rail list, `/api/insights/comprehensive`, plus the per-metric
status calls already on the page — and every gated route lands a
`requireAssistantSurface(surface)` call near the top. Today each
call issues its own `AppSettings.singleton` `SELECT`. The new
`src/lib/request-cache.ts` carrier keys a `Map` on the active
`WideEventBuilder` (already in `AsyncLocalStorage` via the
api-handler) so the same request reads the row once. Outside an
event context (unit tests, background jobs) the factory runs
uncached so behaviour is unchanged.
2. **60-second in-memory LRU on `buildCoachSnapshot()`.** A chat
conversation sends 2-4 turns within a minute and the snapshot reads
only persisted data — ~10 measurement reads + GLP-1 + mood + intake
side-fetches per turn. Cache the result keyed on
`(userId, scope.window, sorted scope.sources)` with a 60 s TTL and
a 64-entry LRU cap. Different scopes recompute; the cache evicts
on the next eligible read. Tests exercise both the hit and the
scope-changed miss; legacy tests gain an explicit reset between
describe blocks.
`.planning/round-v1433-audit-perf.md` §3.3 estimates 200-800 ms of
server-side tail saved on the second+ turn and roughly 4×
`AppSettings.singleton` reads avoided on a cold Coach-drawer open.
Operator-side kill matrix for every LLM-driven surface. Master flag
`assistantEnabled` plus five sub-flags carve the seven surfaces:
Coach + chat, Daily Briefing + advisor, per-metric status cards,
correlation narration, Health-Score delta explainer. Defaults are
`true` so existing installs upgrade without losing affordances.
New `src/lib/feature-flags/` module exposes `getAssistantFlags()`,
`resolveAssistantFlags()` and the `AssistantDisabledError` typed
error route handlers throw to surface the 403 +
`errorCode: "assistant.disabled.<surface>"` envelope. The resolver
forces every sub-flag false when the master is off so callers never
have to compose `master && sub`.
Locks per `.planning/RESPONSE-TO-IOS-TEAM-2026-05-16.md` §3 R5:
the same matrix gates server-routed AND iOS on-device surfaces.
Migration 0065 is additive only; `app_settings` gains six boolean
columns, all default `true`, idempotent under `IF NOT EXISTS`.