Commit Graph
5964 Commits
Author SHA1 Message Date
Antiheld86 a778acac4e chore: regenerate OpenAPI spec 2026-08-31 12:48:18 +00:00
Antiheld86 7c23b14053 Teach the arrival-wiring mock about isScopedCredential
That file stubs @/lib/api-handler wholesale, so it lists the exports it
knew about when it was written. /api/measurements now reaches for
isScopedCredential to decide source attribution and whether the sync
checkpoint may move, the mock does not provide it, and every test in the
file dies on import rather than on an assertion.

`false` is the honest stub: those cases resolve as a cookie session,
which is exactly what unscoped means, so the route takes its ordinary
path through both rules.

Checked the rest of the tree rather than fixing only the file that went
red — this is the only suite mocking that module for a route which
reaches the new export. The batch route's tests mock at a different
seam and are unaffected.

Not verified here: this environment cannot generate the Prisma client,
so that file is one of the suites which fail to import for an unrelated
reason and the fix could not be run.
2026-08-31 12:14:51 +00:00
Antiheld86 19b69dee21 Fix the B8 delegation cases to actually claim a record
Three failures in the integration run, all mine, none in the product.

The two delegation cases set the account selector as a header on the
NextRequest. Nothing reads it there. `readSelectorHeader()` resolves
through `next/headers`, which this suite mocks against the shared
`headerJar`, so the carrier came back `none` and both cases quietly
exercised the ordinary own-record path instead of the one they name. The
scoped case wrote to its own record and returned 201; the wildcard
control wrote to the caller rather than the owner. Neither was testing
delegation at all.

That is worse than a broken test. The refusal these two cover is the
regression this change could most easily have shipped in silence, and
the tests standing in front of it were not looking at it.

They now set the selector on the jar, through the exported
ACCOUNT_SELECTOR_HEADER rather than a literal, so a rename cannot
reintroduce the same gap.

The third failure is unrelated and smaller: the read-leg case read the
audit row once, racing a fire-and-forget write, and reported `undefined`
for a row about to exist. It now polls, matching B1, which carries a
comment about this exact hazard that I did not follow.

Not verified here: this environment has no Docker daemon, so the
integration suite cannot be run against a real Postgres. The diagnosis
is from the mock and the resolver, not from a green run.
2026-08-31 11:57:26 +00:00
Antiheld86 56d6fb180a Make the measurement-token mint cookie-only
The mint used requireAuth() with no scope, which refuses a narrow token
but still admits a cookie-equivalent one. The reach is not the problem —
a wildcard token can already do everything this endpoint does. The
lifetimes are: a native access token lives a day, and what it mints here
lives a year. So a credential that leaked for an afternoon could leave
behind one that outlives revoking it, and the revocation would look like
it had worked.

requireCookieAuth() closes that. It is the helper the passkey
registration and trusted-device routes already use for the same reason,
and the sharing guard recognises it as cookie-only.

The prose said more than the code did, in three places: the route header
and the OpenAPI description both claimed the endpoint "cannot mint
another token", which held only for narrow tokens, and the 403 arm
described a scope refusal that a Bearer never reaches. All three now say
what is true — no Bearer credential reaches this endpoint at any scope,
and a Bearer is refused 401 before the operator switch is read.

Tests follow the status change: the integration case asserting a scoped
token cannot mint now expects 401 rather than 403, since the refusal is
authentication and not scope, and a companion case pins the wildcard —
the credential class this change is actually about — and asserts no
token row is created.
2026-08-31 11:38:16 +00:00
Antiheld86 8f61e0bc6a Plainer copy on the measurement token card
The card read like API documentation sitting in a settings page: it named
the Authorization header, spelled out both endpoint paths, and listed the
scope's limits as four clauses. None of that is what someone deciding
whether to mint a token needs, and the endpoint paths were already in the
API endpoints card immediately above it.

Rewrite to the shape the MCP connector card next door uses — a short
title, one line on what the token is for, one line saying it is shown
once, and one sentence on what it can and cannot do. Drop the endpoints
note entirely rather than rephrase it, since the card above owns that.

Say the useful consequence instead of the mechanism: readings arrive as
if entered by hand, so they can be corrected. That is the thing a person
will actually notice.

