Commit Graph
5233 Commits
Author SHA1 Message Date
Marc-André Bombeck 43361bd74e fix(daily): write the briefing for the reader's own morning and readings
With signals of the day resting on today's readings only, the briefing
had to be written after those readings, and it was only written at the
04:30 Berlin warm, the post-sleep morning refresh, or a page open on a
day-old cache. A morning blood pressure or weigh-in never became a
signal, and since an earlier day's briefing is no longer served, a
reader in Los Angeles or Seoul had none for most of the day: the Berlin
warm lands the previous evening there, or at noon.

The snapshot read every surface goes through (dashboard, iOS, digest,
morning push) now asks for a briefing for today at two moments: the
first read of the reader's local day with no briefing written today,
and the first read after a signal metric's first reading of the day
that the briefing has not read. Each reason is claimed once per user
per local day in a Postgres bucket and remembered in-process, so later
reads cost no query; no write path needs a hook.

A claim enqueues one today warm: immediately for a new day, after a
five-minute window for readings, so a burst (weigh-in, then blood
pressure) folds into one job. The warm writes the comprehensive
briefing only, past the one-hour freshness gate but under the failure
backoff, the daily forced-warm cap (two per 24 h) and the content-hash
gate.

Worst case per user per local day: one enqueue per reason, at most
eight (the day plus seven signal reading types, measured over a day of
reads every two minutes); at most two forced comprehensive attempts
under the existing cap, each at most three completions (the answer, a
JSON retry, a grounding retry), so six provider calls at the ceiling.
A warm whose data has not changed costs at most the existing
once-a-day paragraph re-roll, inside the same cap.
2026-10-05 22:09:41 +02:00
Marc-André Bombeck f6b0131731 fix(daily): serve the briefing as it stands today on every surface
Only the digest applied briefingForToday. The dashboard snapshot (the
web dashboard, its server prefetch and the iOS cold-launch read) and
the /insights advisor payload still served an earlier day's text and
the raw model deltas ("+33.72 bpm").

The snapshot read now resolves the briefing after the cache, beside
the capability decision, from the payload's generation moment and the
snapshot's own newest reading per type; a withheld briefing reads
"preparing", never "ready" with nothing in it. The digest takes the
resolved briefing from that read on its own clock instead of resolving
again. The advisor route resolves dailyBriefing on every branch it
returns, reading the newest reading per signal type in one grouped
query only when a reading-backed signal is present; text generated on
an earlier day no longer counts as fresh on the read GET (so the warm
is enqueued) and no longer short-circuits a provider-backed POST.

The OpenAPI descriptions of both advisor responses say so.
2026-10-05 22:03:58 +02:00
Marc-André Bombeck 491d542f58 fix(daily): judge the briefing's day by when its text was written
The check that keeps an earlier day's briefing off the Today surfaces
read the cache row's insightsCachedAt. Every warm moves that column,
including the unchanged-hash short-circuit that keeps yesterday's text
and only refreshes the stamp, so text written the day before passed as
today's after the nightly warm.

The cached payload now carries briefingGeneratedAt, written only where
new text is written: a full generation in the warm and in the
on-demand route, and the daily paragraph re-roll. The stamp refresh
leaves it untouched. briefingForToday reads that moment, and a payload
without it counts as not from today, so the hero falls back to its own
lead until the next generation.
2026-10-05 21:57:49 +02:00
Marc-André Bombeck 14a2931a99 fix(daily): read signal deltas in the reader's number format
formatSignalDelta matched only the locale's decimal mark, so an
English-formatted figure inside German text was cut at the comma
("+1,234.5 kg" read "+1,2.5 kg"), and a difference that rounded to
nothing lost its sign ("+0.04 kg" read "0.0 kg").

A figure with a decimal or grouping mark is now read as a whole: with
a point and a comma the later one is the decimal, thin spaces and
repeated marks are grouping, and a single mark is grouping only when
it is the locale's grouping mark before exactly three digits. The
figure is re-rounded to the metric's precision and re-written with the
locale's own marks. A non-zero difference that rounds to nothing reads
"±0" at that precision. Plain integers stay as written.

