docs(planning): record v1.4.20.1 hotfix wave

New `.planning/phase-v1421-hotfix-report.md` summarises the six
fixes (Daily Briefing regenerate · Coach day-level snapshot · Coach
prompt update · streaming-bubble dedup · drawer header · sources rail
scope picker), the test-count delta (2026 → 2036 unit), the
token-budget impact of the day-level snapshot (~190 → ~3000 tokens
per full-scope turn), and a recommendation to ship as a v1.4.20.1
hotfix.

STATE.md grows a Phase F section + status block tracking the six
commits on develop. Includes prettier polish across the touched
coach surfaces (sources-rail, drawer, message-thread, snapshot,
chat route).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 19:31:20 +02:00
1 parent 08fd411ff1
commit 52dd9ff9c3
9 files changed
+253 -60

No files matched your search

+1 -6
View File
@@ -143,12 +143,7 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
{ status: 422 },
);
}
const {
conversationId,
message,
locale: bodyLocale,
scope,
} = parsed.data;
const { conversationId, message, locale: bodyLocale, scope } = parsed.data;
await enforceBudget(userId);
@@ -53,9 +53,8 @@ describe("<SourcesRail>", () => {
it("renders a checkbox per row + a window selector trigger", () => {
const html = render(<SourcesRail />);
const checkboxes = (
html.match(/data-slot="coach-sources-checkbox"/g) ?? []
).length;
const checkboxes = (html.match(/data-slot="coach-sources-checkbox"/g) ?? [])
.length;
expect(checkboxes).toBe(5);
expect(html).toContain('data-slot="coach-sources-window-trigger"');
});
@@ -258,10 +258,7 @@ export function CoachDrawer({
}
sourcesRail={
sourcesRail ?? (
<SourcesRail
scope={scope}
onScopeChange={setScope}
/>
<SourcesRail scope={scope} onScopeChange={setScope} />
)
}
thread={
@@ -67,9 +67,7 @@ export function MessageThread({
messages.some((m) => m.id === streaming.messageId);
const streamingActive =
!streamingPersisted &&
(!!streaming?.inProgress ||
!!streaming?.content ||
!!streaming?.errorCode);
(!!streaming?.inProgress || !!streaming?.content || !!streaming?.errorCode);
// Track scroll position so we don't yank the viewport when the user
// is browsing earlier turns.
@@ -82,7 +82,10 @@ export interface SourcesRailProps {
* the controls still render but stay read-only — keeps the legend
* surface stable for callers that don't yet wire scope through.
*/
onScopeChange?: (next: { sources: CoachScopeSource[]; window: CoachScopeWindow }) => void;
onScopeChange?: (next: {
sources: CoachScopeSource[];
window: CoachScopeWindow;
}) => void;
}
interface SourceRow {
+5 -1
View File
@@ -27,7 +27,11 @@ const featuresMock = extractFeatures as unknown as ReturnType<typeof vi.fn>;
/**
* Helper: produce a measurement row N days before "now" at 09:00 UTC.
*/
function daysAgo(n: number, value: number, type: string): {
function daysAgo(
n: number,
value: number,
type: string,
): {
type: string;
value: number;
measuredAt: Date;
+14 -23
View File
@@ -53,15 +53,7 @@ const DEFAULT_SOURCES: ReadonlyArray<CoachScopeSource> = [
"compliance",
];
const WEEKDAY_KEYS = [
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
] as const;
const WEEKDAY_KEYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
function windowToDays(window: CoachScopeWindow): number {
switch (window) {
@@ -163,7 +155,9 @@ function bucketWeekly(
return Array.from(grouped.entries())
.map(([weekISO, values]) => ({
weekISO,
mean: Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 10) / 10,
mean:
Math.round((values.reduce((s, v) => s + v, 0) / values.length) * 10) /
10,
count: values.length,
}))
.sort((a, b) => a.weekISO.localeCompare(b.weekISO));
@@ -177,8 +171,7 @@ function buildDailyValueRows(
const grouped = dailyMeans(recent);
return Array.from(grouped.entries())
.map(([date, info]) => {
const mean =
info.values.reduce((s, v) => s + v, 0) / info.values.length;
const mean = info.values.reduce((s, v) => s + v, 0) / info.values.length;
return {
date,
weekday: utcWeekday(info.date),
@@ -206,10 +199,8 @@ function buildDailyBpRows(
for (const [day, info] of sysByDay) {
const dia = diaByDay.get(day);
if (!dia) continue;
const sysMean =
info.values.reduce((s, v) => s + v, 0) / info.values.length;
const diaMean =
dia.values.reduce((s, v) => s + v, 0) / dia.values.length;
const sysMean = info.values.reduce((s, v) => s + v, 0) / info.values.length;
const diaMean = dia.values.reduce((s, v) => s + v, 0) / dia.values.length;
out.push({
date: day,
weekday: utcWeekday(info.date),
@@ -310,10 +301,12 @@ export async function buildCoachSnapshot(
: [];
const byType = (t: string) =>
measurementRows.filter((r) => r.type === t).map((r) => ({
measuredAt: r.measuredAt,
value: r.value,
}));
measurementRows
.filter((r) => r.type === t)
.map((r) => ({
measuredAt: r.measuredAt,
value: r.value,
}));
if (wantsBp && features.bloodPressure) {
const sysRows = byType("BLOOD_PRESSURE_SYS");
@@ -405,9 +398,7 @@ export async function buildCoachSnapshot(
// Per-day adherence rate within the recent window. Older days
// collapse into a single weekly bucket.
const recent = intakeRows.filter((r) => r.scheduledFor >= recentCutoff);
const olderRows = intakeRows.filter(
(r) => r.scheduledFor < recentCutoff,
);
const olderRows = intakeRows.filter((r) => r.scheduledFor < recentCutoff);
const recentByDay = new Map<
string,
{ date: Date; total: number; taken: number }