Translations were revised across all seven locales — the previous German
in particular read as translated English rather than as the rest of the
app, which uses short informal sentences.
2026-08-31 09:50:47 +00:00
Antiheld86 e79709379b Fix duplicate status keys in the new OpenAPI response blocks
`stdResponses` already carries 401, 422 and 429. The three response
blocks added with the measurements:write scope each declared their own
422 (and, on the mint route, a 429) BEFORE spreading it, so the generic
wording overwrote the specific wording and TypeScript refused the
duplicate key. The build failed at auth.ts with "'422' is specified more
than once".

Move the explicit keys after the spread, which is what the MCP mint next
door already does and the only order in which a per-route description
survives.

Worth stating because the failure mode is quiet: had TypeScript allowed
it, the published contract would simply have carried "Request validation
failed" everywhere instead of naming
measurement.create.source_not_permitted and its siblings.
2026-08-31 09:22:38 +00:00
Antiheld86 cba7a0fce5 Add a self-service measurements:write API token scope
Since v1.30.17 there has been no self-service path to a token that can
write a measurement. That release made the Bearer scope default
fail-closed, which closed a real privilege-escalation hole, and removed
the generic mint at POST /api/tokens along with it. The generic mint had
been the only way to get a token that could reach /api/measurements, so
a local sensor pushed through Home Assistant or Node-RED lost its door
while the OAuth integrations kept theirs.

Add a narrow scope accepted by exactly two routes, both writes, both on
the holder's own record.

The scope constant is a leaf module beside the surface that consumes it,
matching FHIR_READ_SCOPE and the MCP scopes. requireRecordAuth grows an
options object rather than a sibling function, because the sharing
surface guard keys every leg on that one identifier and a second name
would mean two copies of the delegable declaration. AuthContext carries
the admitting scope as a single nullable string rather than the token's
permission array, so nothing downstream can re-derive an authorisation
decision from it.

A scoped credential is refused a record selector before any grant is
read. Placing the refusal above the sharing check rather than beside it
means no future widening of what a grant permits can reach one of these
tokens.

Rows are attributed MANUAL. An entry that explicitly names APPLE_HEALTH
is refused 422 rather than relabelled: that source is half the
(userId, type, source, externalId) dedup key the iOS client writes into,
it participates in the cross-source merge, and it gates the HealthKit
sync fields. For the same reason a scoped write no longer advances
User.lastSyncedAt, which the phone reads to size its undelivered window.

Also closes a narrower gap found while building this. The idempotency
wrapper resolved a replayed delegated response against a live grant
without considering which credential was presenting it, so a cached
response about a shared record could be returned to a token the handler
itself would refuse. No write was reachable that way; the response body
was.