Glucose joins the reading types, its precision following the unit the
delta is written in (whole mg/dL, one decimal of mmol/L). The map is
keyed by string, because the published sourceMetric enum has no
glucose value yet and widening it would change the iOS contract.

Tests cover all seven locales.
2026-10-05 21:54:01 +02:00
Marc-André Bombeck ab14747013 Merge branch 'fix/dashboard-hero-mobile' into release/v1.40.6 2026-10-05 21:38:21 +02:00
Marc-André Bombeck 861d1ba868 Merge branch 'fix/coach-panel-search-composer-ring' into release/v1.40.6 2026-10-05 21:38:14 +02:00
Marc-André Bombeck ff0769f045 fix(dashboard): lay the Today hero out for a phone
Below md the score ring sat centred under the Today facts as a 168 px
dial, where the bottom navigation covered it. The hero is now one
grid with two arrangements: on a phone the ring is a compact 80 px
dial at the top right beside the lead and signal line, flush with the
card's trailing edge, and the Today facts run the full card width
under both; from md up the ring spans both rows of the trailing
column exactly as before (desktop captures are pixel-identical).
Without a lead the facts take the leading cell, so the ring never
sits beside an empty row.

ScoreRing gains a `responsive` size (80 px, md:168 px) whose label is
left out below md; the ring keeps its aria-label and the surrounding
link names it. The meta lines under the phone dial are capped to its
width, so the lead keeps at least 180 px beside it at 360 px.

Each fact now stacks its muted label above its foreground value on
the reading edge at every width, instead of a label-left /
value-right row that broke long values into a ragged right-aligned
block. Rows stay at the 44 px tap floor below md.

The Today overview e2e covers 1440, 390 and 360 px in both themes
for an AI lead, a long AI lead, an AI-off day and a quiet day, and
asserts the dial size, its place beside the lead, symmetric insets,
full-width facts and stacked 44 px rows on a phone.
2026-10-05 21:30:29 +02:00
Marc-André Bombeck ac48e0d0ed fix(coach): give a series read its numbers when the shared build cut its block
Inside a chat turn every tool read lands on the turn's shared full-source
snapshot when its window matches. On an account with many synced series
that build passes the prompt budget, and the budget pass collapses blocks
to a bare unit and then to an `{ omitted }` marker. get_metric_series
sliced the marker out and answered `present: true` with it, so the model
was told the readings exist and was given no figure. The same held for
sleep, glucose, compliance and workouts.

- The snapshot result names the blocks the budget pass cut. A tool whose
  block was cut in the shared build reads it again from its own source at
  the same window.
- A read scoped to one source condenses its own blocks last and never
  swaps them for the marker: it keeps a summary (first, last, min, max,
  mean, change) taken before anything is cut, the newest daily values,
  weekly means folded into monthly means, and names each cut.
- get_metric_series never answers present with a block holding no figure;
  that read reports retrieval_failed instead.
- A chat series read carries the readings it counted, so the step shows
  them, and on a long or condensed window a note on what it covers. The
  tool description and a new tool-mode rule tell the model to call
  get_metric_table itself for longer history instead of asking.
- The screen-reader comma between step parts is unselectable, so a copied
  row no longer reads "Weight · ,all time".

MCP output is unchanged: the source-snapshot equality suite stays green.
2026-10-05 21:29:04 +02:00
Marc-André Bombeck aa222f5b02 fix(daily): speak about today only from today's readings
A statement that says "today", or compares today against a baseline,
now only rests on a reading taken on the current calendar day in the
reader's zone. Yesterday morning's pulse read at nine in the evening
was narrated as today's: the signals of the day accepted any reading
from the last two weeks, and the Today overview, the coincident
deviation flag and the health status counted yesterday as current.

- computeSignalsOfDay drops a metric with no reading today, so the
  briefing prompt and its content hash never see a stale value as the
  day's; the prompt says so in both languages.
- The digest serves no cached briefing written on an earlier day and
  drops a reading-backed signal whose metric was not measured today,
  so the web hero, the iOS topSignal and the morning push agree.
