Commit Graph
2369 Commits
Author SHA1 Message Date
Marc-André Bombeck 0f6669847c Merge remote-tracking branch 'origin/main' into fix/ai-timeout-everywhere 2026-10-02 23:59:38 +02:00
Marc-André Bombeck 2473d52f19 Merge pull request #1089 from MBombeck/feat/manual-workout-entry
Log a workout by hand
2026-10-02 23:59:11 +02:00
Marc-André Bombeck ae0bdd0390 fix(settings): name the real response-timeout defaults
The placeholder said "Default (~120)" while most surfaces fall back to
60 s and the briefing to 180 s. The placeholder and the help line now
interpolate both numbers from the constants the server applies
(PROVIDER_DEFAULT_TIMEOUT_MS, AI_BUDGETS.comprehensive.timeoutMs), in
all seven locales, and a test fails if a locale writes a number of its
own.

Refs #1090
2026-10-02 23:17:37 +02:00
Marc-André Bombeck bf7fb9c401 fix(documents): wait as long as the server may on document reads and lab OCR
The browser gave up on its own: 120 s for the document routes, 90 s for
a lab scan, and the 15 s default on the lab text path, while a slow
self-hosted model with a raised response timeout was still allowed to
answer. A five-page report read by a local model failed on the client
side with the server still working.

Each request now sizes its abort from ai.provider.responseTimeoutMs on
the account payload, times the model calls the route can make, plus
30 s: one call for suggest and summary, three for Read with AI (the
transcription, then the lab staging read and its corrective retry), two
for a lab scan (the read and its retry). With the setting unset that is
90 s, 210 s and 150 s.

Refs #1090
2026-10-02 23:01:41 +02:00
Marc-André Bombeck 5ee1b3e28f fix(ai): run every model call under the response-timeout setting
The response-timeout setting reached the briefing, the status notes and
the Coach, which threaded it by hand. Document reads, document
extraction, lab OCR and lab staging called the provider without a
timeout, so every client fell back to its own 60 s literal and a slow
self-hosted model was cut off however high the setting stood.

The setting now rides on the provider instance. resolveProvider,
resolveProviderChain and resolveProviderForTest stamp the record's
aiResponseTimeoutSeconds on everything they return, and each wire
client reads its ceiling through callTimeoutMs: the setting, else the
surface's own timeoutMs, else 60 s. A call site no longer has to pass
it, so a new one cannot forget it. The connection test runs under the
same ceiling a read does.

The Coach nudge tick and the reaction line keep their short ceilings
(timeoutPolicy: "surface-ceiling"): the tick runs accounts one after
another under a shared budget, the reaction line's claim lease is
shorter than the largest setting, and both have deterministic copy
that stands in.

/api/auth/me publishes the effective value as
ai.provider.responseTimeoutMs, so a client can size its own request
timeout from it instead of guessing.

Refs #1090
2026-10-02 23:01:36 +02:00
Marc-André Bombeck d9dfa7e5db test(dashboard): type the module cases of the add-menu parity check
Refs #1085
2026-10-02 21:44:37 +02:00
Marc-André Bombeck cf678f6ec4 fix(dashboard): gate every add-menu entry on its module, as the capture picker does
The dashboard add menu asked only whether the record's section was
writable, so a switched-off mood or medications module still offered its
entry here while the bottom bar's picker hid it. The menu now renders from
the picker's own visibleCaptureKinds, so the two cannot disagree.

Refs #1085
2026-10-02 21:38:06 +02:00
Marc-André Bombeck 6eb79a1d47 fix(workouts): report manual saves through the outcome module and register the delete
A repeated submit that wrote nothing reads as a neutral notice rather
than a success. The delete control is entered in the destructive-control
registry, and its 409 code in the published error catalogue.

Refs #1085
2026-10-02 21:31:25 +02:00
Marc-André Bombeck 715268c62b feat(workouts): offer Log workout in the add menu and the capture picker
The dashboard add menu and the bottom bar's capture picker gain a
workout entry next to measurement, mood and medication. It belongs to
the workouts module (capture:workout) and is offered in one's own record
only, because the batch ingest resolves the caller and no grant reaches
it.

