diff --git a/CHANGELOG.md b/CHANGELOG.md index 152d56968..77828ea18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## [1.40.2] — 2026-10-05 + +A calmer Coach with conversations in a side panel and settings in place; the +top of the dashboard says what matters today; documents filter by procedure. + +### Added + +- **Coach panel.** Conversations in a collapsible right panel (a sheet on a + phone), grouped by day, with rename, delete with undo and a new-chat + button; a gear opens quick settings (provider, model and reasoning where + they are the user's, and the lookback). `/coach?settings=data` opens it. +- **Coach lookback as a real limit.** 7, 30 or 90 days, 12 months + (`lastYear`, new) or all; it limits the snapshot and every Coach tool, a + read beyond it answers `outside_reach`. MCP keeps its own windows. +- **Today overview.** The digest gains `lead`, `today[]`, `restMode` and + `score.steadyWeeks`/`steadyAtLeast`: a lead line with content (deterministic + without AI), up to five facts (medications, an appointment today or + tomorrow, last night, vitals, cycle, Rest Mode), the ring with how long the + score has been steady. The morning push line carries no vital values. +- **Procedure filter in the document archive (#1154).** `linkedProcedures` on + the usage endpoint; the list already accepted `encounterId`. + +### Changed + +- **Coach answers.** One action row (copy, read aloud, retry, time; tokens and + model); web thumbs and the evidence block removed; remember is an icon; + follow-up chips in the answer column; a scroll-to-bottom button; the + composer's "+" menu removed. +- **Settings → Coach** summarises what the Coach sees and links into the + overlay. +- Coach preferences parse field by field, so one invalid value no longer + drops the others. + +### Fixed + +- The medications fact counts the whole day on a 25-hour DST day. +- Providers without credentials are no longer offered in the Coach overlay; + an operator-managed provider shows as such. +- Document filter controls match the search field's height on a phone. + ## [1.40.1] — 2026-10-04 Restore opens older encrypted values again; faster pages on a long-used diff --git a/bundle-budget.json b/bundle-budget.json index cc062e9d9..125356595 100644 --- a/bundle-budget.json +++ b/bundle-budget.json @@ -1,5 +1,5 @@ { - "$comment": "Client-bundle budgets, enforced by scripts/check-bundle-budget.mjs (--check) after `next build` in CI. Values are KB gzip and include every emitted lazy route and locale chunk, while route budgets include the shared rootMainFiles baseline. Re-measured at v1.37.2: / 455, /insights 443, /measurements 441, /insights/mood 453, total 3328, baseline 130, lazy catalogs 849. Every per-route ceiling is unchanged. The /insights overview crept a kilobyte over its 445 ceiling when the ECG teaser began importing its overview cap from the classification module, dragging that module's label map and helpers into the eager page; the cap now lives in its own value-only module, and the Health Score's single provenance entry is split out of the full metric-provenance map, so the overview imports the bare number and citation it renders rather than every metric's label map behind them. Both are back under 445 with room to spare. The 42 KB aggregate rise from 3286 (v1.35.0) is all locale-catalog strings: v1.36 and v1.37 added the encounter, mood-context, mood-prognosis, practitioner, workout-batch and account-sharing surfaces, and their copy lands across the six locale catalogs the whole-build sum counts independently (804 to 849 KB). The non-catalog client chunks are flat against v1.35.0, so nothing a person downloads on a given route grew beyond its own ceiling. The aggregate cap therefore moves from 3310 to 3352 (24 KB headroom, matching the previous margin); per-route ceilings, the static-catalog guard and the one-Recharts-chunk guard remain unchanged. v1.37.4's honest-error-state charge then added recoverable read-failure handling to thirteen surfaces (the shared QueryErrorCard and QueryErrorRow primitives plus an isError branch per surface) and rewrote the MCP scope note and the managed-record error copy across the six locales; the measured total rose to 3404 while every per-route ceiling stayed met (/page 457, /insights 445, /measurements 443, /insights/mood 455). The aggregate cap moves from 3352 to 3428, keeping the same margin; nothing a person downloads on a given route grew beyond its ceiling. v1.37.15's async-truth charge then added the refresh settling state with its two-locale-key copy, the index-outcome plain-words line (seven keys across the six catalogs) and the reindex-count wiring; the measured total rose to 3429 while every per-route ceiling stayed met (/page 456, /insights 445, /measurements 442, /insights/mood 455). The aggregate cap moves from 3428 to 3452, keeping the same margin; nothing a person downloads on a given route grew beyond its ceiling. v1.37.27 found /insights sitting a few hundred bytes under 445 on main and a few hundred over it on the release branch — the page had been at its ceiling for three releases and the release only had to breathe on it. The weight it was carrying was not its own: the authenticated shell imported one destination predicate from the `@/lib/record-settings` barrel, and the barrel also re-exports the managed settings-patch schemas, so Zod, the module registry and three validation modules rode into the eager bundle of every authenticated route for a function that reads a URL slug. The shell imports the `classification` leaf now. Measured across all 585 client-rendered routes: none grew by a byte, 100 fell, by 4.7 KB at the median and 8.1 KB at the most; /insights 444 to 437, /measurements 442 to 435, / 456 to 450, /insights/mood 455 to 450. The whole-build sum moved the other way, 3451 to 3476, and that direction is the metric rather than the change: the aggregate counts each emitted chunk once, so a graph that leaves the chunk group every route shares gets counted again wherever it lands — here inside the deferred settings chunks that actually use it. Bytes on disk rose 25 KB; bytes a reader downloads only ever fell. The aggregate cap moves from 3452 to 3500, the same 24 KB headroom as the four re-measurements before it. Per-route ceilings keep their old values rather than tightening to the new floor: the CI runner measures each route about 1.5 KB heavier than a local arm64 build does, and a ceiling set from a local number would fail on a build that had not regressed. The static-catalog guard and the one-Recharts-chunk guard remain unchanged. Raised to 3700 at v1.37.32 for the seventh interface language (Korean): the lazy message catalogs went 6->7 chunks (1026 KB gz measured), pushing the aggregate to 3631 against the old 3500. The raise is entirely the new catalog; no route budget moved, and the four eager route ceilings are unchanged and still passing. Most of the aggregate is the dynamic locale-switch and fallback path that a first paint never loads (see issue #253), so this ceiling guards bytes almost no visitor requests; 3700 leaves ~69 KB for ordinary drift until that double-count is cleaned up. v1.38.14 re-measures the four eager routes and changes what the number means. The ceilings had been re-stated but never re-based since v1.37.4, so /insights and /measurements sat within a kilobyte of theirs and / and /insights/mood within one of theirs; e2e run 34043772954 failed on `/insights/page: 445 KB gz exceeds the 445 KB budget` with the other two at 459 of 460, and none of it was a regression anyone had made. A ceiling a route is already touching does not measure the route, it measures whichever dependency bump landed that week. The file states the measured value per route now, plus one drift allowance over it: measured on a local arm64 build at d48bbff01 as / 447, /insights 434, /measurements 433, /insights/mood 447, with a 20 KB allowance, so the effective ceilings are 467 / 454 / 453 / 467 — a raise for all four. The allowance covers the runner delta (the CI runner measures each route a little heavier than a local arm64 build) and ordinary week-to-week drift, while the step change the gate exists for — a statically imported message catalog, a duplicated chart library — is several times larger and still fails. Every run now prints the delta against the baseline, so a route that has crept 15 KB is legible in the log long before it is red; the flat ceiling could only ever say pass or fail. Re-base by re-measuring on a build with nothing else in it and writing the new numbers here with the reason, exactly as a raised ceiling was written before. Raised to 3724 at v1.39.4 on a measured delta, not a feature. The release branch alone measured 3699 KB gz (3698.9) against the 3700 cap, so the ordinary drift room the v1.37.32 raise left had been used up by the releases since; the review fixes on top of it added 1.3 KB gz across the whole build (3700.2), almost all of it new copy in the seven lazy locale catalogs (the total and reused-table labels, the step and chip wording) plus the result-panel states. The four eager routes measured the same on both builds (/ 458, /insights/mood 456, /insights 445, /measurements 442), all inside their ceilings. The aggregate cap moves from 3700 to 3724, the same 24 KB headroom as the re-measurements before it. Raised to 3749 at v1.39.9 on a measured delta. Main measured 3718 KB gz after v1.39.7 and v1.39.8 (unit-aware copy, the phone layouts, the time-zone work and the account-settings restore labels, almost all of it new strings in the seven lazy locale catalogs), leaving 6 KB of the v1.39.4 headroom; the manual workout entry (#1085: the form, its sheet, the delete action and their copy in seven locales) adds about 7 KB and measured 3725. The four eager routes stay inside their drift allowances (/ +12.5, /insights +12.1, /insights/mood +10.2 KB of 20 on main). The aggregate cap moves from 3724 to 3749, the same 24 KB headroom as the re-measurements before it. Raised to 3790 at v1.40.0 on a measured delta. With the background document reads on main (#1103: the run polling hook, the still-reading states and their copy in seven locales), the key backup step and the plain-http sign-in notice (#1104) and the v1.40 fixes (#1106: the duration reader, the marker unit lock, the chart window) each branch measured 3752 KB gz on its own against the 3749 cap; together they add a few KB more. The four eager routes stay inside their drift allowances (/ +18.4, /insights +16.5, /measurements +14.9, /insights/mood +14.7 KB of 20). The aggregate cap moves from 3749 to 3790, about 24 KB above the combined build, the same headroom as the re-measurements before it. Re-based at v1.40.0 when the quick-entry forms began loading on demand. Custom symptoms added a fifth quick-entry form, and with all five forms bundled eagerly every one of the four eager routes measured over its ceiling (/ 472, /insights 457, /insights/mood 468, /measurements 454 KB gz, each 21 to 25 KB over the baseline). Loading the forms on demand brings them to / 429, /insights 368, /insights/mood 379, /measurements 411: 43 to 90 KB less on first load. The four route baselines move down to those measured values, so the gain is protected by the same 20 KB allowance rather than spent. The aggregate moves the other way, 3774 to 4077, for the reason described at v1.37.27: the aggregate counts each emitted chunk once per route group, and on-demand form chunks land in every route group that can open a quick-entry sheet. Bundling the five forms into one shared on-demand module measured the same (4078), so the split is not the cause. The aggregate cap moves from 3790 to 4100. Bytes on disk rose; bytes a reader downloads fell on every eager route.", + "$comment": "Client-bundle budgets, enforced by scripts/check-bundle-budget.mjs (--check) after `next build` in CI. Values are KB gzip and include every emitted lazy route and locale chunk, while route budgets include the shared rootMainFiles baseline. Re-measured at v1.37.2: / 455, /insights 443, /measurements 441, /insights/mood 453, total 3328, baseline 130, lazy catalogs 849. Every per-route ceiling is unchanged. The /insights overview crept a kilobyte over its 445 ceiling when the ECG teaser began importing its overview cap from the classification module, dragging that module's label map and helpers into the eager page; the cap now lives in its own value-only module, and the Health Score's single provenance entry is split out of the full metric-provenance map, so the overview imports the bare number and citation it renders rather than every metric's label map behind them. Both are back under 445 with room to spare. The 42 KB aggregate rise from 3286 (v1.35.0) is all locale-catalog strings: v1.36 and v1.37 added the encounter, mood-context, mood-prognosis, practitioner, workout-batch and account-sharing surfaces, and their copy lands across the six locale catalogs the whole-build sum counts independently (804 to 849 KB). The non-catalog client chunks are flat against v1.35.0, so nothing a person downloads on a given route grew beyond its own ceiling. The aggregate cap therefore moves from 3310 to 3352 (24 KB headroom, matching the previous margin); per-route ceilings, the static-catalog guard and the one-Recharts-chunk guard remain unchanged. v1.37.4's honest-error-state charge then added recoverable read-failure handling to thirteen surfaces (the shared QueryErrorCard and QueryErrorRow primitives plus an isError branch per surface) and rewrote the MCP scope note and the managed-record error copy across the six locales; the measured total rose to 3404 while every per-route ceiling stayed met (/page 457, /insights 445, /measurements 443, /insights/mood 455). The aggregate cap moves from 3352 to 3428, keeping the same margin; nothing a person downloads on a given route grew beyond its ceiling. v1.37.15's async-truth charge then added the refresh settling state with its two-locale-key copy, the index-outcome plain-words line (seven keys across the six catalogs) and the reindex-count wiring; the measured total rose to 3429 while every per-route ceiling stayed met (/page 456, /insights 445, /measurements 442, /insights/mood 455). The aggregate cap moves from 3428 to 3452, keeping the same margin; nothing a person downloads on a given route grew beyond its ceiling. v1.37.27 found /insights sitting a few hundred bytes under 445 on main and a few hundred over it on the release branch — the page had been at its ceiling for three releases and the release only had to breathe on it. The weight it was carrying was not its own: the authenticated shell imported one destination predicate from the `@/lib/record-settings` barrel, and the barrel also re-exports the managed settings-patch schemas, so Zod, the module registry and three validation modules rode into the eager bundle of every authenticated route for a function that reads a URL slug. The shell imports the `classification` leaf now. Measured across all 585 client-rendered routes: none grew by a byte, 100 fell, by 4.7 KB at the median and 8.1 KB at the most; /insights 444 to 437, /measurements 442 to 435, / 456 to 450, /insights/mood 455 to 450. The whole-build sum moved the other way, 3451 to 3476, and that direction is the metric rather than the change: the aggregate counts each emitted chunk once, so a graph that leaves the chunk group every route shares gets counted again wherever it lands — here inside the deferred settings chunks that actually use it. Bytes on disk rose 25 KB; bytes a reader downloads only ever fell. The aggregate cap moves from 3452 to 3500, the same 24 KB headroom as the four re-measurements before it. Per-route ceilings keep their old values rather than tightening to the new floor: the CI runner measures each route about 1.5 KB heavier than a local arm64 build does, and a ceiling set from a local number would fail on a build that had not regressed. The static-catalog guard and the one-Recharts-chunk guard remain unchanged. Raised to 3700 at v1.37.32 for the seventh interface language (Korean): the lazy message catalogs went 6->7 chunks (1026 KB gz measured), pushing the aggregate to 3631 against the old 3500. The raise is entirely the new catalog; no route budget moved, and the four eager route ceilings are unchanged and still passing. Most of the aggregate is the dynamic locale-switch and fallback path that a first paint never loads (see issue #253), so this ceiling guards bytes almost no visitor requests; 3700 leaves ~69 KB for ordinary drift until that double-count is cleaned up. v1.38.14 re-measures the four eager routes and changes what the number means. The ceilings had been re-stated but never re-based since v1.37.4, so /insights and /measurements sat within a kilobyte of theirs and / and /insights/mood within one of theirs; e2e run 34043772954 failed on `/insights/page: 445 KB gz exceeds the 445 KB budget` with the other two at 459 of 460, and none of it was a regression anyone had made. A ceiling a route is already touching does not measure the route, it measures whichever dependency bump landed that week. The file states the measured value per route now, plus one drift allowance over it: measured on a local arm64 build at d48bbff01 as / 447, /insights 434, /measurements 433, /insights/mood 447, with a 20 KB allowance, so the effective ceilings are 467 / 454 / 453 / 467 — a raise for all four. The allowance covers the runner delta (the CI runner measures each route a little heavier than a local arm64 build) and ordinary week-to-week drift, while the step change the gate exists for — a statically imported message catalog, a duplicated chart library — is several times larger and still fails. Every run now prints the delta against the baseline, so a route that has crept 15 KB is legible in the log long before it is red; the flat ceiling could only ever say pass or fail. Re-base by re-measuring on a build with nothing else in it and writing the new numbers here with the reason, exactly as a raised ceiling was written before. Raised to 3724 at v1.39.4 on a measured delta, not a feature. The release branch alone measured 3699 KB gz (3698.9) against the 3700 cap, so the ordinary drift room the v1.37.32 raise left had been used up by the releases since; the review fixes on top of it added 1.3 KB gz across the whole build (3700.2), almost all of it new copy in the seven lazy locale catalogs (the total and reused-table labels, the step and chip wording) plus the result-panel states. The four eager routes measured the same on both builds (/ 458, /insights/mood 456, /insights 445, /measurements 442), all inside their ceilings. The aggregate cap moves from 3700 to 3724, the same 24 KB headroom as the re-measurements before it. Raised to 3749 at v1.39.9 on a measured delta. Main measured 3718 KB gz after v1.39.7 and v1.39.8 (unit-aware copy, the phone layouts, the time-zone work and the account-settings restore labels, almost all of it new strings in the seven lazy locale catalogs), leaving 6 KB of the v1.39.4 headroom; the manual workout entry (#1085: the form, its sheet, the delete action and their copy in seven locales) adds about 7 KB and measured 3725. The four eager routes stay inside their drift allowances (/ +12.5, /insights +12.1, /insights/mood +10.2 KB of 20 on main). The aggregate cap moves from 3724 to 3749, the same 24 KB headroom as the re-measurements before it. Raised to 3790 at v1.40.0 on a measured delta. With the background document reads on main (#1103: the run polling hook, the still-reading states and their copy in seven locales), the key backup step and the plain-http sign-in notice (#1104) and the v1.40 fixes (#1106: the duration reader, the marker unit lock, the chart window) each branch measured 3752 KB gz on its own against the 3749 cap; together they add a few KB more. The four eager routes stay inside their drift allowances (/ +18.4, /insights +16.5, /measurements +14.9, /insights/mood +14.7 KB of 20). The aggregate cap moves from 3749 to 3790, about 24 KB above the combined build, the same headroom as the re-measurements before it. Re-based at v1.40.0 when the quick-entry forms began loading on demand. Custom symptoms added a fifth quick-entry form, and with all five forms bundled eagerly every one of the four eager routes measured over its ceiling (/ 472, /insights 457, /insights/mood 468, /measurements 454 KB gz, each 21 to 25 KB over the baseline). Loading the forms on demand brings them to / 429, /insights 368, /insights/mood 379, /measurements 411: 43 to 90 KB less on first load. The four route baselines move down to those measured values, so the gain is protected by the same 20 KB allowance rather than spent. The aggregate moves the other way, 3774 to 4077, for the reason described at v1.37.27: the aggregate counts each emitted chunk once per route group, and on-demand form chunks land in every route group that can open a quick-entry sheet. Bundling the five forms into one shared on-demand module measured the same (4078), so the split is not the cause. The aggregate cap moves from 3790 to 4100. Bytes on disk rose; bytes a reader downloads fell on every eager route. Raised to 4155 at v1.40.2 on a measured delta. The Coach redesign (the right conversation panel, its row menu and the quick-settings overlay, all loaded on demand), the answer action row, the dashboard's Today overview, the document procedure filter and their copy in seven locales took the whole-build sum from 4080 to 4129 KB gz; most of it is new strings in the seven lazy locale catalogs and the on-demand Coach chunks, which a first paint does not load. The four eager routes stay well inside their drift allowances (/ +1.9, /measurements +1.9, /insights/mood +1.4, /insights +1.0 KB of 20). The aggregate cap moves from 4100 to 4155, about 26 KB above the build, the same headroom as before; per-route baselines are unchanged.", "routeBaselineKbGz": { "/page": 429, "/insights/page": 368, @@ -7,6 +7,6 @@ "/insights/mood/page": 379 }, "routeDriftAllowanceKbGz": 20, - "totalClientKbGz": 4100, + "totalClientKbGz": 4155, "maxRechartsChunks": 1 } diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 54c7d32f5..62082e6fa 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.40.1 + version: 1.40.2 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. @@ -16410,7 +16410,8 @@ paths: required: false schema: type: string - description: Only documents linked to this visit. + description: Only documents linked to this visit. The procedure filter sends a `linkedProcedures` id from the usage + read; an id that is not one of the caller's visits matches nothing. - name: year in: query required: false @@ -16744,8 +16745,11 @@ paths: summary: Storage usage + effective limits description: "The caller's vault usage and effective limits: `usedBytes` (every non-purged row — tombstones inside the 30-day undo grace still count), `quotaBytes` (per-user override ?? instance default), `maxFileBytes` - (admin-tunable per-file cap), `acceptedExtensions` for picker `accept` lists, and `linkedEpisodes` (episodes - carrying at least one live document link — the condition-filter chips). Read this before offering an upload." + (admin-tunable per-file cap), `acceptedExtensions` for picker `accept` lists, `linkedEpisodes` (episodes + carrying at least one live document link — the condition-filter chips), and `linkedProcedures` (visits of kind + PROCEDURE carrying at least one live document link, newest first — the procedure-filter choices; pass one's + `encounterId` to the list's `encounterId` filter). `linkedProcedures` is empty when a sharing grant does not + cover the visits section. Read this before offering an upload." responses: "200": description: Usage + limits. @@ -25929,7 +25933,10 @@ components: - last7days - last30days - last90days + - lastYear - allTime + description: "How far back the Coach may read, in every answer: snapshot, retrieval tools and summaries alike. `allTime` + (the default) is no limit. Formerly only the starting window; `lastYear` came with the limit." dataClusters: maxItems: 10 type: array @@ -32076,7 +32083,10 @@ components: - last7days - last30days - last90days + - lastYear - allTime + description: "How far back the Coach may read, in every answer: snapshot, retrieval tools and summaries alike. `allTime` + (the default) is no limit. Formerly only the starting window; `lastYear` came with the limit." dataClusters: maxItems: 10 type: array @@ -48822,7 +48832,10 @@ components: - last7days - last30days - last90days + - lastYear - allTime + description: "How far back the Coach may read, in every answer: snapshot, retrieval tools and summaries alike. `allTime` + (the default) is no limit. Formerly only the starting window; `lastYear` came with the limit." dataClusters: maxItems: 10 type: array @@ -68514,6 +68527,45 @@ components: - episodeId - name additionalProperties: false + linkedProcedures: + type: array + items: + type: object + properties: + encounterId: + type: string + occurredAt: + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$ + reason: + anyOf: + - type: string + - type: "null" + bodySite: + anyOf: + - type: string + - type: "null" + laterality: + anyOf: + - type: string + enum: + - LEFT + - RIGHT + - BOTH + - type: "null" + practitionerName: + anyOf: + - type: string + - type: "null" + required: + - encounterId + - occurredAt + - reason + - bodySite + - laterality + - practitionerName + additionalProperties: false assistAvailable: type: boolean description: "Whether a document read can run for this record now: a provider can serve it and the `documentAi` @@ -68539,6 +68591,7 @@ components: - maxFileBytes - acceptedExtensions - linkedEpisodes + - linkedProcedures - assistAvailable - contentIndex additionalProperties: false @@ -72427,6 +72480,20 @@ components: so this is what distinguishes a narrow score from a broad one. Optional so older cached digests without the field stay valid. $ref: "#/components/schemas/HealthScoreBasis" + steadyWeeks: + description: Whole weeks the score has held where it is (same band, within two points, computed the same way), read off + the stored daily scores. Null when it has not held for two weeks, when the stored record does not + reach today, or when the score on screen differs from the newest stored day in value, band, + algorithm version or composition. Shown in place of a delta, never beside one. + anyOf: + - type: integer + exclusiveMinimum: 0 + maximum: 9007199254740991 + - type: "null" + steadyAtLeast: + description: "True when the run reaches back past everything the read covered (120 days): `steadyWeeks` is then a lower + bound, said as \"at least\", and the run's start is unknown. Absent on older cached digests." + type: boolean required: - value - band @@ -72469,11 +72536,60 @@ components: anyOf: - type: string - type: "null" - description: First sentence of the cached briefing paragraph. Null while the `briefing` capability is unavailable; - `line` then falls to its deterministic floor. + description: "The first sentence of the cached briefing paragraph that says something: a greeting or a sentence that + only repeats the score is skipped. Null while the `briefing` capability is unavailable or when no sentence + qualifies; `line` then falls to its deterministic floor." + lead: + anyOf: + - type: object + properties: + text: + type: string + source: + type: string + enum: + - reaction + - briefing + - signal + description: "`reaction` and `briefing` are cached model text; `signal` is the deterministic sentence about the day's + strongest signal." + required: + - text + - source + additionalProperties: false + - type: "null" + description: "The Today hero's lead line, resolved: the reaction line or a briefing sentence while their capabilities + are available, otherwise a deterministic sentence about the day's strongest signal (a vital outside its + personal range, an unusual night, vitals in range, last night). Null when there is nothing to say. When the + deterministic sentence is built from a fact, that fact is left out of `today`. Additive since v1.40." + today: + maxItems: 5 + type: array + items: + $ref: "#/components/schemas/DailyTodayFact" + description: "The Today overview: up to 5 statements about the day in priority order (Rest Mode, medications taken + today, an appointment today or tomorrow that is not already on the rail, last night's sleep, vitals against + their personal range, cycle day), never padded. A fact about a switched-off module is absent. A narrow + surface shows the first 4. Additive since v1.40." + restMode: + anyOf: + - type: object + properties: + day: + type: integer + exclusiveMinimum: 0 + maximum: 9007199254740991 + description: 1-based day of the episode on the reader's calendar. + required: + - day + additionalProperties: false + - type: "null" + description: "Rest Mode: present while an illness episode is active and the illness module is on, else null. Additive + since v1.40." line: type: string - description: "Push / lock-screen line: cached-AI lead with a deterministic floor." + description: "Push / lock-screen line: cached-AI lead with a deterministic floor. Carries no number (no reading, dose or + score), since it is shown on a locked phone; the in-app `lead` keeps its figures." worthALook: type: array items: @@ -72535,6 +72651,9 @@ components: - score - topSignal - briefingLead + - lead + - today + - restMode - line - worthALook - justIn @@ -72543,6 +72662,39 @@ components: additionalProperties: false description: The one data spine of the daily-value system — composed from already-cached data, never a fresh AI call. Consumed by the Today surface, the daily push, and a future iOS widget. + DailyTodayFact: + type: object + properties: + kind: + type: string + enum: + - rest_mode + - medications + - appointment + - sleep + - vitals + - cycle + description: Closed fact kind. + label: + type: string + description: Localised label (resolved server-side). + value: + type: string + description: Localised value (resolved server-side). + href: + type: string + description: The page the fact belongs to. + moduleKey: + description: The module that admitted the fact; absent for core data. + type: string + required: + - kind + - label + - value + - href + additionalProperties: false + description: One statement about the day on the Today overview. A fact never asks the reader to do anything; the rail is + the only place that does. DailyPriorityItem: type: object properties: @@ -77933,7 +78085,10 @@ components: - last7days - last30days - last90days + - lastYear - allTime + description: "How far back the Coach may read, in every answer: snapshot, retrieval tools and summaries alike. `allTime` + (the default) is no limit. Formerly only the starting window; `lastYear` came with the limit." dataClusters: maxItems: 10 type: array diff --git a/docs/ops/deploy.md b/docs/ops/deploy.md index 4cfe63778..94f0c047d 100644 --- a/docs/ops/deploy.md +++ b/docs/ops/deploy.md @@ -78,6 +78,12 @@ roll back: dev --create-only --name vX_Y_Z_rollback_
- {t("daily.today.allClear")} -
- )} + {todayBlock} + {rail ?? + (hasFacts ? null : ( ++ {t("daily.today.allClear")} +
+ ))} {sleepPendingNote}+ {signalLine.headline} + {signalLine.headline && signalLine.delta ? " · " : null} + {signalLine.delta} +
+ ) : null}- {topSignal.headline} - {topSignal.delta ? ( - - {" "} - · {topSignal.delta} - - ) : null} -
- ) : null} + {todayBlock} {compactAllClear ? (- - {t( - settled === "adopted" - ? "insights.coach.rememberMessage.done" - : "insights.coach.rememberMessage.duplicate", - )} -
- ); - } - - return ( -+ {t("common.loadFailed")} +
++ + {t("common.loading")} +
+ ); + } + + return ( ++ {t("insights.coach.frame.operatorManaged")} +
+ ) : null} ++ {t("insights.coach.frame.modelManaged")} +
+ )} + + {reasoning ? ( ++ {t("insights.coach.frame.saving")} +
+ ) : null} + {notice ? ( ++ {notice} +
+ ) : null} + {error ? ( ++ {error} +
+ ) : null} ++ {meta} +
+- {label} -
- ); -} diff --git a/src/components/insights/coach-panel/method-line.tsx b/src/components/insights/coach-panel/method-line.tsx index 60a2227b3..3b6246b8e 100644 --- a/src/components/insights/coach-panel/method-line.tsx +++ b/src/components/insights/coach-panel/method-line.tsx @@ -1,11 +1,10 @@ "use client"; /** - * v1.39.4 — how an answer was worked out, as one muted line inside the - * evidence disclosure: sources, windows, counts and aggregation, rendered + * v1.39.4 — how an answer was worked out, as one muted line at the end of + * the open steps list: sources, windows, counts and aggregation, rendered * on the server in the request locale. Never a health value, so it sits in - * the meta tier beside the key values it explains rather than repeating - * them. + * the meta tier. */ import { useTranslations } from "@/lib/i18n/context"; import { COACH_METHOD_KEYS } from "@/lib/ai/coach/dialog-keys"; diff --git a/src/components/insights/coach-panel/read-aloud.tsx b/src/components/insights/coach-panel/read-aloud.tsx index ac94adba6..2fe931eae 100644 --- a/src/components/insights/coach-panel/read-aloud.tsx +++ b/src/components/insights/coach-panel/read-aloud.tsx @@ -260,9 +260,9 @@ export function ReadAloudButton({ content }: { content: string }) { className={COACH_ICON_BUTTON} > {speaking ? ( - + ) : ( - + )} ); diff --git a/src/components/insights/coach-panel/scroll-to-bottom-button.tsx b/src/components/insights/coach-panel/scroll-to-bottom-button.tsx new file mode 100644 index 000000000..73aeb8af8 --- /dev/null +++ b/src/components/insights/coach-panel/scroll-to-bottom-button.tsx @@ -0,0 +1,54 @@ +"use client"; + +/** + * The round "jump to latest" button over the bottom of the Coach thread. + * + * It fades in once the reader has scrolled away from the end and fades out + * at the end. While hidden it stays mounted (so the fade has something to + * run on) but leaves the accessibility tree and the tab order and takes no + * pointer events. With reduced motion it switches without a transition. + */ +import { ArrowDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { useTranslations } from "@/lib/i18n/context"; + +export interface ScrollToBottomButtonProps { + visible: boolean; + onClick: () => void; +} + +export function ScrollToBottomButton({ + visible, + onClick, +}: ScrollToBottomButtonProps) { + const { t } = useTranslations(); + const label = t("insights.coach.answer.scrollToBottom"); + return ( ++ {t( + `insights.coach.lookback.${activeText.detailKey}`, + activeText.detailParams, + )} + {bounded ? ` ${t("insights.coach.lookback.note")}` : null} +
- {t("insights.coach.settingsSourcesPointer")}{" "} - - {t("insights.coach.settingsSourcesPointerLink")} - -
+ {sourcesSummary(draft, t)} + +