- The Today overview's vitals line and deterministic lead, the
  coincident flag and the health status deviations use the new
  isFromToday rule (calendar day, not elapsed hours).

Signal deltas now carry each metric's display precision: the prompt
input is rounded (whole bpm and mmHg, one decimal of kg), and the
delta string under the hero lead is re-read at that precision in the
reader's locale ("+34 bpm", not "+33,72 bpm").
2026-10-05 21:22:57 +02:00
Marc-André Bombeck bed47387de fix(ui): show text-field focus as a neutral border, not a purple ring
Every text-entry control (Input, Textarea, Select and NativeSelect
triggers, the date and time fields, the calendar's dropdowns, the Coach
composer and the hand-built note fields) now marks focus by turning its
border the new neutral input-focus tone, about 5:1 on the dark
background and 4:1 on the light one, with no ring. A text field matches
:focus-visible on every mouse click, so the ring was on for everyone.
An invalid field keeps its red border and draws its destructive ring
only while focused. Buttons, links, checkboxes and switches keep their
rings.

text-field-focus-guard.test.ts fails when a text-field class string
brings a focus ring or a primary/ring-coloured focus border back.
2026-10-05 21:08:48 +02:00
Marc-André Bombeck b6ce54e2c6 fix(coach): drop the search field from the conversations panel
The panel lists the history grouped by recency with load-more, rename
and delete; title search stays on the full /coach/conversations page.
2026-10-05 21:05:13 +02:00
Marc-André Bombeck 315b653079 fix(coach): keep the docked panel header and new-chat button clear of the safe areas
A standalone PWA on a tablet reserves the status bar in the top bar but
not in the docked panel's header row, which could tuck its title and
icons under the status bar; the new-chat button sat without the home
indicator inset. Both now reserve the same insets as the top bar.
2026-10-05 19:47:50 +02:00
Marc-André Bombeck 61f065c651 feat(coach): run the conversations panel the full height of the window
The docked panel was part of the page, below the top bar. It is now a
column of the shell beside the content column, portalled through a new
side-panel slot: it runs from the top of the window to the bottom, flush
right, and the top bar ends at its left edge. Shut, the panel is gone
entirely and the top bar runs to the window's edge. The panel's header row
is the top bar's band, so the two bottom borders draw one line; the panel
takes the sidebar surface and border, like the rail on the left.

The top bar says where the reader is on desktop: "Coach", then the open
conversation's title, truncated. The panel toggle is its last item, 12 px
from its trailing edge, so it stands against the open panel and at the
window's edge when the panel is shut; it carries a tooltip and is 28 px
beside a fine pointer, 44 px with touch.

New chat is a round 48 px primary button with a plus, 24 px in from the
panel's corner (16 px in the phone sheet), with a tooltip. The top bar's
action slot and the new context and side-panel slots share one portal
helper.
2026-10-05 18:46:27 +02:00
Marc-André Bombeck 3c59fc168e test(coach): document that a hidden stored view chip still resolves
Reading a stored reply back drops a view chip whose table the answer
already shows with its toggle, but the resolver reads the raw stored
chips. A tap from a client that still shows such a chip is answered, by
design: the chip was a valid offer when it was stored. The test pins both
halves for "as a table" and "as a chart".
2026-10-05 17:53:18 +02:00
Marc-André Bombeck 7533a3c5de test(shell): pin the full-bleed condition to exactly /coach
The condition moves out of the shell into isFullBleedPage(), a pure
helper with its own test: /coach renders edge to edge; /coach/plans,
/coach/conversations, a trailing slash, look-alike paths and other
routes keep the padded frame, and so does a refusal on /coach (outside a
shared record, module off).
2026-10-05 17:52:45 +02:00
Marc-André Bombeck b2c188bc46 fix(coach): keep the answer details open on Enter and Space
Focus opens the details tooltip, and the click that Enter or Space then
fires toggled it shut again. A click with no pointer-down before it is a
keyboard activation and now always opens; a tap or a mouse click still
toggles, and Escape closes it.
2026-10-05 17:52:14 +02:00
Marc-André Bombeck 11b6119482 refactor(shell): return the switched-off module instead of a flag
The flag helper sat between the module gate's doc comment and the
component, so the comment read as its own. It is now
switchedOffModule(), which returns the owning module when it is off and
has a doc comment of its own above the gate's; the gate narrows on its
result directly instead of looking the module up twice.
2026-10-05 17:51:32 +02:00
Marc-André Bombeck c133b4ac74 fix(coach): size the panel's controls by pointer, not viewport width
The panel header buttons shrank to 28 px from md and the conversation rows
to 36 px from sm, so a touch tablet wide enough to dock the panel got
pointer-sized targets. They now shrink only under pointer-fine: 28 px
header buttons, a 36 px gear in the drawer, 36 px rows and row menus
beside a mouse or trackpad, the 44 px floor with touch at any width.