Refs #1085
2026-10-02 21:23:01 +02:00
Marc-André Bombeck e0fc879d72 feat(workouts): log a workout by hand from the workouts page
A small sheet with sport, start, duration and optionally distance and
active energy, nothing else. The start is read in the profile timezone,
distance in the reader's unit and stored in metres. It saves through
POST /api/workouts/batch as source MANUAL with an external id minted once
per opened form, so a second tap lands as a duplicate, not a second row.

The workouts page carries the action in its header and in the empty
state; the detail page offers delete for a hand-entered workout.

Refs #1085
2026-10-02 21:22:56 +02:00
Marc-André Bombeck 885efaafd9 fix(dashboard): open the body fat page from the body fat tile
The body fat tile was the one strip tile that led nowhere. Its href map
said there was no dedicated sub-page, which was true: body fat percentage
was tiled and charted on the dashboard but never got the Insights page
every body-composition sibling has.

Add /insights/body-fat on the generic metric-page scaffold (body group,
tab-strip pill, seven locales, manual-entry empty state) and link the
tile to it.

Move the tile href map out of the page into tile-destinations.ts, keyed by
the tile-capable widget id and typed against the sub-page slug union, so
a missing or non-existent destination fails to compile. The link passes
the destination's insights-page surface gate, so it never points at a
page a switched-off module hides.

A guard derives the tile set from TILE_CAPABLE_WIDGET_IDS and from the
ids the dashboard actually pushes into its strip, and fails when a tile's
metric has a sub-page the tile does not open, when a destination has no
route, or when a tile links to a page owned by another module. Tiles that
deliberately lead nowhere need a written reason; there are none today.

The reverse-lookup test pinned BODY_FAT as having no page; it now
resolves to body-fat.

Refs #1084
2026-10-02 21:21:38 +02:00
BernhardBuckel a2a3414b29 feat(labs): give the lab-scan review stage the width a large screen offers
The review stage is a list of readings to compare and correct, but it sat in
the default 448 px dialog, so with several readings it only grew taller.

- ResponsiveSheet gains an opt-in 6xl width tier (72rem). Nothing else
  changes width; the Sheet branch on phones ignores the tier as before.
- The review stage uses it. The picking stage stays narrow.
- Each row lays out from the width of its container, not the window:
  stacked in a narrow dialog, and once the dialog is wide enough the reading
  sits on the left with its fields on the right.
- The field grid (value, unit, date) is a container of its own, so it takes
  three columns when the fields have room for them, whatever the window
  width. The result-text span follows the same query.
- The dialog-overflow e2e spec covers the review stage at 390, 560, 768,
  1280, 1920 and 3440 px. 560 is wider than a phone's field grid needs but
  below the window sm breakpoint, which is what tells a container query from
  a window one.

Refs #1056
2026-10-02 09:14:51 +02:00
Marc-André Bombeck ba426706be Merge remote-tracking branch 'origin/main' into fix/v1398-polish 2026-10-01 22:23:09 +02:00
Marc-André Bombeck debd44a75b fix(charts): keep axis unit labels on one line; 44 px date and time triggers
Recharts word-wraps a tick label that it measures wider than its axis,
and it measures in the page's body font rather than the tick's 11 px.
"62 bpm" on the pulse chart under a workout and "106 mg/dL" on the
glucose page therefore broke onto two lines at every width, and the
pulse chart's second line sat on the first x tick. Axis units are now
joined with a no-break space through `axisUnitSuffix`, so the label is
one word and stays on one line. A guard fails when a chart axis joins a
unit with a plain space.

The calendar and clock buttons in the date and time fields paint at
32 px; a `::before` halo now gives them a 44 px touch target without
changing the field.
2026-10-01 22:18:28 +02:00
Marc-André Bombeck 801295f55a fix(dates): read date-only lab and document dates as calendar dates
A date-only value is stored at 12:00 UTC. The lab list and history, the
document cards and pickers, the document date suggestion, extracted
fact dates, source-picker rows and the compliance and mood heatmaps
formatted that instant in the reader's zone, which is the next day from
UTC+12 to UTC+14.

