mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
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:
1 parent
08fd411ff1
commit
52dd9ff9c3
9 files changed
+253
-60
No files matched your search
+58
-19
@@ -85,17 +85,17 @@ ships behind a feature flag and unlocks independently.
|
|||||||
action row + suggested-prompt strip; Daily Briefing card mounts
|
action row + suggested-prompt strip; Daily Briefing card mounts
|
||||||
full-width below the hero)
|
full-width below the hero)
|
||||||
- [x] New `src/components/insights/{hero-strip,daily-briefing,
|
- [x] New `src/components/insights/{hero-strip,daily-briefing,
|
||||||
suggested-prompts}.tsx`
|
suggested-prompts}.tsx`
|
||||||
- [x] Extend `aiInsightResponseSchema` with `dailyBriefing`
|
- [x] Extend `aiInsightResponseSchema` with `dailyBriefing`
|
||||||
(`paragraph`, `keyFindings[]`)
|
(`paragraph`, `keyFindings[]`)
|
||||||
- [x] PROMPT_VERSION bump 4.19.0 → 4.20.0
|
- [x] PROMPT_VERSION bump 4.19.0 → 4.20.0
|
||||||
- [~] Apple Health gated tiles — descoped per `phase-D-v1419-product-
|
- [~] Apple Health gated tiles — descoped per `phase-D-v1419-product-
|
||||||
lead-review.md` decision. The 4-vitals tile row from the artboard
|
lead-review.md` decision. The 4-vitals tile row from the artboard
|
||||||
uses BP / Weight / Pulse / Mood (all data already in the app);
|
uses BP / Weight / Pulse / Mood (all data already in the app);
|
||||||
HRV / Sleep / Resting-HR tiles defer to v1.5 / iOS app. The B1
|
HRV / Sleep / Resting-HR tiles defer to v1.5 / iOS app. The B1
|
||||||
tile row itself (Vitals at-a-glance) is deferred to a later
|
tile row itself (Vitals at-a-glance) is deferred to a later
|
||||||
B-phase since it duplicates the per-section status cards below
|
B-phase since it duplicates the per-section status cards below
|
||||||
— see B1 report for the rationale.
|
— see B1 report for the rationale.
|
||||||
- Detailed report: `.planning/phase-B1-v1420-report.md` (the older
|
- Detailed report: `.planning/phase-B1-v1420-report.md` (the older
|
||||||
`phase-B1-report.md` is from the v1.4.18 achievements marathon)
|
`phase-B1-report.md` is from the v1.4.18 achievements marathon)
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ commit.
|
|||||||
|
|
||||||
- [x] New `POST /api/insights/chat` SSE-streaming endpoint
|
- [x] New `POST /api/insights/chat` SSE-streaming endpoint
|
||||||
- [x] New `GET /api/insights/chat` (list) + `GET/DELETE
|
- [x] New `GET /api/insights/chat` (list) + `GET/DELETE
|
||||||
/api/insights/chat/[id]` (single + delete)
|
/api/insights/chat/[id]` (single + delete)
|
||||||
- [x] New Prisma `CoachConversation` + `CoachMessage` + `CoachUsage`
|
- [x] New Prisma `CoachConversation` + `CoachMessage` + `CoachUsage`
|
||||||
(encrypted content, GDPR cascade)
|
(encrypted content, GDPR cascade)
|
||||||
- [x] Source-chip provenance attached to every assistant message
|
- [x] Source-chip provenance attached to every assistant message
|
||||||
@@ -130,14 +130,14 @@ commit.
|
|||||||
- [x] New `src/lib/insights/correlations.ts` (Pearson + weekday-ANOVA;
|
- [x] New `src/lib/insights/correlations.ts` (Pearson + weekday-ANOVA;
|
||||||
n >= 14 + p < 0.05 surfacing gate)
|
n >= 14 + p < 0.05 surfacing gate)
|
||||||
- [x] New `src/components/insights/{correlation-card,correlation-row,
|
- [x] New `src/components/insights/{correlation-card,correlation-row,
|
||||||
trends-row,trend-annotation}.tsx`
|
trends-row,trend-annotation}.tsx`
|
||||||
- [x] Confidence interval via Fisher z-transform; conservative
|
- [x] Confidence interval via Fisher z-transform; conservative
|
||||||
phrasing locked in by code-review convention
|
phrasing locked in by code-review convention
|
||||||
- [x] PROMPT_VERSION 4.20.0 → 4.20.1; new `trendAnnotations` block
|
- [x] PROMPT_VERSION 4.20.0 → 4.20.1; new `trendAnnotations` block
|
||||||
on the AI response schema (nullable + optional for legacy
|
on the AI response schema (nullable + optional for legacy
|
||||||
payloads)
|
payloads)
|
||||||
- [x] `/api/analytics` now emits `correlations: {bpCompliance,
|
- [x] `/api/analytics` now emits `correlations: {bpCompliance,
|
||||||
moodPulse, weightWeekday}` server-side
|
moodPulse, weightWeekday}` server-side
|
||||||
- [x] `<CorrelationRow>` + `<TrendsRow>` mounted on `/insights`
|
- [x] `<CorrelationRow>` + `<TrendsRow>` mounted on `/insights`
|
||||||
between Daily Briefing and the Advisor card
|
between Daily Briefing and the Advisor card
|
||||||
- Detailed report: `.planning/phase-B3-report.md`
|
- Detailed report: `.planning/phase-B3-report.md`
|
||||||
@@ -175,14 +175,14 @@ commit.
|
|||||||
|
|
||||||
Wave B closed the Insights redesign across 5 phases on `develop`:
|
Wave B closed the Insights redesign across 5 phases on `develop`:
|
||||||
|
|
||||||
| Phase | Theme | Commits | Test count delta (unit / int) |
|
| Phase | Theme | Commits | Test count delta (unit / int) |
|
||||||
| ----- | ------------------------------------------ | --------- | ----------------------------- |
|
| ----- | ------------------------------------------ | ------- | ----------------------------- |
|
||||||
| B1 | Hero strip + Daily Briefing + Suggested | 5 | 1672 → 1753 (+81) / 67 → 67 |
|
| B1 | Hero strip + Daily Briefing + Suggested | 5 | 1672 → 1753 (+81) / 67 → 67 |
|
||||||
| B2 | AI Coach drawer (B2a backend + B2b UI) | 5 + 4 | 1753 → 1833 (+80) / 67 → 78 |
|
| B2 | AI Coach drawer (B2a backend + B2b UI) | 5 + 4 | 1753 → 1833 (+80) / 67 → 78 |
|
||||||
| B3 | Correlation discovery + Trends row + AI | 5 | 1833 → 1907 (+74) / 78 → 78 |
|
| B3 | Correlation discovery + Trends row + AI | 5 | 1833 → 1907 (+74) / 78 → 78 |
|
||||||
| B4 | Weekly Report + Storyboard + Mobile passes | 5 | 1907 → 1975 (+68) / 78 → 78 |
|
| B4 | Weekly Report + Storyboard + Mobile passes | 5 | 1907 → 1975 (+68) / 78 → 78 |
|
||||||
| B5 | Personal Health Score | 5 | 1975 → 2026 (+51) / 78 → 81 |
|
| B5 | Personal Health Score | 5 | 1975 → 2026 (+51) / 78 → 81 |
|
||||||
| | **Cumulative** | **29** | **+354 / +14** |
|
| | **Cumulative** | **29** | **+354 / +14** |
|
||||||
|
|
||||||
Test count: 1672 → 2026 unit (+354), 67 → 81 integration (+14).
|
Test count: 1672 → 2026 unit (+354), 67 → 81 integration (+14).
|
||||||
Test files 217 → 237. typecheck clean every phase; lint
|
Test files 217 → 237. typecheck clean every phase; lint
|
||||||
@@ -225,7 +225,7 @@ Test files 217 → 237. typecheck clean every phase; lint
|
|||||||
JSON-LD `softwareVersion` 1.4.20 + 3 new featureList entries)
|
JSON-LD `softwareVersion` 1.4.20 + 3 new featureList entries)
|
||||||
- [x] `docs/audit/v1420-summary.md` (release brief, `7f9ae21`)
|
- [x] `docs/audit/v1420-summary.md` (release brief, `7f9ae21`)
|
||||||
- [x] v1.5 plan refreshed — see `.planning/phase-D-v1420-product-
|
- [x] v1.5 plan refreshed — see `.planning/phase-D-v1420-product-
|
||||||
lead-review.md`
|
lead-review.md`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -295,6 +295,45 @@ Test files 217 → 237. typecheck clean every phase; lint
|
|||||||
`coach-chat.test.ts` "round-trips" (not introduced by Phase-D —
|
`coach-chat.test.ts` "round-trips" (not introduced by Phase-D —
|
||||||
consistent re-runs pass). Phase E (release v1.4.20) unblocked.
|
consistent re-runs pass). Phase E (release v1.4.20) unblocked.
|
||||||
|
|
||||||
|
## Phase F — v1.4.20.1 hotfix
|
||||||
|
|
||||||
|
Triggered by Marc's testing of the new Insights/Coach surface in
|
||||||
|
production after v1.4.20 went live. Five issues captured in
|
||||||
|
`feedback_v1420_post_deploy_bugs.md` — landed across six atomic
|
||||||
|
commits on `develop`. No tag yet; maintainer decides whether to ship
|
||||||
|
as v1.4.20.1 or bundle into v1.4.21.
|
||||||
|
|
||||||
|
- [x] Daily Briefing regenerate produces a non-empty briefing
|
||||||
|
(`/api/insights/generate` now uses `getStrictInsightsSystemPrompt`
|
||||||
|
so GROUND RULE 8 reaches the model — `7921ffc`)
|
||||||
|
- [x] Coach snapshot ships day-level readings with weekday labels
|
||||||
|
and a per-day medication-adherence row (`ed61b17`)
|
||||||
|
- [x] Coach system prompt instructs the model to use the timeline
|
||||||
|
block for day-specific or weekday-specific questions
|
||||||
|
(`2143377`)
|
||||||
|
- [x] Streaming bubble clears once the persisted twin lands —
|
||||||
|
message-thread render-time dedup keyed on `messageId`
|
||||||
|
(`f07e35b`)
|
||||||
|
- [x] Drawer header settings cog moved to the left side so Radix
|
||||||
|
Sheet's close-X no longer overlaps it (`ddb2914`)
|
||||||
|
- [x] Sources rail grows per-source checkboxes + window selector;
|
||||||
|
scope flows through `useSendCoachMessage` to the chat request
|
||||||
|
body and into `buildCoachSnapshot` (`08fd411`)
|
||||||
|
- [x] Detailed report: `.planning/phase-v1421-hotfix-report.md`
|
||||||
|
|
||||||
|
## Status block — F (v1.4.20.1 hotfix)
|
||||||
|
|
||||||
|
- 2026-05-10 (post-deploy) — Hotfix complete. Six commits on
|
||||||
|
`develop` (`7921ffc` → `08fd411`). Test counts: 2026 → 2036 unit
|
||||||
|
(+10 net across snapshot + message-thread + sources-rail tests).
|
||||||
|
Integration tests unchanged at 81 — coach-chat.test.ts still 5/5.
|
||||||
|
typecheck + lint clean (13 baseline warnings unchanged). Token-
|
||||||
|
budget impact of the day-level snapshot: ~190 → ~3000 tokens per
|
||||||
|
full-scope turn; the new scope picker is the relief valve, single-
|
||||||
|
source narrowed turns land around 600-700 tokens. v1.4.21 should
|
||||||
|
watch usage and consider trimming `DAILY_TIMELINE_DAYS` from 14 to
|
||||||
|
10 if heavy users start hitting the 25k/day cap.
|
||||||
|
|
||||||
## Status block — B5 (v1.4.20)
|
## Status block — B5 (v1.4.20)
|
||||||
|
|
||||||
- 2026-05-10T17:45+02:00 — B5 complete. Personal Health Score
|
- 2026-05-10T17:45+02:00 — B5 complete. Personal Health Score
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Phase v1.4.20.1 — hotfix report
|
||||||
|
|
||||||
|
Date: 2026-05-10 (post-v1.4.20 deploy).
|
||||||
|
Triggered by Marc's testing of the new Insights/Coach surface in
|
||||||
|
production. Five issues captured in
|
||||||
|
`feedback_v1420_post_deploy_bugs.md`; all six commits below land on
|
||||||
|
`develop` with no tag — the maintainer decides whether to ship as a
|
||||||
|
v1.4.20.1 hotfix or bundle into v1.4.21.
|
||||||
|
|
||||||
|
## Issue 1 — Daily Briefing regenerate produced nothing
|
||||||
|
|
||||||
|
**Diagnosis.** `/api/insights/generate` was still calling the legacy
|
||||||
|
`getInsightsSystemPrompt` from `src/lib/insights/prompt.ts`, which
|
||||||
|
returns the v1.4.5 `{changed, stable, drivers, …}` shape and never
|
||||||
|
asks the model for a `dailyBriefing`. The B1 phase shipped GROUND
|
||||||
|
RULE 8 (emit dailyBriefing when the snapshot has signal) on
|
||||||
|
`getStrictInsightsSystemPrompt` in
|
||||||
|
`src/lib/ai/prompts/insight-generator.ts`, but the route never
|
||||||
|
adopted it. Re-runs from the hero strip therefore stored a payload
|
||||||
|
with no briefing block and the `<DailyBriefing>` card painted its
|
||||||
|
empty state forever.
|
||||||
|
|
||||||
|
**Fix.** Switch the route to `getStrictInsightsSystemPrompt(locale)`.
|
||||||
|
The route's existing `validated.success ? validated.data : parsed`
|
||||||
|
fall-through and the strict schema's `passthrough()` mean the change
|
||||||
|
is back-compat for cached legacy blobs. Commit `7921ffc`.
|
||||||
|
|
||||||
|
## Issue 2 — Coach felt generic, couldn't answer day questions
|
||||||
|
|
||||||
|
**Diagnosis.** `buildCoachSnapshot` shipped only aggregated
|
||||||
|
statistics (mean, slope, SD, range, count) per metric. A turn that
|
||||||
|
asked "why was BP higher on Monday?" had no day-level rows and the
|
||||||
|
model fell back to apologetic aggregate-only phrasing — Marc's
|
||||||
|
actual reply was "from the snapshot I cannot isolate the specific
|
||||||
|
Monday because no day-specific individual values with weekday are
|
||||||
|
included." Two layers of fix needed: snapshot must carry the data;
|
||||||
|
prompt must tell the model to use it.
|
||||||
|
|
||||||
|
**Primary fix (commit `ed61b17`).** Extend `buildCoachSnapshot` to
|
||||||
|
fold in a `timeline.recent` block (one row per UTC day for the last
|
||||||
|
14 days, each tagged with weekday) and a `timeline.weekly` block
|
||||||
|
(ISO-week buckets covering days 15..N). Pair systolic + diastolic
|
||||||
|
into one BP row per day; drop half-measured days so the model never
|
||||||
|
fabricates a complement. Pull per-day medication adherence rows from
|
||||||
|
`MedicationIntakeEvent`. Add an optional `scope` field to
|
||||||
|
`coachChatRequestSchema` (sources + window) so a future picker can
|
||||||
|
narrow without breaking existing clients.
|
||||||
|
|
||||||
|
**Secondary fix (commit `2143377`).** Add a DAY-LEVEL READINGS
|
||||||
|
section to both EN and DE Coach system prompts: explain the timeline
|
||||||
|
shape; instruct the model to answer day-specific or weekday-specific
|
||||||
|
questions out of `timeline.recent` and to cite the actual reading
|
||||||
|
with date + weekday; tell the model to acknowledge missing days
|
||||||
|
plainly and offer the closest available day; let older weekday
|
||||||
|
questions fall back to `timeline.weekly`.
|
||||||
|
|
||||||
|
## Issue 3 — Duplicate assistant reply in display
|
||||||
|
|
||||||
|
**Diagnosis.** After SSE `done` the streaming hook keeps
|
||||||
|
`streaming.content` populated to support the in-flight render path,
|
||||||
|
then fires a TanStack invalidate that pulls the persisted assistant
|
||||||
|
message into `conversation.messages`. Until the next `send` reset
|
||||||
|
the message thread therefore rendered the assistant reply twice.
|
||||||
|
Marc noticed the duplicate cleared on drawer close + reopen — the
|
||||||
|
persisted state was correct, the bug was purely client-side render
|
||||||
|
overlap.
|
||||||
|
|
||||||
|
**Fix.** Suppress the streaming bubble at render time as soon as the
|
||||||
|
persisted twin lands. The match keys on `streaming.messageId`,
|
||||||
|
which the SSE `done` frame fills in once the row is on disk; mid-
|
||||||
|
stream the field is null so the in-flight bubble stays visible.
|
||||||
|
Commit `f07e35b`.
|
||||||
|
|
||||||
|
## Issue 4 — Settings cog vs Sheet close-X collision
|
||||||
|
|
||||||
|
**Diagnosis.** Radix Sheet paints its default close-X at absolute
|
||||||
|
top-4 right-4. The v1.4.20 drawer header parked the settings cog
|
||||||
|
inside the right-edge button cluster, so the close-X overlay
|
||||||
|
visually swallowed the cog — the icon still rendered but read as
|
||||||
|
unclickable.
|
||||||
|
|
||||||
|
**Fix.** Move the cog to the left side, immediately after the
|
||||||
|
gradient avatar, so the right edge is reserved for Radix's close-X
|
||||||
|
alone. Add `pr-12 / sm:pr-14` padding to the header so the New chat
|
||||||
|
button can never slide under the close-X on narrower viewports
|
||||||
|
either. Commit `ddb2914`.
|
||||||
|
|
||||||
|
## Issue 5 — Sources rail had no toggle UI
|
||||||
|
|
||||||
|
**Diagnosis.** The Sources rail listed BP / Weight / Pulse / Mood /
|
||||||
|
Compliance as a static legend. Marc wanted per-source checkboxes
|
||||||
|
(include/exclude) plus a window selector (last 7d / 30d / 90d /
|
||||||
|
all-time) feeding the next Coach turn.
|
||||||
|
|
||||||
|
**Fix (commit `08fd411`).** Wire the controls all the way down:
|
||||||
|
`<SourcesRail>` grows `scope` + `onScopeChange` props with real
|
||||||
|
checkboxes (36px touch target; 60% opacity when excluded) and a
|
||||||
|
`<Select>` for the window. `<CoachDrawer>` owns the scope state
|
||||||
|
(resets each mount per the v1.4.20.1 plan) and forwards picked
|
||||||
|
scope through `useSendCoachMessage` to the chat request body. The
|
||||||
|
payload only ships when the user has narrowed away from the all-
|
||||||
|
source last30days default — server can tell "no opinion" from
|
||||||
|
"intentionally narrow". `buildCoachSnapshot` already accepted the
|
||||||
|
optional `CoachScope` from the previous commit; it now flows end-
|
||||||
|
to-end. New i18n key `insights.coach.windowLabel` in EN+DE.
|
||||||
|
|
||||||
|
## Test count delta
|
||||||
|
|
||||||
|
- 2026 → 2036 unit tests (+10 net).
|
||||||
|
- 6 new tests in `src/lib/ai/coach/__tests__/snapshot.test.ts`
|
||||||
|
covering empty input, day-level BP rows, scope.sources filter,
|
||||||
|
scope.window narrowing, default scope, mood-only path.
|
||||||
|
- 2 new tests in
|
||||||
|
`src/components/insights/coach-panel/__tests__/message-thread.test.tsx`
|
||||||
|
pinning the streaming-bubble dedup behaviour (suppress when
|
||||||
|
persisted twin lands; keep when still streaming).
|
||||||
|
- 2 new tests in
|
||||||
|
`src/components/insights/coach-panel/__tests__/sources-rail.test.tsx`
|
||||||
|
pinning the checkbox count + scope-driven active state.
|
||||||
|
|
||||||
|
Integration suite green; `tests/integration/coach-chat.test.ts` still
|
||||||
|
passes 5/5 with the route changes.
|
||||||
|
|
||||||
|
## Token-budget impact of the day-level snapshot
|
||||||
|
|
||||||
|
Aggregates-only snapshot before: ~760 bytes JSON ≈ 190 tokens.
|
||||||
|
Day-level snapshot after, full 5-source 30-day window: ~12 KB JSON
|
||||||
|
≈ 3000 tokens. Delta is roughly +2800 tokens per turn. The
|
||||||
|
`MAX_TOKENS_PER_USER_PER_DAY = 25_000` cap therefore drops a heavy
|
||||||
|
user from ~13 turns/day to ~8 turns/day. The new scope picker is
|
||||||
|
the relief valve: a single-source, last-7-days narrowed turn lands
|
||||||
|
around ~600-700 tokens. v1.4.21 should consider reducing the
|
||||||
|
`DAILY_TIMELINE_DAYS` constant from 14 → 10 if heavy users start
|
||||||
|
hitting the cap; usage data will tell.
|
||||||
|
|
||||||
|
## Dispatch recommendation
|
||||||
|
|
||||||
|
Ship as **v1.4.20.1 hotfix** rather than bundling into v1.4.21.
|
||||||
|
Reasoning:
|
||||||
|
|
||||||
|
- Issue 1 is a regression of the v1.4.20 headline feature — Daily
|
||||||
|
Briefing was the centerpiece of the Insights redesign and it
|
||||||
|
hasn't been working since the tag.
|
||||||
|
- Issue 3 is a render bug that makes every Coach interaction look
|
||||||
|
broken to a first-time user.
|
||||||
|
- Issues 2, 4, 5 are quality-bar items that, taken together, fix the
|
||||||
|
"Coach feels impersonal" perception that undercuts the AI-Insights-
|
||||||
|
as-differentiator product principle.
|
||||||
|
- The diff is contained: 8 files touched across 6 commits, no
|
||||||
|
schema migration, no new dependencies, no breaking API change
|
||||||
|
(`scope` is additive optional). All 2036 unit tests + the
|
||||||
|
coach-chat integration suite green.
|
||||||
|
|
||||||
|
The runtime risk of holding the fixes for v1.4.21 is that production
|
||||||
|
keeps shipping a broken Daily Briefing. A patch tag here is exactly
|
||||||
|
what the conservative-semver memory expects.
|
||||||
|
|
||||||
|
## What was not fixed
|
||||||
|
|
||||||
|
Nothing was deferred — all five issues have a working fix on
|
||||||
|
`develop`. One follow-up worth tracking in v1.4.21:
|
||||||
|
|
||||||
|
- The +2800 tokens/turn snapshot delta is at the upper end of what
|
||||||
|
the 25k/day budget tolerates for power users. v1.4.21 should
|
||||||
|
watch real usage and either trim `DAILY_TIMELINE_DAYS` from 14 to
|
||||||
|
10, or add a "summarise-when-narrow" path that drops the
|
||||||
|
`timeline.weekly` block when the window is `last7days`.
|
||||||
@@ -143,12 +143,7 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
|
|||||||
{ status: 422 },
|
{ status: 422 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const {
|
const { conversationId, message, locale: bodyLocale, scope } = parsed.data;
|
||||||
conversationId,
|
|
||||||
message,
|
|
||||||
locale: bodyLocale,
|
|
||||||
scope,
|
|
||||||
} = parsed.data;
|
|
||||||
|
|
||||||
await enforceBudget(userId);
|
await enforceBudget(userId);
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,8 @@ describe("<SourcesRail>", () => {
|
|||||||
|
|
||||||
it("renders a checkbox per row + a window selector trigger", () => {
|
it("renders a checkbox per row + a window selector trigger", () => {
|
||||||
const html = render(<SourcesRail />);
|
const html = render(<SourcesRail />);
|
||||||
const checkboxes = (
|
const checkboxes = (html.match(/data-slot="coach-sources-checkbox"/g) ?? [])
|
||||||
html.match(/data-slot="coach-sources-checkbox"/g) ?? []
|
.length;
|
||||||
).length;
|
|
||||||
expect(checkboxes).toBe(5);
|
expect(checkboxes).toBe(5);
|
||||||
expect(html).toContain('data-slot="coach-sources-window-trigger"');
|
expect(html).toContain('data-slot="coach-sources-window-trigger"');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -258,10 +258,7 @@ export function CoachDrawer({
|
|||||||
}
|
}
|
||||||
sourcesRail={
|
sourcesRail={
|
||||||
sourcesRail ?? (
|
sourcesRail ?? (
|
||||||
<SourcesRail
|
<SourcesRail scope={scope} onScopeChange={setScope} />
|
||||||
scope={scope}
|
|
||||||
onScopeChange={setScope}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
thread={
|
thread={
|
||||||
|
|||||||
@@ -67,9 +67,7 @@ export function MessageThread({
|
|||||||
messages.some((m) => m.id === streaming.messageId);
|
messages.some((m) => m.id === streaming.messageId);
|
||||||
const streamingActive =
|
const streamingActive =
|
||||||
!streamingPersisted &&
|
!streamingPersisted &&
|
||||||
(!!streaming?.inProgress ||
|
(!!streaming?.inProgress || !!streaming?.content || !!streaming?.errorCode);
|
||||||
!!streaming?.content ||
|
|
||||||
!!streaming?.errorCode);
|
|
||||||
|
|
||||||
// Track scroll position so we don't yank the viewport when the user
|
// Track scroll position so we don't yank the viewport when the user
|
||||||
// is browsing earlier turns.
|
// is browsing earlier turns.
|
||||||
|
|||||||
@@ -82,7 +82,10 @@ export interface SourcesRailProps {
|
|||||||
* the controls still render but stay read-only — keeps the legend
|
* the controls still render but stay read-only — keeps the legend
|
||||||
* surface stable for callers that don't yet wire scope through.
|
* 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 {
|
interface SourceRow {
|
||||||
|
|||||||
@@ -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.
|
* 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;
|
type: string;
|
||||||
value: number;
|
value: number;
|
||||||
measuredAt: Date;
|
measuredAt: Date;
|
||||||
|
|||||||
@@ -53,15 +53,7 @@ const DEFAULT_SOURCES: ReadonlyArray<CoachScopeSource> = [
|
|||||||
"compliance",
|
"compliance",
|
||||||
];
|
];
|
||||||
|
|
||||||
const WEEKDAY_KEYS = [
|
const WEEKDAY_KEYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
|
||||||
"Sun",
|
|
||||||
"Mon",
|
|
||||||
"Tue",
|
|
||||||
"Wed",
|
|
||||||
"Thu",
|
|
||||||
"Fri",
|
|
||||||
"Sat",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
function windowToDays(window: CoachScopeWindow): number {
|
function windowToDays(window: CoachScopeWindow): number {
|
||||||
switch (window) {
|
switch (window) {
|
||||||
@@ -163,7 +155,9 @@ function bucketWeekly(
|
|||||||
return Array.from(grouped.entries())
|
return Array.from(grouped.entries())
|
||||||
.map(([weekISO, values]) => ({
|
.map(([weekISO, values]) => ({
|
||||||
weekISO,
|
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,
|
count: values.length,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.weekISO.localeCompare(b.weekISO));
|
.sort((a, b) => a.weekISO.localeCompare(b.weekISO));
|
||||||
@@ -177,8 +171,7 @@ function buildDailyValueRows(
|
|||||||
const grouped = dailyMeans(recent);
|
const grouped = dailyMeans(recent);
|
||||||
return Array.from(grouped.entries())
|
return Array.from(grouped.entries())
|
||||||
.map(([date, info]) => {
|
.map(([date, info]) => {
|
||||||
const mean =
|
const mean = info.values.reduce((s, v) => s + v, 0) / info.values.length;
|
||||||
info.values.reduce((s, v) => s + v, 0) / info.values.length;
|
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
weekday: utcWeekday(info.date),
|
weekday: utcWeekday(info.date),
|
||||||
@@ -206,10 +199,8 @@ function buildDailyBpRows(
|
|||||||
for (const [day, info] of sysByDay) {
|
for (const [day, info] of sysByDay) {
|
||||||
const dia = diaByDay.get(day);
|
const dia = diaByDay.get(day);
|
||||||
if (!dia) continue;
|
if (!dia) continue;
|
||||||
const sysMean =
|
const sysMean = info.values.reduce((s, v) => s + v, 0) / info.values.length;
|
||||||
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 diaMean =
|
|
||||||
dia.values.reduce((s, v) => s + v, 0) / dia.values.length;
|
|
||||||
out.push({
|
out.push({
|
||||||
date: day,
|
date: day,
|
||||||
weekday: utcWeekday(info.date),
|
weekday: utcWeekday(info.date),
|
||||||
@@ -310,10 +301,12 @@ export async function buildCoachSnapshot(
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const byType = (t: string) =>
|
const byType = (t: string) =>
|
||||||
measurementRows.filter((r) => r.type === t).map((r) => ({
|
measurementRows
|
||||||
measuredAt: r.measuredAt,
|
.filter((r) => r.type === t)
|
||||||
value: r.value,
|
.map((r) => ({
|
||||||
}));
|
measuredAt: r.measuredAt,
|
||||||
|
value: r.value,
|
||||||
|
}));
|
||||||
|
|
||||||
if (wantsBp && features.bloodPressure) {
|
if (wantsBp && features.bloodPressure) {
|
||||||
const sysRows = byType("BLOOD_PRESSURE_SYS");
|
const sysRows = byType("BLOOD_PRESSURE_SYS");
|
||||||
@@ -405,9 +398,7 @@ export async function buildCoachSnapshot(
|
|||||||
// Per-day adherence rate within the recent window. Older days
|
// Per-day adherence rate within the recent window. Older days
|
||||||
// collapse into a single weekly bucket.
|
// collapse into a single weekly bucket.
|
||||||
const recent = intakeRows.filter((r) => r.scheduledFor >= recentCutoff);
|
const recent = intakeRows.filter((r) => r.scheduledFor >= recentCutoff);
|
||||||
const olderRows = intakeRows.filter(
|
const olderRows = intakeRows.filter((r) => r.scheduledFor < recentCutoff);
|
||||||
(r) => r.scheduledFor < recentCutoff,
|
|
||||||
);
|
|
||||||
const recentByDay = new Map<
|
const recentByDay = new Map<
|
||||||
string,
|
string,
|
||||||
{ date: Date; total: number; taken: number }
|
{ date: Date; total: number; taken: number }
|
||||||
|
|||||||
Reference in new issue
Block a user