A unit test refuses a width breakpoint on a control size in the three
panel files; an e2e case measures the docked panel at 1280 px with touch
and with a fine pointer.
2026-10-05 17:50:55 +02:00
Marc-André Bombeck b0ac267af9 fix(coach): stop offering a view chip the result panel already toggles
A table the answer displays with a chart renders its own chart/table
toggle, and the "Show as a chart" / "Show as a table" chips under it
repeated that control. The server no longer derives a view chip for such
a table; a table the answer only used, folded under "Data used", still
gets "Show as a chart", since that brings it into the answer. A reuse
turn shows its table with the toggle and so offers no view chip either.

The rule is one predicate in a leaf module, applied where chips are
derived and where a stored reply is read back, so a reply stored before
the change offers the same chips as a new one on every client.
2026-10-05 17:41:20 +02:00
Marc-André Bombeck 0c3de5bdfb feat(coach): move an answer's model and tokens behind a details icon
The tokens and model no longer sit as right-aligned text under a Coach
answer. The row is one left-aligned group: copy, read aloud, try again, a
details icon, then the time. The details icon is a focusable button whose
tooltip names the model and the token count; it opens on hover, on
keyboard focus and on a tap, and a second tap, a tap elsewhere or Escape
closes it. Strings in all seven locales; the joined tokens-with-model
string is gone.
2026-10-05 17:29:28 +02:00
Marc-André Bombeck 3bede8230b fix(coach): mirror the panel toggle glyph and keep it beside the open panel
The toggle shows the panel-right open glyph while the panel is open and
the close glyph while it is shut, both mirrored so the chevron points the
way the panel moves on a click. Docked and open, the toggle keeps the
panel's width as its right margin, so it sits beside the panel's leading
edge and travels with it (200 ms, none under reduced motion). Labels and
aria-expanded are unchanged.
2026-10-05 17:25:24 +02:00
Marc-André Bombeck c49a04bac6 fix(coach): dock the conversations panel flush against the right edge
The Coach page cancelled the shell's container padding with negative
margins, so <main> kept its reserved scrollbar gutter and painted a dead
strip to the right of the panel on classic-scrollbar platforms. The shell
now renders /coach full bleed: no centred container, no padding and no
reserved gutter, and the page fills what <main> leaves below the top bar.
A refusal card (shared record, module off) keeps the padded frame.

The panel header takes one 12 px inset with a gap of 8 between title and
icons, a text-lg title, and 28 px icon buttons beside a pointer (44 px on
touch). Conversation rows take the control radius and a 36 px pointer
floor.
2026-10-05 17:24:49 +02:00
Marc-André Bombeck 22cb792379 fix(insights): read a same-hours day by its day mean
The same-hours standing averaged each day as sum / n. Pulse already folds
its day as the mean of its local hours' means, and the day-aggregate reader
returns that as dayMean; a type moved onto that reader would have been
compared raw. The standing now reads dayMean where the reader carries one.
2026-10-05 13:12:06 +02:00
Marc-André Bombeck abb1c9f8a9 fix(daily): count the ellipsis toward the lead budget
Text with no space inside the budget was cut at 160 characters and then
given an ellipsis, so the lead came out 161 long. The ellipsis now counts
toward the budget: a space at the edge still ends a whole word, and
unbroken text is cut one short.
2026-10-05 13:11:29 +02:00
Marc-André Bombeck 2a3bd67b68 fix(insights): cut glucose days at today's latest reading and say so
The same-hours comparison cut the earlier days at the current clock time.
At two in the afternoon a day holding only its fasting reading was then
held against mornings plus lunch, and read as below its range again. The
earlier days are now cut at the local time of today's latest reading.