`useCalendarDate` formats such a value through the UTC-pinned calendar
formatter the vaccination list already uses, with the same field order
and locale as instant dates. Lab readings hold both kinds, so
`useLabDate` reads a noon-UTC anchor as its calendar date and a reading
typed with its time of day in the reader's zone. A document without a
filing date shows its upload day, an instant, in the reader's zone.
2026-10-01 22:14:07 +02:00
Marc-André Bombeck fe13fa966b fix(labs): print range bars and lab values in the reader's number format
Range bars and the lab list, detail rows, chart tooltip and reference
ranges printed numbers with a plain `String()`, so a German reader saw
"1.8 mIU/L" and "61.3 kg" under a German interface. The same was true
of the mean, GMI, eA1C and the advanced indices on the glucose panel.

`RangeBar` now prints value, bounds and delta through the locale number
formatter and takes an optional `formatValue` for surfaces with their
own precision. Lab values keep their trim rule (whole numbers bare, at
most two decimals) in `formatLabValue`, which now hands the digits to a
caller-supplied locale formatter; `useLabNumber` wires it to the
reader's locale for every lab surface. Inputs are unchanged.
2026-10-01 22:09:30 +02:00
Marc-André Bombeck 75361d0666 fix(glucose): state band edges in the reader's glucose unit
The time-in-range labels in the doctor report, the target caption on
the glucose panel, the glucose page description and the fasting
explainer printed fixed mg/dL figures, so a reader on mmol/L saw
"Low (< 70 mg/dL)" beside values in mmol/L.

The cut-points now live in one place (`GLUCOSE_TIR_CUT_POINTS`,
`GLUCOSE_FASTING_CUT_POINTS`) with the SI figure each guideline prints,
the analytics engine counts its bands from the same constants, and
every sentence takes the band edges and unit as parameters through
`glucoseBandParams`, formatted with the reader's number format.

The unit-preference guard now also reads the locale bundles: a string
that names mg/dL or mmol/L fails unless it is allowlisted with a
reason.

Refs #1067
2026-10-01 22:07:07 +02:00
Marc-André Bombeck 08d5acdcb5 feat(measurements): log a resting heart rate by hand
The entry form offered pulse but not resting heart rate, which has its own
chart and series, so a value read off a watch or taken on waking could only
land among spot pulses. The form now offers it next to pulse. The server
already gates the type on the recovery module (its resting_hr domain), so
the form, the dashboard summary, the resting-pulse page, its trend slot and
its correlation channel now follow that module too.

Refs #1079
2026-10-01 21:53:38 +02:00
Marc-André Bombeck 661378bfa9 Merge remote-tracking branch 'origin/main' into fix/v1397-units2 2026-10-01 13:47:10 +02:00
Marc-André Bombeck 0028290117 fix(cycle): read basal temperature and the unit-bearing chart titles in the reader's units
The basal body temperature field, its chart and its tooltip were fixed
to degrees Celsius whatever the reader had chosen: the placeholder said
°C, the field accepted only 30-45, the chart plotted Celsius and the
tooltip printed "{temp} °C". Two chart titles carried a fixed unit as
well ("Wrist temperature (°C)", "Walking speed (km/h)") above an axis
already in the reader's unit.

The field now reads and writes in the reader's unit and stores Celsius;
a reading the person did not touch is saved back unchanged rather than
through a round trip of rounding. The chart plots, pads and labels in
the reader's unit, the two titles drop the unit the axis already
names, and the cycle contrast's fixed kg, °C and mg/dL labels, which
the transform had already replaced, leave the bundles.

Refs #1067
2026-10-01 13:23:17 +02:00
Marc-André Bombeck 7abaa25c14 feat(backup): export the account settings and restore them onto the account
Both backup purposes now carry an accountSettings section. The restore writes
it to the account being restored inside the same transaction, names every
refused value under the new accountSetting skip catalogue, and re-warms the
stored texts after the transaction when a unit preference changed, the way the
unit settings routes do. A file without the section leaves the account row as
it is, which is how every older backup restores. The instance settings
restore stays its own opt-in.
2026-10-01 13:04:41 +02:00
Marc-André Bombeck 383323b542 Merge remote-tracking branch 'origin/main' into fix/v1397-meta 2026-10-01 11:29:08 +02:00
Marc-André Bombeck 03eced171e Merge remote-tracking branch 'origin/main' into fix/v1397-meta 2026-10-01 11:25:06 +02:00
Marc-André Bombeck ada490c7e9 Merge remote-tracking branch 'origin/main' into fix/v1397-tz 2026-10-01 10:59:51 +02:00
Marc-André Bombeck b4577f5c31 test: give every scanning guard a floor on what it found
A guard that compares a sweep against an allowlist, or against "none of
these may exist", passes when the sweep comes back empty. walkSourceFiles
already refuses a short walk; this adds the same refusal for what a guard
does with the walk.

