Commit Graph
6164 Commits
Author SHA1 Message Date
Marc-André Bombeck 90ca2cd899 chore(api): regenerate the OpenAPI contract
Picks up the managed-profile read and edit endpoints, the sex field on the
create body, the record-scoped module map on the account payload, and the
delegated cycle field on the record's module settings.

Refs #939
2026-09-10 00:02:15 +02:00
Marc-André Bombeck fb1cb4e3ed feat: gate a switched browser on the record's modules, not the actor's
Turning a module off for a profile you look after wrote a column nothing
on screen was reading. GET /api/auth/me published the ACTOR's module map
while switched, and the navigation, the dashboard tiles and the insights
sections all gate on it — so a guardian who turned Cycle off for the child
kept seeing a Cycle destination inside the child's record, and a guardian
who tracks it saw one there whether or not the child did (#939).

The map and the resolved cycle flag now answer for the record the session
is inside. They are not display preferences: every surface they gate shows
the record's data, so the record is what decides. Locale, theme and layout
stay the actor's. The record is taken from the grant-validated active
entry rather than the session's own selector, so a revoked switch cannot
publish somebody's configuration, and with no switch the payload is what
it was.

Cycle also gets a row on the guardian's Modules surface, which it never
had. It is a delegated module, so its state is the record's own cycle
profile and modulePreferences.cycle is refused — which left a guardian no
way to turn it off for a record whose sex says it should be on. The family
names the real column instead of a blob entry the gate ignores.

Refs #939
2026-09-10 00:02:03 +02:00
Marc-André Bombeck f521b03c35 feat: let a guardian edit a managed record, and record its sex
A managed profile could be created and deleted and never changed. A name
typed as a placeholder was permanent, a household that moved kept the
timezone the record was created in, and there was no way to correct a date
of birth somebody finally had (#939).

PATCH /api/managed-profiles/{id} takes the fields creation takes, every
one optional and at least one required — an empty body is a refusal rather
than a save that audits a change nobody made. It carries the gate the rest
of the family carries: a cookie session with a fresh second factor, an
active Guardian grant on the record, and the same byte-identical 404 for
"no such record" and "not yours". A GET beside it answers the record as it
stands, because the account payload names a managed record and nothing
else, and a form filled from that would have had blank fields to send.

The new field is gender. Leaving it out of v1.37.0 meant every managed
record started with the column NULL, which is what the cycle module
derives its default from: a guardian creating a record for a child could
neither record the answer nor turn the module off from there. It reuses
the column and the three values every other surface writes, so there is
one word for one column rather than two.

Refs #939
2026-09-10 00:01:50 +02:00
Marc-André Bombeck 1d372df3a0 Merge pull request #942 from MBombeck/fix/managed-record-capabilities
v1.38.13 — a guardian sees the controls the routes accept
v1.38.13
2026-09-09 22:03:41 +02:00
Marc-André Bombeck 8a3499d0f9 chore(release): add the v1.38.13 changelog entry
The release commit carried the version anchors and the OpenAPI document but not the changelog entry; it is added here.
2026-09-09 21:38:42 +02:00
Marc-André Bombeck 40b9427259 chore(deps): let the newer hono and js-yaml bounds win
pnpm-workspace.yaml carried two overrides for the same packages: an older floor (hono < 4.12.25, js-yaml >= 4.0.0 < 4.3.1) and the newer one added for the scanner findings. The older selector won the resolution, so the lockfile kept hono 4.13.3 and js-yaml 4.3.1 and four of the seventeen findings stayed open after v1.38.12. The stale bounds are gone; the lockfile now resolves hono 4.13.7 and js-yaml 4.3.2.
2026-09-09 21:38:16 +02:00
Marc-André Bombeck d464a3efe4 Merge remote-tracking branch 'origin/main' into fix/managed-record-capabilities
# Conflicts:
#	public/sw.js
2026-09-09 21:38:11 +02:00
Marc-André Bombeck 3f67a78952 Merge pull request #941 from MBombeck/ci/trivy-install
Install Trivy from its release, not through apt
v1.38.12
2026-09-09 20:58:38 +02:00
Marc-André Bombeck 915bd63a4a chore(release): v1.38.13 — a guardian sees the controls the routes accept
Version anchors, the OpenAPI document and the changelog entry. Reported in #939.
2026-09-09 20:55:06 +02:00
Marc-André Bombeck 386e92e6f9 test(sharing): carry the two domain lists through the account-access contract
The account-access integration contract compares whole entries, so the
two lists the payload now publishes belong in its expected shapes. The
managed-record leg posts the mood entry with the logged-at instant the
create schema requires.
2026-09-09 20:06:48 +02:00
Marc-André Bombeck 77eaf22c0c ci: install Trivy from its release, not through apt
The scanner job installed Trivy through apt, which runs `apt-get update`
across every repository on the runner image. On the v1.38.12 merge
commit the runner's preinstalled Chrome repository answered with a
corrupt index ("Hash Sum mismatch") twice in a row, and the release gate
went red on a step that has nothing to do with the image under test.

The step now fetches the pinned release binary through Trivy's own
installer, which downloads the tarball for that exact version and checks
it against the published checksums. No apt, so no unrelated repository
can fail the job.
2026-09-09 20:04:13 +02:00
Marc-André Bombeck 4b3e71570c test(sharing): drive the managed-record capabilities over the routes and the browser
The integration leg reads GET /api/auth/me as a guardian acting on a
managed profile and asserts both lists, lands a mood entry and an
allergy on the profile's record, is refused a vault write, and shows a
WRITE delegate writable sections without manageable ones together with
the refused mood entry the list withheld.

The browser leg opens the managed profile, adds a mood entry through
the control that used to be hidden and asserts the POST's status, and
finds the owner-only sentence and no upload control on the documents
page. The spec gets its own session jar and runs in the desktop
project only, like every other spec that moves the record selector.
2026-09-09 19:42:27 +02:00
Marc-André Bombeck e95612e3d3 fix(sharing): show a guardian the controls the routes accept (#939)
useRecordCapabilities answers per section now. canWriteDomain and
canManageDomain read the two lists the account payload publishes; in
one's own record they are always true, and under a refused, pending or
unproven context always false. canManage becomes the coarse switch,
true when any section is manageable, and is what the banner and the
navigation read.

Every add, edit and delete control names its own section: labs, mind,
profile, illness, cycle, medications, measurements, documents. Creates
that a WRITE route answers (visits, practitioners) ask canWriteDomain.
The generic delete button, the selection bar, the capture picker, the
dashboard quick entry and the bottom nav take the section from their
callers. Controls whose routes resolve the caller rather than the
record (settings shortcuts, chart overlay preferences, custom metrics,
the Today rail, OCR, efficacy targets, credential tabs) ask for the
caller's own record instead, and the customise-shortcut guard now
requires that shape.

The vault has no delegated write route, so its controls stay withheld
under every grant, and the documents page says so in one sentence
where the upload control would be, in all seven locales.
2026-09-09 19:42:27 +02:00
Marc-André Bombeck b5c392636f feat(sharing): publish writable and manageable domains on the account payload
Every accountAccess entry on GET /api/auth/me carries writableDomains
and manageableDomains: the sections the grant opens, intersected with
the sections whose routes accept a delegated write at that level. A
READ grant gets two empty lists, a WRITE grant gets writable sections
only, a MANAGE grant gets both, and the vault appears in neither.

The client parser checks the structural invariants (no repeats, within
the grant's sections, manageable within writable, nothing below the
level) and requires the two published views of the active record to
agree on the lists. Regenerate the OpenAPI document and extend the
payload consumer guard with the active-record readers.
2026-09-09 19:42:26 +02:00
Marc-André Bombeck 54f0030ead feat(sharing): freeze which sections accept delegated writes
Add the one table of which record sections have a route declaring
requireRecordAuth("write") or ("manage"), derived from the route files,
and a structural guard that re-derives it from src/app/api on every run
and fails on a disagreement in either direction. The matcher tolerates
the line break a formatter puts after the opening parenthesis and
asserts a non-zero match count.

delegatedDomains() turns a grant's level and scope into the sections it
can add to and the sections it can change, keeping write within manage.
2026-09-09 19:42:26 +02:00
Marc-André Bombeck 925cc59ea5 Merge pull request #940 from MBombeck/chore/trivy-findings
v1.38.12 — the scanner findings are cleared
2026-09-09 19:38:19 +02:00
Marc-André Bombeck b8a3890ef4 chore(release): v1.38.12 — the scanner findings are cleared
Version anchors, the OpenAPI document and the changelog entry for the dependency release.
2026-09-09 19:19:50 +02:00
Marc-André Bombeck 312da96d9a build: type-check the application program, not the test tree, in next build
next 16.3 type-checks the whole tsconfig program during the build. That
reaches the unit tests under src/**/__tests__, their fixtures under
tests/, the Playwright config and the e2e tree, none of which is in the
image build context — .dockerignore leaves tests/ and e2e/ out on
purpose — so the image build stopped at "Cannot find module
'../../tests/fixtures/...'" on both architectures.

The build now reads tsconfig.build.json, which extends the main tsconfig
and excludes the test tree and the test-runner configs. `pnpm typecheck`
keeps using tsconfig.json and still covers the full program; the change
narrows only what `next build` compiles, which was already the case on
16.2 by omission. Verified by building with tests/ and e2e/ moved away.
2026-09-09 19:03:38 +02:00
Marc-André Bombeck 47953f5b8a chore(deps): clear the seventeen scanner findings
Trivy flags twelve distinct advisories on the lockfile and the image, none
in this code: next 16.2.11 (two critical, fixed in 16.3.3), sharp 0.35.3
pulled by next (high), nodemailer 9.0.3 (one high, three medium), js-yaml
4.3.1 (high), hono 4.13.3 under the MCP SDK's node server (three medium),
and the vitest mocker and baseline-browser-mapping (medium). Dependabot
lists none of them, which is why they sat.

next moves to 16.3.4 and nodemailer to 9.1.1 as direct pins; vitest to
4.1.11. js-yaml, hono, baseline-browser-mapping and sharp are transitive
and move through pnpm overrides bounded to the vulnerable ranges, so the
copies the scanner names change and nothing else does. Typecheck, lint,
the unit suite and the production build pass locally; the release gate
runs the integration and end-to-end suites on the minor Next bump.
2026-09-09 18:38:29 +02:00
Marc-André Bombeck 75d3fad04e Merge pull request #935 from MBombeck/dependabot/npm_and_yarn/types/pg-8.23.1
chore(deps-dev): bump @types/pg from 8.20.0 to 8.23.1
2026-09-08 21:46:24 +02:00
Marc-André Bombeck 5080624925 Merge pull request #934 from MBombeck/dependabot/npm_and_yarn/axe-core/playwright-4.13.0
chore(deps-dev): bump @axe-core/playwright from 4.12.1 to 4.13.0
2026-09-08 21:44:50 +02:00
Marc-André Bombeck 13a9c6576b Merge pull request #932 from MBombeck/dependabot/npm_and_yarn/lefthook-2.1.12
chore(deps-dev): bump lefthook from 2.1.10 to 2.1.12
2026-09-08 21:43:15 +02:00
Marc-André Bombeck 7a54aae1ca Merge pull request #931 from MBombeck/dependabot/npm_and_yarn/jose-6.2.10
chore(deps): bump jose from 6.2.3 to 6.2.10
2026-09-08 21:41:41 +02:00
Marc-André Bombeck c2bd573c99 Merge pull request #937 from MBombeck/release/v1.38.11
v1.38.11 — sign out everywhere means everywhere
v1.38.11
2026-09-08 21:06:06 +02:00
Marc-André Bombeck e0cf6c2403 chore(release): v1.38.11 — sign out everywhere means everywhere
Version anchors, the OpenAPI document, the changelog entry, and the
README sentence that still declared immunization records out of scope
while the vaccination record has shipped since v1.37.3.
2026-09-08 20:23:30 +02:00
Marc-André Bombeck 076c2f9c5d fix(auth): sign out the other devices, lose a refresh to a revocation, key the replay cache by authority
"Sign out everywhere" revoked every other refresh token and nothing else.
A native login is two rows, and the Bearer check consults only the
paired access token, so the other phones kept working until that token
expired, up to a day on the default policy. The route also handed
destroyOtherSessions a session-kind credential for a Bearer caller, whose
session.id is the ApiToken row id; the exception matched nothing and the
calling phone revoked its own refresh token. destroyOtherSessions now
finds the paired access tokens through the refresh rows and revokes them
inside the same transaction, reporting the count, and the route names a
Bearer caller by its access-token hash. Programmatic tokens are untouched.

rotateRefreshToken marked the consumed row under usedAt: null alone, so a
revocation landing between the read and the write was overtaken and a
pair minted after the family ended stayed live. The guard now includes
revokedAt: null; on a lost race the minted pair is retired and the reason
distinguishes a revoked family from a double consume.

withIdempotency consulted the grant only at replay time, and only for its
existence; a narrow-scoped token was checked only on the delegated arm.
Both move into the cell key: a delegated cell carries the grant's id,
level and scope fingerprint, a narrow token's cells carry the token, and
a request with no live grant neither reads nor claims a cell. Cookie and
wildcard own-record cells are keyed byte-for-byte as before.

Tests: integration for the paired access tokens (cookie and Bearer
caller), a deterministic revoke-mid-rotation race against Postgres, unit
coverage for the new cell keys, and the structural guard moved to the
new invariant.
2026-09-08 20:23:30 +02:00
dependabot[bot] 776e36a37f chore(deps-dev): bump @types/pg from 8.20.0 to 8.23.1
Bumps [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg) from 8.20.0 to 8.23.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

---
updated-dependencies:
- dependency-name: "@types/pg"
  dependency-version: 8.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-06 15:55:49 +00:00
dependabot[bot] 4feccf3a07 chore(deps-dev): bump @axe-core/playwright from 4.12.1 to 4.13.0
Bumps [@axe-core/playwright](https://github.com/dequelabs/axe-core-npm) from 4.12.1 to 4.13.0.
- [Release notes](https://github.com/dequelabs/axe-core-npm/releases)
- [Changelog](https://github.com/dequelabs/axe-core-npm/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/dequelabs/axe-core-npm/commits/v4.13.0)

---
updated-dependencies:
- dependency-name: "@axe-core/playwright"
  dependency-version: 4.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-06 15:55:18 +00:00
dependabot[bot] 846e735b58 chore(deps-dev): bump lefthook from 2.1.10 to 2.1.12
Bumps [lefthook](https://github.com/evilmartians/lefthook) from 2.1.10 to 2.1.12.
- [Release notes](https://github.com/evilmartians/lefthook/releases)
- [Changelog](https://github.com/evilmartians/lefthook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/evilmartians/lefthook/compare/v2.1.10...v2.1.12)

---
updated-dependencies:
- dependency-name: lefthook
  dependency-version: 2.1.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-06 15:53:56 +00:00
dependabot[bot] 0e51a17ab4 chore(deps): bump jose from 6.2.3 to 6.2.10
Bumps [jose](https://github.com/panva/jose) from 6.2.3 to 6.2.10.
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v6.2.3...v6.2.10)

---
updated-dependencies:
- dependency-name: jose
  dependency-version: 6.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-06 15:53:22 +00:00
Marc-André Bombeck a262cf7bb1 Merge pull request #930 from MBombeck/release/v1.38.10
v1.38.10 — the off-host backup streams, the rotated token sunsets
v1.38.10
2026-09-06 16:28:46 +02:00
Marc-André Bombeck 69fc8bf2e3 chore(release): v1.38.10 — the off-host backup streams, the rotated token sunsets 2026-09-06 16:02:50 +02:00
Marc-André Bombeck fe71c9bf6b Merge pull request #929 from MBombeck/fix/rotation-sunset
fix(auth): give the rotated access token a fifteen-second sunset
2026-09-06 16:01:17 +02:00
Marc-André Bombeck dfb5d7483f fix(auth): sunset the rotated-away access token instead of revoking it
Rotation revoked the access token paired with the consumed refresh token
the instant the rotation committed. A native client with requests already
on the wire got 401 revoked on those, read that as "re-authenticate",
rotated again with the refresh token it had just spent, and tripped reuse
detection: the device's whole token family died because the client was
busy at the boundary.

Pull the paired token's expiry in to fifteen seconds instead. It is long
enough for an in-flight request on a slow mobile link to finish, including
a retry, and short enough that the token gains nothing meaningful over the
validity it already had. bearer.ts answers a passed expiry with reason
expired, which a client treats as "refresh", not as "sign in again".

LEAST(expires_at, $1) takes the minimum inside the single UPDATE, so the
write can only ever shorten: a token already inside its last seconds keeps
its own expiry, and there is no read-then-write window. The bound value is
an ISO-8601 UTC string cast in SQL so the comparison does not shift with
the host's timezone.

Reuse detection is untouched. A replayed refresh token, or one presented
under a spoofed device id, still revokes the family and its access tokens
on the spot, and so does the logout path.
2026-09-06 15:36:35 +02:00
Marc-André Bombeck 48fa96d4b6 Merge pull request #927 from MBombeck/fix/offhost-backup-streaming
fix(backup): stream the off-host copy so it fits a small container
2026-09-06 14:25:19 +02:00
Marc-André Bombeck 040cc6b767 Merge pull request #928 from MBombeck/docs/credit-1389-reporters
docs(changelog): credit the two reporters behind v1.38.9
2026-09-05 20:24:58 +02:00
Marc-André Bombeck 3ee3461f92 chore(backup): drop the whole-string builder the streaming path replaced
Nothing calls it any more: the weekly pass and the off-host job both
walk the payload through the streaming writer. Keeping an exported
function that builds the entire record as one string invites the next
caller to reintroduce the memory profile this branch removes.
2026-09-05 20:23:08 +02:00
Marc-André Bombeck 9d1246b525 docs(changelog): credit the two reporters behind v1.38.9 2026-09-05 20:09:20 +02:00
Marc-André Bombeck 13a9aacf20 fix(backup): stream the nightly off-host upload instead of building it
The off-host job was said to benefit from the streaming work the weekly
in-database backup got, and it did not. Only the payload builder was
shared: `runOffhostBackup` called `buildFullBackupJson` for the whole
document, `gzipSync` over that whole string, a whole-buffer
`createCipheriv` pass over the result, and handed the finished buffer to
a single `PutObject`. Four full copies of the record alive at once, on
the same arithmetic the weekly pass had already been fixed for.

Configuring an S3 target on a long-lived record therefore restarted the
container. On an account of 445 000 measurements the JSON alone is
242 MB, and against a 1 GB container's 524 MB heap the run died of heap
exhaustion seventeen seconds in — taking every signed-in session with
it, because the job shares the app process.

The write path is a stream end to end now: `streamFullBackupJson`
produces the document a page at a time, gzip and an incremental
AES-256-GCM writer consume it as it arrives, and `@aws-sdk/lib-storage`
puts it up in two 8 MB parts. What the process holds is fixed by the
pipeline's shape rather than by the record. Measured on that same
445 000-measurement account under `--max-old-space-size=450`, with the
heap pre-loaded to what a warm server holds: the old path dies with
`Ineffective mark-compacts near heap limit`, the new one finishes with a
peak 79 MB above its baseline and writes a 9.1 MB object.

The stored envelope gains version 3, which moves the GCM tag from in
front of the ciphertext to the end. That is the only change the stream
required — the tag exists only once the last block is in, so a leading
one means the whole object must exist before its first byte can be sent.
Nothing about the authentication moves: the tag still covers every
ciphertext byte and is verified before a byte of plaintext comes back.
Versions 1 and 2 still read, so every object already in a bucket
restores through the same reader with no flag to tell them apart.

The incremental writer is the crypto module's, split into its byte-level
half so the base64 form the in-database blob uses and the raw form an
object needs are one implementation under two framings. The off-host key
handling is unchanged: a separate `BACKUP_ENCRYPTION_KEY`, 64 hex or
32-byte base64, passed in rather than looked up.

A run that uploaded nothing for anybody is a failed job now, not
`ok: true` with an empty bucket behind it. Wrong credentials, a missing
bucket and an unreachable endpoint all fail every account rather than
one, and the target's own sentence rides out as the cause. A run where
some account got a copy still succeeds; retrying the whole cohort over
one object would re-upload everybody's.

The one ceiling left is structural rather than a memory bound, and it is
counted rather than discovered: a multipart upload carries 10 000 parts,
so an object past 80 GB is refused for that account with a clear message
instead of failing halfway through with an SDK error about part numbers.

`tests/integration/offhost-backup-streaming-memory.test.ts` pins the
budget from both sides — what the streaming uploader holds, and that the
materialising path does not fit the same budget on the same fixture in
the same process, so the number cannot pass by measuring nothing.
2026-09-05 19:13:11 +02:00
Marc-André Bombeck 1cd398897e Merge pull request #926 from MBombeck/release/v1.38.9
v1.38.9 — date order everywhere, plain-HTTP writes, gateway errors named
v1.38.9
2026-09-05 18:56:12 +02:00
Marc-André Bombeck e9358170bc chore(release): v1.38.9 — date order everywhere, plain-HTTP writes, gateway errors named 2026-09-05 18:21:20 +02:00
Marc-André Bombeck 5694e25e68 Merge pull request #924 from MBombeck/fix/gateway-error-surfacing
fix(ai): say what the gateway actually answered instead of "empty content"
2026-09-05 18:17:42 +02:00
Marc-André Bombeck d4ee3480f1 Merge pull request #925 from MBombeck/fix/date-format-everywhere
fix(format): carry the date-order preference to every numeric date
2026-09-05 18:16:24 +02:00
Marc-André Bombeck 703ca2744e Surface what an OpenAI-compatible gateway actually refused
A gateway can answer a failed generation with HTTP 200 and put the reason
in the body: a top-level `error` object and no `choices`, or
`choices[0].error` beside `finish_reason: "error"` when a generation dies
mid-flight. Both fell through to the empty-content throw, so a dead credit
balance, a data-policy block, a rate limit and a model that is down all
recorded the same hop: status 0, reason "returned empty content", no
excerpt. Nothing on the provider health card told them apart.

Raise the shape a non-2xx reply raises instead, with the gateway's own
message leading and its `error.code` as the status when that code is an
HTTP error status. The chain classifier, the health ledger and the card
then see 402 / 403 / 429 / 503 and treat each of them the way they treat
the same status on the wire. Message and excerpt are bounded and
key-redacted by the same helper the non-2xx path uses, so remote text is
no more trusted here than it is there.

Gated on the gateway tag: api.openai.com signals every failure with a
status, so the pinned tags parse exactly as before, and a genuine empty
reply stays the empty-content throw it was. The local-endpoint provider
had the same hole on its buffered path, its buffered-reply fallback and
its stream, and gets the same treatment. The body classifier learned the
two account-side refusals a gateway words in its own vocabulary rather
than the vendor's.
2026-09-05 17:56:47 +02:00
Marc-André Bombeck 031600d69d fix(format): carry the date-order preference to every numeric date
The per-user date order reached the entry form and nothing else. A
reporter switched away from MM/DD/YYYY, the date picker followed, and
the dashboard chart axes and the measurements list kept the old order.

`makeFormatters` took `timeFormat` and `dateFormat` as optional
parameters defaulting to "AUTO", so six of the seven constructions in
the tree silently rendered the locale default and nothing anywhere
said the setting had been dropped. Drop both defaults. The compiler
then names every caller: the chart bucket labels, the legacy
`@/lib/format` helpers the measurements list renders through, the
medication compliance and efficacy charts, the doctor-report PDF and
the clinician share view.

Thread the preference:

- `makeBucketLabelFormatters` takes the order; the health and mood
  charts read `useDateFormatPreference()`. The UTC pin decides which
  day the label names, the preference decides how it is spelled, and
  the two are separate questions.
- `format.ts` reads `readStoredDateFormat()` beside the timezone and
  hour-cycle mirrors it already read.
- The PDF takes `dateFormat` as a required render option, alongside
  `timeFormat`, which was optional for the same reason.
- The clinician share view spells the OWNER's dates the owner's way.
  It also picked up the owner's hour cycle, which it had been ignoring
  while the PDF route beside it already honoured it. Both resolve
  through the new `resolveUserFormatPreferences`, the counterpart to
  `resolveUserTimezone`.

Textual months stay on the UI locale. The preference pins numeric
field order by rendering through de-DE / en-US / en-CA, and a textual
month through those would change the month name's language.

Add `date-order-explicit-formatter-guard`: a raw numeric date renderer
must take its locale from the preference or carry a written reason,
and a formatter construction may not hard-code "AUTO" for the order.
Its allowlist holds thirteen calendar-day key builders, each with the
reason it has no order to get wrong. The sibling timezone guard passes
every one of them, because naming a calendar says nothing about
spelling a date.

Refs #922
2026-09-05 17:06:34 +02:00
Marc-André Bombeck 0913e8f2b7 Merge pull request #923 from MBombeck/fix/upload-https-hint
fix: writes and uploads work again on a plain-HTTP self-host
2026-09-05 16:48:04 +02:00
Marc-André Bombeck 5556478691 Make uploads and every other write work over plain HTTP
crypto.randomUUID() is [SecureContext] in the Web Cryptography IDL, so a
page served over plain HTTP never gets it. Measured in Chromium against
http://<lan-ip>: isSecureContext false, randomUUID and subtle undefined,
getRandomValues still a function.

The document vault minted its queue id and Idempotency-Key that way, inside
the file-input change handler. On a LAN self-host the call threw before a
queue row existed, so picking a file produced no upload, no error and no
trace — the picker simply looked inert, and the only way through was to
reach the app over HTTPS. The client transport has minted a default
Idempotency-Key the same way since v1.37.18, which put every apiPost and
apiPatch on the same footing: on such a host nothing wrote at all.

Add randomId(), which prefers the platform call and otherwise assembles a
v4 UUID from getRandomValues — the same CSPRNG, the same 122 bits, no
secure context required — and route every browser-side call site through
it. Nothing in an upload needs one: the bytes travel over multipart XHR and
every cryptographic operation happens on the server. Passkeys, web push and
home-screen install still need HTTPS; browsers withhold those outright.

A structural guard fails a direct crypto.randomUUID() anywhere outside the
two server-side files that may keep it, so the class cannot return one call
site at a time.

Also point the CSV and JSON import cards straight at the import guide. They
resolved through a redirect under the integrations tree, which is the wrong
place for a guide that is not an integration runbook. The suite that
covered those links pinned the old spelling rather than the guarantee; it
now asserts an absolute docs-site URL, no internal path, and new-tab
hygiene.
2026-09-05 16:22:39 +02:00
Marc-André Bombeck 295d70988d Merge pull request #921 from MBombeck/release/v1.38.8
v1.38.8 — the HRV tile for ring and strap accounts, and a token ceiling
v1.38.8
2026-09-03 21:28:44 +02:00
Marc-André Bombeck 315341cbb8 chore(release): v1.38.8 — the HRV tile for ring and strap accounts, and a token ceiling 2026-09-03 21:02:49 +02:00
Marc-André Bombeck c0cbe10033 Merge pull request #899 from Antiheld86/fix/dashboard-hrv-rmssd-fallback
Fix/dashboard hrv rmssd fallback
2026-09-03 21:01:25 +02:00