A range read this way is the usual range for this time of day, not the
whole-day band other surfaces show. The deviation carries a basis, the
health-status API publishes it as `basis` (`day` or `sameHours`), and the
Today lead and the health-status card name the range as the one for this
time of day, in all seven locales.
2026-10-05 13:10:42 +02:00
Marc-André Bombeck 130ace74ee fix(crypto): keep updatedAt when key rotation re-seals a value
Key rotation rewrote every ciphertext column through a plain Prisma update,
in the in-app corpus walk and in the rotation script alike, so a rotation
stamped the run time on every encrypted row: the Coach panel then listed
old conversations as recent, and every client re-pulled measurements and
mood entries whose sync cursor had moved. Both now read each row's
updatedAt and write it back, matching on it, so a row edited since the
read is left for the next run instead of being stamped. The models that
carry the column are listed beside the registry and checked against the
schema, and the updatedAt guard now covers the rotation writes.
2026-10-05 13:07:22 +02:00
Marc-André Bombeck 7400852fb4 fix(daily): match a lead's metric by whole words
The terms that decide whether the Today lead already names the top
signal's metric were bare stems: "risen" and "chosen" matched the Polish
"sen" (sleep), "impulse" matched "puls", "Passing" matched the Italian
"passi", and the Korean "잠" matched "잠시" (a moment). A lead about
weight then dropped a sleep headline. Every term now has to start a word,
whole-word terms have to end one (Unicode-aware on both sides), and the
Korean terms carry their particle.
2026-10-05 13:02:04 +02:00
Marc-André Bombeck a6d841d3ae fix(insights): compare a glucose day in progress with the same hours
Today's glucose mean was held against a band built from whole days. By
mid-morning the day holds a fasting reading and nothing after, so an
ordinary morning read as "below your range". A glucose day still in
progress is now compared with the earlier days cut at the same local clock
time, read in SQL so a dense stream is averaged whole rather than through
its newest fifty rows. With fewer than seven earlier days that have
readings in those hours, today gets no verdict. The day-aggregate reader
gains the local-clock cut; an integration case pins it against the
in-memory fold in a half-hour zone.
2026-10-05 12:40:55 +02:00
Marc-André Bombeck 7d42cf7122 fix(coach): keep updatedAt through storage-only rewrites
The title encryption backfill moved each Coach conversation's title into
ciphertext with a plain Prisma update, which stamps updatedAt. The Coach
panel orders and groups conversations by that column, so threads untouched
for months showed up under Today. The memory summary refresh did the same
in the background.