- requireFloor(label, found, floor) throws below the floor and refuses a
  floor of zero.
- scanSourceMatches(root, pattern, { fileFloor, matchFloor }) walks, matches
  and floors both the files read and the hits found.

Floors added, pinned at the current counts:

- coach_usage owner columns: the clauses it actually examined (6).
- measurement reference index: the foreign keys it extracted (2).
- job outcome fact inventory: job files (130, floor 120) and fact keys
  (399, floor 350).
- tone harness: assessment prompt modules (11).
- coach locale default: turn pipeline modules (15, floor 12).
- locale integrity: message bundles (7).
- settings card header coverage and settings anatomy: the walked cards.
- coach metric scope inventory: insight route sources.
- side-effect drift guard: both enums populated, since two empty keysets
  are equal.

The pill-mapper guard walked with globSync, which skips dot-prefixed
directories; it now walks through walkSourceFiles like the rest.
2026-10-01 10:52:48 +02:00
Marc-André Bombeck 27ac4c1d63 Merge origin/main into fix/v1397-tz 2026-10-01 10:39:26 +02:00
Marc-André Bombeck da8cc7a6ee Merge remote-tracking branch 'origin/main' into fix/v1397-derive 2026-10-01 10:34:17 +02:00
Marc-André Bombeck a3b5480d04 Merge pull request #1069 from MBombeck/fix/v1397-units
One unit system: every surface follows the reader's glucose and metric/imperial preference
2026-10-01 10:34:06 +02:00
Marc-André Bombeck 533b75fdcf Merge remote-tracking branch 'origin/main' into fix/v1397-derive 2026-10-01 10:30:10 +02:00
Marc-André Bombeck 5f548d331f fix(labs): say when a scanned PDF was read from its first pages only
A PDF longer than the pages a scan renders was read from its first ten
pages, and the cut was recorded only in the wide event. The person checking
the extracted rows had no way to know that values on later pages were never
read.

The rasterizer now returns the PDF's page count, the extract response
carries pageCoverage { read, total } when the cap applied, and the review
screen says how many of the pages were read.
2026-10-01 10:05:35 +02:00
Marc-André Bombeck e37263e8cc fix(vaccinations): render a dose date through the calendar-date formatter
The noon-UTC rendering trick fixes the day west of UTC and moves it east
of UTC+12. A dose date is a calendar date, so it goes through the
UTC-pinned formatter the date fields already use, which is right in every
zone.
2026-10-01 10:01:17 +02:00
Marc-André Bombeck 06e86ef584 fix(ui): keep the page-header title a sibling of its description
The grid change wrapped the heading in a cell div, which separated it
from the description paragraph the settings-score spec reads as its
following sibling. The heading is the grid item itself now.
2026-10-01 10:01:14 +02:00
Marc-André Bombeck a66976874e fix(mood): report the tag and factor keys a mood write did not store
An unknown or archived tag key, or a factor the catalog no longer carries,
was dropped on the way in while the entry still reported "inserted" (bulk)
or 201 (create, edit). A client had no way to tell that part of what it sent
never landed.

Each write now names those keys: the bulk result per entry, the create and
edit responses on the entry, as droppedTagKeys and droppedFactorKeys. Both
are present only when non-empty, so a response where everything landed is
unchanged. An archived tag an edit preserved is on the entry and is not
listed. The web shows a short notice after saving when a picked tag was left
off.
2026-10-01 10:01:06 +02:00
Marc-André Bombeck 7c5ff89def feat(lint): refuse UTC day keys outside the time zone module
`instant.toISOString().slice(0, 10)` and parsing a key back as UTC
midnight are the idioms behind every day-boundary fix this year: the
evening west of UTC and the small hours east of it land on the
neighbouring day. A new rule, healthlog/no-utc-day-key, refuses them in
src/ outside src/lib/tz and tests, and refuses a disable directive for
itself that is not next-line or gives no reason.

The time zone module now names every legitimate form: userDayKey for a
person's day, dateOnlyKey / dateOnlyAtNoonUtc / dayKeyAsUtcMidnight for
date-only values and day labels, daysBetweenDateKeys / weekdayOfDateKey /
isCalendarDateKey / statedDateKey for key arithmetic and parsing.