Guards: T3 gains the mint site, T4's vocabulary gains the scope and
learns to read the requireRecordAuth form (without which this change
would open a way to widen a route that the closed vocabulary could not
see), and a new T5 freezes the set of delegable routes naming a scope.
The sharing surface guard's AST matcher reads only the first two
arguments, so it needed prose but no code.
2026-08-31 08:46:16 +00:00
Marc-André Bombeck 4308114c5b Merge pull request #879 from MBombeck/fix/lab-outlier-range-assertion
test(labs): assert the outlier's distance from the edge, not one string
2026-08-31 00:22:15 +02:00
Marc-André Bombeck 5e89a201f7 test(labs): assert the outlier's distance from the edge, not one string
Follow-up to #860, promised in review rather than asked of the
contributor. `not.toContain("left: 4%")" passes for a marker at 4.02%,
which is the same defect one rounding away, so the test now reads the
marker position and asserts it sits inside the padding at both ends.

EDGE_PADDING_PERCENT moves to module scope and is exported, so the
assertion uses the number the component clamps with instead of a copy
that can drift. It was being reallocated on every render besides.

Verified by mutation: dropping the outlier expansion puts the marker at
exactly 4 and turns this test red.
2026-08-31 00:06:50 +02:00
Marc-André Bombeck 7cca0a0710 Merge pull request #860 from TimonBed/feat/labs-ocr-openai-fix
rework of range bar ind lab list
2026-08-31 00:04:14 +02:00
Timon_bed f30a202a67 fix: keep lab outliers clear of range bar edges 2026-08-30 23:45:47 +02:00
Marc-André Bombeck b1d6765790 Merge pull request #875 from MBombeck/dependabot/npm_and_yarn/vitejs/plugin-react-6.1.0
chore(deps-dev): bump @vitejs/plugin-react from 6.0.4 to 6.1.0
2026-08-30 22:24:11 +02:00
Marc-André Bombeck 78921399fb Merge pull request #877 from MBombeck/dependabot/npm_and_yarn/radix-ui-1.6.7
chore(deps): bump radix-ui from 1.6.2 to 1.6.7
2026-08-30 22:07:32 +02:00
Marc-André Bombeck e7d40e22b0 Merge pull request #876 from MBombeck/dependabot/npm_and_yarn/lucide-react-1.34.0
chore(deps): bump lucide-react from 1.27.0 to 1.34.0
2026-08-30 22:05:58 +02:00
Marc-André Bombeck 68f33dad5f Merge pull request #874 from MBombeck/dependabot/npm_and_yarn/next/bundle-analyzer-16.3.3
chore(deps-dev): bump @next/bundle-analyzer from 16.3.1 to 16.3.3
2026-08-30 22:00:18 +02:00
Marc-André Bombeck 0be1663989 Merge pull request #873 from MBombeck/dependabot/npm_and_yarn/hookform/resolvers-5.9.1
chore(deps): bump @hookform/resolvers from 5.4.0 to 5.9.1
2026-08-30 21:58:27 +02:00
Marc-André Bombeck a4b2a1541d Merge pull request #872 from MBombeck/fix/pg-runtime-dependency
fix: declare pg as the runtime dependency it already was
2026-08-30 21:57:26 +02:00
dependabot[bot] c532f60c57 chore(deps): bump radix-ui from 1.6.2 to 1.6.7
Bumps [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) from 1.6.2 to 1.6.7.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/1.6.7/packages/react/radix-ui)

---
updated-dependencies:
- dependency-name: radix-ui
  dependency-version: 1.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:17:23 +00:00
dependabot[bot] 07b32617d2 chore(deps): bump lucide-react from 1.27.0 to 1.34.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.27.0 to 1.34.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.34.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:02:53 +00:00
dependabot[bot] 098cc9d5d4 chore(deps-dev): bump @vitejs/plugin-react from 6.0.4 to 6.1.0
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 6.0.4 to 6.1.0.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.0/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:02:16 +00:00
dependabot[bot] 5898742fa3 chore(deps-dev): bump @next/bundle-analyzer from 16.3.1 to 16.3.3
Bumps [@next/bundle-analyzer](https://github.com/vercel/next.js/tree/HEAD/packages/next-bundle-analyzer) from 16.3.1 to 16.3.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.3/packages/next-bundle-analyzer)

---
updated-dependencies:
- dependency-name: "@next/bundle-analyzer"
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:01:40 +00:00
dependabot[bot] 2e6e133409 chore(deps): bump @hookform/resolvers from 5.4.0 to 5.9.1
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.4.0 to 5.9.1.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.4.0...v5.9.1)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:00:56 +00:00
Marc-André Bombeck b4c18d131a Merge pull request #871 from MBombeck/fix/codeql-action-4.37.9
ci: move every codeql-action pin to v4.37.9 in one commit
2026-08-30 20:50:25 +02:00
Marc-André Bombeck 9f07a46633 fix: declare pg as the runtime dependency it already was
The Dockerfile asserts that the standalone image can resolve pg, because
pg-boss loads it through native Node resolution. That assertion has been
passing on a coincidence: pg sat in devDependencies, and the version our own
entry pinned happened to match the one @prisma/adapter-pg pulled in, so both
names resolved to a single copy in the store and Next's file tracer packed it
whole.

Bumping the adapter to 7.10.0 moves its transitive pg to 8.23.0 while ours
stayed at 8.22.0. The tracer packs only the copy the adapter reaches, the
top-level link points at the other one, and the image fails to build on both
architectures:

    Error: Cannot find module '/app/node_modules/pg/lib/index.js'

Move pg into dependencies at ^8.23.0. The lockfile now carries one pg version
instead of two, so the two resolutions cannot drift apart again on the next
adapter release. Carries the adapter bump from #865, which cannot build alone.
2026-08-30 20:49:46 +02:00
Marc-André Bombeck eb49580271 Merge pull request #868 from MBombeck/dependabot/npm_and_yarn/zod-openapi-6.0.1
chore(deps): bump zod-openapi from 6.0.0 to 6.0.1
2026-08-30 20:47:35 +02:00
Marc-André Bombeck 95a75cd9e0 Merge pull request #862 from MBombeck/dependabot/npm_and_yarn/knip-6.32.3
chore(deps-dev): bump knip from 6.32.2 to 6.32.3
2026-08-30 20:47:27 +02:00
Marc-André Bombeck 314f35b2ee ci: move every codeql-action pin to v4.37.9 in one commit
init, analyze and upload-sarif are the same release pinned in three
workflows. Dependabot opened them as three separate pull requests, so each
run mixed two versions and analyze rejected the config init had written:
"Loaded a configuration file for version 4.37.7, but running version
4.37.9". None of the three could go green alone, and the code-scanning
analysis stayed red while they waited on each other.

Bump all five references together and group the action in dependabot.yml so
the next release arrives as one pull request.
2026-08-30 20:20:31 +02:00
Marc-André Bombeck 58de37cf52 Merge pull request #869 from MBombeck/dependabot/npm_and_yarn/simplewebauthn/server-13.3.3
chore(deps): bump @simplewebauthn/server from 13.3.2 to 13.3.3
2026-08-30 20:19:17 +02:00
dependabot[bot] e87d44952c chore(deps-dev): bump knip from 6.32.2 to 6.32.3
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 6.32.2 to 6.32.3.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.32.3/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 6.32.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 18:16:04 +00:00
Marc-André Bombeck d67115b115 Merge pull request #867 from MBombeck/dependabot/npm_and_yarn/node-rs/argon2-2.1.0
chore(deps): bump @node-rs/argon2 from 2.0.2 to 2.1.0
2026-08-30 20:12:57 +02:00
Marc-André Bombeck 2326ca585b Merge pull request #861 from TimonBed/feat/openai-ocr-only
fix: fixes ocr for newer open ai models
2026-08-30 20:02:24 +02:00
Marc-André Bombeck fe0c248d5c Merge pull request #864 from MBombeck/release/v1.38.0
chore(release): v1.38.0 — the score works with the record you keep
v1.38.0
2026-08-30 18:50:21 +02:00
Timon_bed d1251634b3 prettier fix 2026-08-30 18:07:19 +02:00
Timon_bed 34e37fe850 prettier fix 2026-08-30 18:01:47 +02:00
dependabot[bot] a425cf58bf chore(deps): bump @simplewebauthn/server from 13.3.2 to 13.3.3
Bumps [@simplewebauthn/server](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/server) from 13.3.2 to 13.3.3.
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v13.3.3/packages/server)

---
updated-dependencies:
- dependency-name: "@simplewebauthn/server"
  dependency-version: 13.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 15:56:00 +00:00
dependabot[bot] 89b630060d chore(deps): bump zod-openapi from 6.0.0 to 6.0.1
Bumps [zod-openapi](https://github.com/samchungy/zod-openapi) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/samchungy/zod-openapi/releases)
- [Changelog](https://github.com/samchungy/zod-openapi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/samchungy/zod-openapi/compare/zod-openapi@6.0.0...zod-openapi@6.0.1)

---
updated-dependencies:
- dependency-name: zod-openapi
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 15:55:30 +00:00
dependabot[bot] b16f5f9947 chore(deps): bump @node-rs/argon2 from 2.0.2 to 2.1.0
Bumps [@node-rs/argon2](https://github.com/napi-rs/node-rs) from 2.0.2 to 2.1.0.
- [Release notes](https://github.com/napi-rs/node-rs/releases)
- [Commits](https://github.com/napi-rs/node-rs/compare/@node-rs/argon2@2.0.2...@node-rs/argon2@2.1.0)

---
updated-dependencies:
- dependency-name: "@node-rs/argon2"
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 15:55:00 +00:00
Marc-André Bombeck f71b0142a8 chore(release): v1.38.0 — the score works with the record you keep
Three areas of health become a recommendation rather than a gate. A one
or two area record now scores, the card says what the number rests on,
and refusal narrows to the one honest case: nothing scoreable yet.

Carries the composition notice, the reference-citation guard that keeps
invented targets out, @TimonBed's OAuth callback warning, and the fix
for a geometry check that had never measured in CI.
2026-08-30 17:54:02 +02:00
Marc-André Bombeck 28af7c13d3 Merge pull request #859 from MBombeck/feat/health-score-target-guards
feat: pin the rule that keeps invented targets dead
2026-08-30 17:50:57 +02:00
Timon_bed 6ab3b02935 fixes ocr for newer open ai models 2026-08-30 17:37:52 +02:00
Marc-André Bombeck b4ebd6ffce test: drop the fixture's unused module-path export
The constant was left over from an earlier shape of the guard, which
located the fixture by path before it imported it directly. Nothing reads
it now, and the dead-code gate says so.
2026-08-30 17:33:07 +02:00
Timon_bed de9a16b69d rework of range bar ind lab list 2026-08-30 17:24:46 +02:00
Marc-André Bombeck f29995a18c docs(api): correct the score descriptions the adaptive rule outdated
Three descriptions in the OpenAPI registry still describe the floor that
v1.38 removed, and one of them contradicts its own error-response
description two dozen lines below it.

- The score-config PATCH still said the write is refused below three
  distinct areas and one physical measurement. The route refuses the
  empty selection and nothing else; its 422 description already said so.
- The dashboard snapshot's healthScore still said the block is null until
  three eligible domains include a physiological one. It is null only
  when no pillar is readable at all.
- The pillar domain field still pointed at a three-distinct-domains rule
  that no longer exists, where what a client needs to hear is that domain
  count comes resolved on scoreBasis and must not be counted out of
  composition.

Regenerated docs/api/openapi.yaml.

The user-facing description of the score lives in the docs site's own
repository, not here; nothing under docs/ describes the composite.
2026-08-30 17:08:39 +02:00
Marc-André Bombeck 81be61b6d1 fix(seed): give the demo every area of health the score can span
The demo seeds no waist measurement and no wellbeing screener, so two of
the seven pillars have never had data and the score rests on three areas
instead of five. Adiposity and wellbeing render as empty rows on the card
that is meant to show the feature.

Both are seeded now: a fortnightly waist that walks 96 cm down to 88 cm
over the window (0.527 to 0.484 at the demo profile's height, so the
ratio crosses NICE 2022's threshold inside the seeded span rather than
sitting flat green), and six WHO-5 / PHQ-9 / GAD-7 administrations with
their item answers encrypted the way the check-in route writes them, each
with the COMPUTED projection measurement so the history view and the
trend chart agree. Totals sit in the calm bands and item 9 is answered
zero throughout, which also keeps the pillar clear of its
crisis-signposting arm.

Verified against a throwaway Postgres: before, five ok pillars over three
areas; after, seven over five, tier full.

The dates were never the problem. Every timestamp the seed writes is
already relative to the run, and the header says so. What ages is the
seeded DATA — the pillar windows keep moving after a seed, so four weeks
on, activity and sleep fall out of theirs and the score narrows to
whatever cardiometabolic data is still fresh. Since v1.38 that is a
labelled partial score rather than a blank card, but it is still not the
demo at its best, and the header now says that too.
2026-08-30 17:08:29 +02:00
Marc-André Bombeck d98d714ee7 feat(score): pin the rule that keeps invented targets dead
The score once graded weight against 22 x height squared, widened by 2 kg
either side: a band nobody set, no surface named, and the only producer
of that pillar's verdict. v1.34 removed it and the composite rebuild left
a pillar set where every scored band cites somebody. Nothing but review
has kept it that way since, and review is what let the original in.

So the rule is written down. PillarReference.kind becomes a runtime list
carrying the reason there is no fourth kind, reference.source is
documented as a citation a reader could go and look up, and a guard
beside the pillars checks all seven modules three ways.

The arm worth arguing about is the second. An invented target is a
continuous function of a profile field; a guideline band is a step
function into a published table. So the guard sweeps every pillar across
a matrix of heights and ages and requires the scored bands it emits to
land inside a small enumerated set transcribed from the guideline that
pillar cites. No finite enumeration can hold a per-person curve, which is
what makes the arm able to fail rather than merely able to pass. The
other two arms cover the citation string and freeze which pillar may read
which profile field, with the reason written beside it and stale entries
failing as loudly as missing ones.

The proof is in the file: the pre-v1.34 weight target is restored as a
fixture in today's shape and each arm is shown refusing it.
2026-08-30 17:08:16 +02:00
Marc-André Bombeck a2a743c9e1 Merge pull request #858 from MBombeck/feat/health-score-composition-notice
feat: the score says when a pillar stopped counting
2026-08-30 16:42:42 +02:00
Marc-André Bombeck 58701706db Merge pull request #857 from MBombeck/fix/geometry-guard-must-measure
fix: the geometry guard has never measured in CI
2026-08-30 16:42:27 +02:00
Marc-André Bombeck e4880dcdc1 feat: the score says when a pillar stopped counting
A pillar can leave the set behind the number without anybody choosing it:
a window rolls past its floor, a module goes off, a source goes quiet. The
delta already refuses to narrate that as a health event, which stops a
false "you dropped six points" and puts nothing in its place. The level
moves, the panel shows a different number, and the cause is nowhere on the
surface. Under the adaptive minimum this matters more rather than less,
because compositions are smaller now and sit closer to their floors.

The report carries `compositionNotice` when today's set differs from the
last local day the account has a stored score for. Optional and nullable,
the additive shape the score's wire has used six times, so a payload
cached before it existed stays valid. It names what left and what joined
as pillar ids; the surfaces already own the labels, and the reason a
departed pillar stopped counting comes from that pillar's own row in the
same report rather than from a second vocabulary.

Two changes deliberately do not raise it. A method move and a recipe
change each already have their own note, and telling someone twice about
one event teaches them to ignore both.

The key is `health-score:v<version>:composition:<hash of the sorted set>`
and rides the existing dismissed-item ledger, so a dismissal holds until
the set moves again rather than until tomorrow. The read excludes today on
purpose: the row for today is written by the same request that computed
the report, so comparing against the newest row would find today's own
composition on the second load of the same page and the note would vanish
between two views of it.

This gives `HealthScoreRecord` its first production reader. Since it
shipped it has been written on every scored read and consumed by nothing
but backup and restore, which is the write-only shape the contributor
notes call a liability and which nothing in the gate can notice, because
every check proves one end. The round trip is proved against real
Postgres: raise the note, wipe the table, restore it through the admin
route, get the same note back, and hold the dismissal across it.

Rendering is one muted line on the score card and a card of its own on the
settings page, beside the recipe note rather than inside it. Deliberately
not a push.

Four fake Prisma clients gain the new read. They are doubles of a client
the reader really does call now; leaving them short and guarding the call
site instead would have been a shim for a caller that does not exist.
2026-08-30 16:16:57 +02:00
Marc-André Bombeck 624432b3fe Merge pull request #855 from TimonBed/fix/withings-dev-redirect-uri
Shows warning for misconfigured callback urls
2026-08-30 15:59:17 +02:00
Marc-André Bombeck 88750e0048 fix: give the geometry check a browser and stop it skipping in CI
The hero-geometry check has never measured anything in CI. It launches
Chromium in a hook, catches the launch failure, and every case then
returns early — green. The quality job installs no browsers, so every
run of it since the file landed printed

  [hero geometry] SKIPPED — no Chromium build available (Error:
  browserType.launch: Executable doesn't exist at
  /home/runner/.cache/ms-playwright/chromium_headless_shell-1234/ch)

and passed. The file's own header warned about exactly this trap and
then fell into it.

Both ends move. The quality job installs the headless shell before the
unit suite — `--only-shell chromium`, because that is the build
`chromium.launch()` resolves to and the one the failing runs named, with
a cache on the lockfile hash so the download is paid once. And the check
now refuses to be optional: with CI set, a launch failure throws out of
the hook and fails the suite with the step to restore. Without CI it
still skips, through the test context so the skip and its reason land in
the report rather than in a console line nobody reads.

No panel number changed. The measurements the check reads back are the
same ones the reserve was set against: panels of 400 / 428 / 456 / 484
px for three to six rows, 420 px for three rows plus the basis line,
against a 448 px reserve.
2026-08-30 15:55:46 +02:00