Storage-only rewrites now carry the row's own updatedAt and match on it, so
a turn that lands meanwhile wins and the row is retried on the next pass:
conversation titles, practitioner contacts, measurement and mood notes
(where updatedAt is the sync cursor), inventory notes, and the mood-note
script. The summary refresh writes through raw SQL. A guard freezes both
rules: every backfill write on a model with @updatedAt names the column or
is listed with the reason the stamp is wanted, and every conversation
write outside the person's own actions keeps it.
2026-10-05 12:40:55 +02:00
Marc-André Bombeck 3381026acb fix(insights): give a personal band a minimum width
When most days in the window carry the same value, the median ± k·MAD band
has no width: the coach strip read "Your usual range is 61–61 bpm" and the
next ordinary reading of 62 counted as outside it. The shared band builder
now keeps the edges at least the measurement's own day-to-day wobble apart
(per metric, in its canonical unit, falling back to one display step), so
every surface built on it reads a real range and classifies against it.
The reported spread stays the measured dispersion.
2026-10-05 12:40:55 +02:00
Marc-André Bombeck f5b63d33ab fix(charts): name every series by its localised metric label
The chart's label map covered eleven measurement types and fell back to
the raw enum for the rest, so a resting-pulse chart with its trend
overlays on showed "RESTING_HEART_RATE" in the trend line, the legend,
the tooltip and the spoken summary. Types outside the chart's short
labels now take their measurement label, which exists for every type in
every locale.
2026-10-05 12:40:55 +02:00
Marc-André Bombeck 524975d5f3 fix(daily): decide the line under the Today lead on the server
The muted line under the lead repeated what the lead had just said: a lead
about blood pressure sat above "Your latest blood pressure is sitting in
the optimal band · ↓ ~10 mmHg…". The web client only dropped the headline
when the lead quoted it word for word, and other clients had no rule at
all. The digest now carries `signalLine`, built next to the lead: when the
lead already talks about the top signal's metric (matched in every shipped
language) or says its headline, only the delta remains, and with no delta
there is no line. The hero renders the field verbatim.
2026-10-05 12:19:41 +02:00
Marc-André Bombeck 20085d5759 fix(daily): show the Today lead whole instead of cutting it mid-word
A lead past the 160-character budget was sliced at a character count, so
a long briefing sentence reached the hero as "…sleep landed r…". Model
text now leads only when it fits whole: the reaction line keeps the
complete sentences that fit, the briefing leads with its first sentence
when it fits, and otherwise the headline or the deterministic sentence
takes the slot. Only when no source fits is the first sentence shortened,
at a word boundary. The budget is unchanged, so the hero keeps its
height.
2026-10-05 12:16:40 +02:00
Marc-André Bombeck a1ec3b1264 Merge branch 'fix/v1402-review' into release/v1.40.2 2026-10-05 08:42:28 +02:00
Marc-André Bombeck 4db7fc5113 fix(coach): open the deep-linked sheet only once the viewport is known
/coach?settings=data opened the conversations sheet from the first
render. The server and hydration renders do not know the viewport and
take the narrow layout, so on a desktop the sheet was open until the
docked panel replaced it; only the batching of the hydration re-render
kept it off the screen. The deep link now opens the sheet only once
hydrated on a viewport that does not dock the panel; on a docked one the
panel is revealed as before.

The desktop deep-link spec records every mount of the sheet from the
first byte and expects none; a phone deep-link spec covers the sheet
path and serves as the watcher's positive control.
2026-10-05 08:34:28 +02:00
Marc-André Bombeck 4bb03600b3 perf(dashboard): cache today's cycle read and run the vitals reads together
The digest is polled every 120 seconds. Its cycle line read the whole
cycle history, 90 days of day logs and wrist temperature on every poll,
outside any cache, and the coincident-deviation engine walked its eleven
vitals one after another, two queries each.

The cycle read now rides the digest extras cell, which every cycle write
already hard-evicts; the cycle preferences route evicts it too, and a
module switch does through the health-score invalidator. A record without
a cycle profile stops after one point read. The per-vital reads run
together and are folded in registry order, so the result does not depend
on which finished first.

On a seeded four-year account (1.25M measurements, 52 cycles) under a
792 MB heap, the warm digest went from 9.9 to 5.0 ms and the vitals engine
from 11.1 to 4.6 ms (medians of four interleaved rounds).
2026-10-05 08:34:28 +02:00
Marc-André Bombeck bfcf71b7eb fix(dashboard): keep numbers out of the morning push line
The digest line is the body of the morning push and lands on a locked
phone. It took the briefing's first sentence, the top signal's headline
or the deterministic signal sentence verbatim, so a resting heart rate or
a blood pressure average could sit on the lock screen, and its score
floor stated the score.

A candidate with a figure in it is now passed over for the next, and the
floor says the score is ready instead of what it is. The hero's lead in
the app keeps its numbers. Tests that pinned the numeric line are turned
around, and the push builder is checked on a digest whose lead carries a
reading.
2026-10-05 08:34:28 +02:00
Marc-André Bombeck bd8e05e484 fix(dashboard): drop a lead sentence only when it repeats the score as the score
The lead filter dropped any sentence containing the score's digits, so a
score of 52 threw out "You slept 7 h 52 min" and a resting heart rate of
52 bpm. A sentence now repeats the ring only when it names the score (in
any shipped language, or as "out of 100") and carries the number on its
own, not as part of a time, a decimal or a measurement with a unit.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 9e647051d5 fix(dashboard): tell a steady score only when its band held, and say at least at the read's edge
The steady line compared the newest stored score with the number on
screen but not with its band, version or composition, so a score whose
band moved overnight at the same number was still called steady. A run
that reached back past the 120 days read was also told as if it started
there.