Of the 133 sites the rule found, 84 are gone. Most moved onto a named
helper with no change in behaviour; these changed behaviour:

- cycle calendar, insights, Coach snapshot and prediction refresh start
  their lookbacks at the first instant of the user's day
- a model-transcribed date like "June 10, 2026" no longer lands a day
  early in a process east of UTC (documents and lab scans)
- a custom report range covers exactly the picked days in the profile
  zone instead of starting the evening before west of UTC
- date-of-birth pickers cap at the user's today, not the UTC one
- an allergy onset date is stored at noon UTC like other date-only values
- the report's cycle summary, the Coach's usual BP range and the intake
  projector's rollup recompute use the user's day
- vaccination dates render as dates instead of the previous evening

49 remain, each with its reason on the line above: 39 UTC by design (15
file name stamps, rollup and object keys, the UTC-day score engines) and
10 marked baseline. A guard test requires a classified reason on every
directive and holds the baseline to a ceiling that only goes down.
2026-10-01 09:59:57 +02:00
Marc-André Bombeck fdbbd3a84a fix(settings): gate the Coach tuning on the record's module state
Settings → Coach hid its preference and nudge cards on the raw disableCoach
column, which /me publishes for the signed-in person. Inside a managed or
shared record that answered for the wrong person: a guardian with the Coach
on saw the tuning for a record that has it off, and the other way round.

The cards now read the module map the server resolves for the record on
screen, the same map the navigation and the settings entry already follow.
2026-10-01 09:50:41 +02:00
Marc-André Bombeck eae46ed535 fix(medications): preselect only a dose offered today, graded by the account's thresholds
The intake pickers chose their default from every active medication, so a
course that had ended but was not archived could be the preselected dose.
The default now reads the server's intakeActionable answer; the medications
page dialog still lists ended courses for back-filling a past dose, they are
just never the default.

Both pickers also graded "due" against the fixed 120 / 240-minute defaults
while the cards beside them used the account's own late and missed
thresholds. They read the same thresholds now.
2026-10-01 09:48:48 +02:00
Marc-André Bombeck 537ac3c7a1 fix(settings): stop settings and admin cards widening the page on a phone
Three cards put a full sentence into the SettingsCardHeader status slot
(mood reminder, low-stock warning, the app-log refresh button), which
does not wrap: the row grew to 1000px and /settings/notifications
panned sideways at every width. The sentence moves into the card body,
and the slot is capped at the row so a long status wraps instead.

Also on a phone: the provider-chain add control wraps, the report
picker's standard-template action drops under its sentence instead of
running past the card, and the dashboard layout rows put their switches
and arrows on a second line so the tile name is no longer cut to five
characters.
2026-10-01 09:44:43 +02:00
Marc-André Bombeck dc38540573 fix(ui): give the page-header description the full row on a phone
With three or four icon actions beside the title, the description was
left a ~130px column that wrapped five or six times. Below sm it now
spans the row under the title and actions; from sm up the actions span
both rows exactly as before.

A one-word German title wider than the column beside the actions ran
underneath them; the heading hyphenates by the page language and breaks
as a last resort. Notifications, whose two actions are text links,
moves them under the description on a phone (stackActionsOnPhone).
2026-10-01 09:44:43 +02:00
Marc-André Bombeck 24e6972f95 fix(labs): keep the biomarker chart axis at zero for a range starting at zero
A reference window of 0-130 padded the axis to -13, a tick that reads
like a value for a concentration that cannot be negative. The pad only
goes below zero when the data already does.
2026-10-01 09:44:30 +02:00
Marc-André Bombeck 4a13004917 fix(labs): fit the lab list, values list and add sheet to a phone
The compact list was a five-column table at every width. Below the fixed
columns (badge, 12rem range bar, 72px trend, chevron) the name column
collapsed to nothing, the badge sat over the value and the row panned
sideways off the card. Below lg the same cells now fold into two lines
per row; the desktop subgrid is unchanged.

The values table needed ~510px, so on a phone its edit and delete
buttons sat in a sideways-scrolling strip and the range bar labels were
clipped. Below lg each reading is a stacked row with its actions beside
it.