The newest stored day now has to match the ring in value, band and,
where the digest carries them, algorithm version and composition. A run
the read cannot see the start of is marked as open (steadyAtLeast on the
digest score) and the hero says "at least" in seven locales.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 87cf32bc67 fix(coach): leave derived scores out when their window does not fit the limit
The derived block shortened each engine's window to the lookback limit,
so a 7-day limit produced a readiness computed over 7 days instead of the
30 the ring on the dashboard uses, under the same name. A block whose own
window does not fit is now left out, as the lookback rule says, and the
coincident-deviation flag follows its 30-day baseline the same way.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 8dc34c7691 fix(coach): keep GLP-1 dose changes before the lookback limit out
The GLP-1 block sent every dose change with its decrypted note whatever
the lookback limit said. Dose changes are entries, so only those inside
the limit reach the prompt; the dose in force stays as the current dose.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 308e154978 fix(coach): move focus to the composer before hiding the jump button
Jumping to the latest message pinned the thread first, which hides the
button behind aria-hidden, and only then moved focus to the composer.
Focus now leaves the button before its state changes.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 3b283507b5 fix(dashboard): lead with the stored lines for a digest cached before the lead
The hero reads only digest.lead. A digest the service worker cached
before the update carries no such field, so the hero lost its lead line
until the next fetch. Such a digest now falls back to the reaction line
or the briefing lead it was built to show; a current digest whose lead
is null stays silent.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 35edc33871 fix(coach): lock the reasoning select while a change saves
The provider and model selects in the Coach's settings already lock while
a write is in flight; the reasoning select did not, so a second PATCH
could race the first and one change could be lost.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck eda0b5831d feat(settings): summarise what the Coach sees beside the link to its settings
Settings → Coach pointed at the gear with a muted paragraph. It now says
how many data areas the Coach reads and how far back it looks, from the
saved preferences, next to an Open Coach settings button that lands on
that section.
2026-10-05 08:11:10 +02:00
Marc-André Bombeck 816269b3ed fix(documents): match the filter triggers to the search field's height
The facet triggers and the clear control were 32 px beside a 44/40 px
search field, below the phone tap floor. They now share its height and
reach 44 px square on a phone, and the browser's own clear button no
longer sits on the search icon when the field shrinks.
2026-10-05 08:11:10 +02:00
Marc-André Bombeck 88406f8e66 fix(dashboard): give the Today overview room under the lead and phone-sized rows
The overview sat 12 px under the lead sentence; it now keeps a section's
distance (16 px, 24 px from md). On a phone each fact row is a link and
now meets the 44 px tap floor.
2026-10-05 08:11:10 +02:00
Marc-André Bombeck 150ee7695c fix(coach): lift toasts clear of the panel's New chat button
Bottom toasts landed on the round New chat button for their whole
lifetime, so the delete Undo blocked the next action. While the
conversations panel is open, docked or as a sheet, bottom toasts rise by
the button's footprint.
2026-10-05 08:11:09 +02:00
Marc-André Bombeck 07f4961f27 fix(coach): label the quick-settings fields alike and give the lookback full width
The model fields carried the form label's colon and the lookback picker
an uppercase section-style label and a fit-content 36 px trigger. All four
fields now share one unadorned label and the full-width 44/40 px control.
2026-10-05 08:11:09 +02:00
Marc-André Bombeck d0b680c277 fix(dashboard): count the last hour of a 25-hour day in today's doses
buildMedsTodayBlock ended the day at local midnight plus 24 hours. On the
day the clocks go back that stops an hour early, so a dose between 23:00
and midnight was neither projected nor counted in the doses tally. The
exclusive end is now the next local day's start, as the digest loader
already computes it.
2026-10-05 07:48:58 +02:00