The add sheet's three footer buttons did not fit a phone row in German
and pushed Cancel off the left edge; the secondary save takes a row of
its own there, and the footer wraps instead of overflowing on desktop.
The scan review's 'links to existing marker' chip truncates instead of
running past its row.
2026-10-01 09:44:29 +02:00
Marc-André Bombeck 5820078008 fix(environment): default the date fields to the user's own day
The travel and backfill pickers defaulted to the UTC day, which was
tomorrow east of UTC in the small hours and yesterday west of it in the
evening, and the backfill start cut the home's effective instant the same
way. Both now read the day in the account's zone.
2026-10-01 09:39:09 +02:00
Marc-André Bombeck 9a282ed698 fix(units): keep converted values on the rounding helpers
The narrative, the workout distance and the elevation chart converted
through the unrounded escape hatch, and the trend caption scaled its
noise floor off the raw factor. They now take the rounded helpers, so a
narrated pound or mmol/L figure reads at the grain every screen shows
it, and the caption's floors are declared canonically (the distance
floor in metres). The trends row joins the chart-feeding surfaces that
may hand a raw scale to a chart.

Refs #1067
2026-10-01 09:17:44 +02:00
Marc-André Bombeck 8a11e60bb3 fix(cycle): state the cycle-phase glucose row in the reader's unit
The cycle-phase table printed the bundle's mg/dL label beside raw mg/dL
values for every reader. Weight, temperature and now glucose rows take
their unit and conversion from the one reading resolver; every other
row keeps its bundle label.

Refs #1067
2026-10-01 09:10:16 +02:00
Marc-André Bombeck bc2d60cce8 fix(insights): plot the trends row and log fevers in the reader's units
Two more surfaces carried a fixed unit. The insights trends row labelled
weight in kg and body temperature in °C for every reader, and plotted
walking distance as raw metres under a km label; its trend caption
said the same. A single transformed series there now takes unit, scale
and offset from the preference, and the caption describes the converted
series against a converted noise floor.

The illness day sheet and timeline showed and took a fever in °C. The
fever is now seeded, typed and shown in the reader's temperature unit
and stored in °C as before.

Refs #1067
2026-10-01 09:08:11 +02:00
Marc-André Bombeck d6f79c8bef fix(workouts): show distance, pace and elevation in the reader's units
The workout list, the detail header, the stat tiles, the sport-average
line and the elevation chart printed kilometres, minutes per kilometre
and metres for every reader. They now read distance, pace and elevation
through the unit registry (km or mi, /km or /mi, m or ft). The splits
table keeps its kilometre rows, which are cut server-side, and says so.
A pace that rounded to sixty seconds now carries into the minute.

Refs #1067
2026-10-01 09:03:51 +02:00
Marc-André Bombeck d0a9086538 fix(targets): edit glucose thresholds in the reader's glucose unit
The settings threshold editor took the identity path for the glucose
thresholds, so an account on mmol/L was shown a mg/dL default and asked
for mg/dL figures against 40-400 bounds, while the insights sheet for
the same threshold already spoke mmol/L. The shared target adapter now
owns the glucose unit as it owns pounds: fields, inward-rounded bounds,
the default hint and the canonical write all go through it, and the
insights panel and its sheet use the same branch instead of their own
conversion.

Refs #1067
2026-10-01 08:58:24 +02:00
Marc-André Bombeck d00e106ce1 fix(clinician): print every shared statistic with its unit
The clinician view printed bare numbers for every measurement group and
the per-context glucose rows in raw mg/dL, so a recipient read glucose
without a unit and in the wrong one for an account on mmol/L. The PDF's
measurement table had the same gap for the raw glucose series.

Both artefacts now take the unit and the displayed number from one helper
(doctor-report/stat-display.ts): SI for mass, length and temperature as
before, hours for sleep, and the record owner's unit for glucose.

Refs #1067
2026-10-01 08:46:39 +02:00
Marc-André Bombeck 02be4d3dff Merge branch 'main' into fix/ocr-review-date-validation 2026-10-01 00:20:48 +02:00
Marc-André Bombeck 85d11bc364 Merge branch 'main' into fix/v1396-small-fixes 2026-09-30 23:27:25 +02:00
Marc-André Bombeck a5d19bfe98 Merge pull request #1064 from MBombeck/fix/backup-summary-manifest
Answer the restore preview from counts kept with the backup
2026-09-30 23:25:26 +02:00