Commit Graph
8078 Commits
Author SHA1 Message Date
Marc-André Bombeck 55ddad3d6a fix(oura): read a 401 as a scope gap only on the scope-gated collections
A 401 on any collection next to a success was recorded as a scope gap, so a
real auth failure on sleep, readiness or activity was hidden from the
failure ledger and shown as a reconnect hint. Only spo2, vo2max, cardio_age
and resilience sit behind a scope beyond daily; a 401 anywhere else keeps
its normal failure handling, and a stored note naming another collection
no longer skips the token refresh.
2026-10-14 19:47:05 +02:00
Marc-André Bombeck c7164be0d3 fix(withings): keep the plan re-probe date on a run that only narrowed
A run that skipped remembered types and met one more refusal restamped the
leg's checkedAt, pushing the weekly re-probe of the remembered types out on
every such run. Only a re-probe, or a leg with nothing remembered yet, now
starts the week again.
2026-10-14 19:46:03 +02:00
Marc-André Bombeck 6feaf6849a fix(withings): keep weight and blood pressure when every request is refused
A run where Withings refused every requested type stored all of them, core
types included, as outside the plan for a week; later runs then requested
nothing and moved the watermark on with no readings, losing the gap. A
refusal is now remembered only from a run in which another type was
accepted. A run that read nothing keeps the watermark and reports that it
did not land, and the watermark also holds while plan memory skips a core
type (weight, blood pressure, pulse, free body composition).
2026-10-14 19:45:38 +02:00
Marc-André Bombeck 8ce6a83da5 fix(withings): never remember a type the plan probe could not resolve
When the request budget ran out mid-split, every subset not yet tried was
stored as refused, so free types (temperature, SpO2, body composition) could
be skipped for a week and named as withheld by the plan. Those types are now
unverified: not remembered, asked for again on the next run, and the measure
watermark holds until every type was read or proven refused.
2026-10-14 19:44:05 +02:00
Marc-André Bombeck 787aafaee1 docs(changelog): list only the analytes the registry converts 2026-10-14 19:41:19 +02:00
Marc-André Bombeck 9d167069db chore(release): v1.42.3 2026-10-14 19:35:25 +02:00
Marc-André Bombeck f564c44753 chore(labs): drop the pending-consumer markers now that the readers have landed 2026-10-14 19:35:00 +02:00
Marc-André Bombeck 42b66b2228 Merge branch 'v1423/labs' into release/v1.42.3 2026-10-14 19:34:44 +02:00
Marc-André Bombeck 035ae37ee4 Merge branch 'v1423/integrations' into release/v1.42.3 2026-10-14 19:20:16 +02:00
Marc-André Bombeck ec57ae10b1 docs(labs): describe the registry listing by what uses it
Rewords a code comment that referred to the work by its planning step, and a
loop counter's comment, to say what the code does.
2026-10-14 19:20:01 +02:00
Marc-André Bombeck eabbe3e032 fix(labs): let the conversion badge in the scan review wrap
At 390 px the badge truncated, and the part it cut off was the value the
report printed. It now wraps inside the row like a sentence.
2026-10-14 19:19:44 +02:00
Marc-André Bombeck 65669e52d2 fix(withings): bind the plan-state timestamp instead of reading the database clock 2026-10-14 19:19:27 +02:00
Marc-André Bombeck 350fd1b767 fix(labs): name calcifediol as the source of the vitamin D factor
The note said the factor was taken from cholecalciferol (C27H44O2). Vitamin
D3 is C27H44O; C27H44O2 is 25-hydroxyvitamin D3 (calcifediol), the form the
assay reports and the one whose molar mass gives 1 ng/mL = 2.496 nmol/L. The
factor was right; the note now says what it rests on.
2026-10-14 19:19:15 +02:00
Marc-André Bombeck 07f09e1b61 fix(labs): store the catalog slug as a marker's analyte key
The matcher stored the registry entry's key, so LDL and HDL markers were
recorded as total-cholesterol. The key now names what the marker is (the
catalog slug, or a registry-only key for an analyte with no seed), and the
registry resolves ldl and hdl to the cholesterol entry for the conversion as
before. A key already written in the old form is rewritten by the same fill
step that brings a stale key in line with its name, the first time a reading
needs it. The OpenAPI descriptions say what the key is and that it follows
the marker's name and unit.
2026-10-14 19:18:49 +02:00
Marc-André Bombeck cd1449e63e fix(labs): keep a reading's conversion record unless its value actually changes
The single-reading edit cleared sourceValue and sourceUnit whenever the body
carried a value, and the web edit sheet always sends one, so a date or note
edit erased the "converted from" hint. It is now cleared only when the value
differs from the stored one. The sheet also opened a converted reading at its
stored precision (100.90191999999999); it now starts from the value as the lab
surfaces show it and leaves an untouched value out of the request, so the
stored full-precision number is not replaced by its rounded display.
2026-10-14 19:15:33 +02:00
Marc-André Bombeck 47a91d6922 fix(labs): never present a converted reading's printed range in the wrong unit
A converted reading's bounds move with its value, but the printed range text
stays in the unit the report used, and every reader preferred that text: the
list showed "Report range 3.9 to 5.5" beside 100.9 mg/dL, and the doctor
report and the FHIR referenceRange.text did the same. The text is still
stored verbatim. The range resolver now takes the reading's sourceUnit (as a
required field, so no reader can forget to select it) and, for a converted
reading, leaves the window's text empty so every surface states the converted
bounds in the marker's unit, and hands the printed text over separately with
its unit. The lab DTO carries it as printedReferenceText, and the note beside
a reading shows it as "as printed in mmol/L: ...".
2026-10-14 19:10:49 +02:00
Marc-André Bombeck 4793edb961 test(integrations): pin the Withings plan handling and the Oura scope gap; document the plan change 2026-10-14 19:08:22 +02:00
Marc-André Bombeck 9c5fa6d14a Merge branch 'v1423/fixes' into release/v1.42.3 2026-10-14 19:05:56 +02:00
Marc-André Bombeck 044f9008ba fix(oura): ask for the spo2 and heart_health scopes, explain a missing scope calmly
Oura's current authorization server enforces scopes per collection.
daily_spo2 needs spo2; vO2_max, daily_cardiovascular_age and
daily_resilience need heart_health. The authorize request now asks for
both.

A grant issued before keeps answering 401 on those four collections
while every other one succeeds. Such a 401 next to a success is now
recorded as a scope gap rather than a failure: it no longer counts
toward the reconnect threshold, it no longer forces a token refresh once
known, and the Oura card asks for one reconnect instead. The Withings
card names the readings the account's API plan withholds.
2026-10-14 19:01:02 +02:00
Marc-André Bombeck 8b18b1c9fe test(e2e): fail clearly on a medications POST body that is not JSON
The route stub parsed the body inside the un-awaited route callback, so
a malformed body threw there and the request hung. The parse error is
now recorded and the route is fulfilled; reading capture.postBody in
the spec throws with the parse error.
2026-10-14 18:59:33 +02:00
Marc-André Bombeck cc0313c955 fix(labs): re-derive a marker's analyte key when its name or unit changes
The key is a function of the marker's name and unit, but it was only ever
filled, never replaced, so a marker renamed from Glucose to Cholesterol kept
converting readings with the glucose factor. The marker edit now re-derives
it with the same match the creation paths use (clearing it when the new name
matches nothing), and the fill step a reading runs brings a stored key back in
line with the name, so a key written before this fix is corrected the first
time it matters. The scan review derives the key the same way as the commit.
2026-10-14 18:59:32 +02:00
Marc-André Bombeck 63882bf3ec fix(labs): give the draw date a full-width row
The lab form placed the draw date in the second column of a two-column
grid. DateTimeField sets its date and time halves side by side from sm
up, so the date half was squeezed until a date could only be set
through the calendar. The value and the date now stack in one column,
as in the measurement form, and the date can be typed again.

Refs #1214
2026-10-14 18:58:55 +02:00
Marc-André Bombeck cc507deafa fix(notifications): log every refused notification answer
A quick answer whose request never reached the route left no trace: a
redirect in front of the app turns ntfy's POST into a GET, which Next
answered 405 before any wide event, and the worker-only and demo-mode
refusals in the proxy wrote nothing either. The proxy now answers a
non-POST on the action path itself with 405 and Allow: POST, and every
refusal it makes there writes one warn line with a reason and the path
without its token. The route's rate-limit refusal carries a reason too.

The regression test sends the action ntfy publishes, in the shape ntfy
sends it, through the real proxy and route against Postgres.

Refs #1203
2026-10-14 18:57:06 +02:00
Marc-André Bombeck 9aa60fb2ff fix(illness): keep chronic conditions out of rest mode
A chronic ongoing condition has no end date, so counting it as rest mode
kept the account in it for good: Day N counted from an onset years back,
cadence nudges never resumed and every streak gap since the onset was
bridged. Only active episodes that are not CHRONIC_ONGOING (acute,
recurring, flare) now trigger rest mode, and Day N counts from the
earliest of those. The achievements streak freeze applies the same
filter to its own episode query.

Chronic conditions stay context: a coincident vital deviation is still
framed as explained by any active episode, and the Coach block and the
briefing still name them, with restMode false.

Refs #1216
2026-10-14 18:57:01 +02:00
Marc-André Bombeck 39b0cabb43 fix(withings): keep the sync inside the account's API plan
Withings split its public API into plans on 2026-10-12. A request naming
one meastype outside the free plan is refused whole with status 277, so
every measure sync stopped importing anything, and the 200..299 reauth
range parked the connection at error_reauth.

The measure, sleep-summary and ECG legs now drop the documented advanced
types on a 277, split the rest until the refused keys are isolated
(bounded), remember the refusal per connection and re-probe weekly. 277
is classified persistent and never enters the ledger; connections parked
over it are lifted by the fallback tick, and the held watermark lets the
next run read the whole gap.
2026-10-14 18:53:58 +02:00
BernhardBuckel 747e0964ad feat(labs): show a reading's conversion in the scan review and beside the value
Slice 3 of phase 3 of #1095. The scan review no longer treats every reading in
another unit as unwritable. The server tells it which analyte the matched marker
is (markerAnalyteKey on each extracted row); a row whose unit differs but can be
converted starts selected and carries a badge saying what Save will store and
what the report stated ("Saved as 100.9 mg/dL, converted from 5.6 mmol/L"). A
row that cannot be converted keeps the existing blocked badge and error. Save
still sends what the report printed; the server converts, as in slice 2.

The review's duplicate flag also recognises a re-scan of a reading that was
stored converted, by what the report printed, the same rule the commit applies.

A converted reading carries a quiet "converted from 5.6 mmol/L" on the lab list
(row and tile) and the history list (phone and table), so a number nobody typed
can always be traced back to what the report printed.

Strings in all seven locales.

Refs #1095
2026-10-11 20:41:08 +02:00
BernhardBuckel caa7a937fd feat(labs): convert a reading into its marker's unit when the pair is known
Slice 2 of phase 3 of #1095. A reading that stated a unit other than its
marker's was refused. When the marker's analyte is known and the registry has the
pair, the three write paths now convert it instead: POST /api/labs, the OCR
commit and the document commit.

The converted value is stored at full precision in the marker's unit, with what
the report printed in the existing source_value and source_unit columns. The
report's own reference window is in the printed unit, so it is converted with the
value; otherwise the verdict would compare a converted value with an unconverted
window. A pair the registry does not have is still refused as before (the 422
now says why), as is a marker whose analyte is unknown.

The marker's key is filled the first time a reading needs it. The same scan
committed again is still a duplicate: a converted row holds the number in the
marker's unit, so the duplicate check also matches on what the report printed.
Editing the value of a converted reading clears the conversion record, because
the number is then the person's own.

The lab result DTO and the OpenAPI schemas (labs and the OCR commit) carry
converted, sourceValue and sourceUnit.

Refs #1095
2026-10-11 20:28:00 +02:00
BernhardBuckel 08c69d4a5a test(labs): give the biomarker fixtures the new analyteKey field
BiomarkerDto gained analyteKey, and two component test fixtures typed as it
were missing the field, which the typecheck job rejects.
2026-10-11 20:16:31 +02:00
BernhardBuckel 52ea3b9c2f feat(labs): link a marker to its analyte for the unit conversion
Slice 1 of phase 3 of #1095. A Biomarker row keeps only its name, so nothing
said which analyte it was. This adds the explicit alias list in the catalog the
issue settled on, including the registry-only keys (urea, blood urea nitrogen,
uric acid, total calcium, total bilirubin), and sets biomarkers.analyte_key
(from migration 0385) from it.

matchAnalyteKey equals a marker's folded name against the localised names of the
catalog seeds the registry knows and against the alias list. It matches the
whole name only, so "Calcium (ionisiert)" is not "Calcium"; a name two analytes
claim matches neither; and the marker's own unit must be one the analyte reaches,
so a marker called "Glucose" in a unit the registry cannot convert stays
unlinked.

The key is set when a marker is created (the biomarker route, the mint used by
lab writes, the legacy backfill) and, for an existing marker, by
ensureAnalyteKey the first time a reading needs it. It never overwrites a key
that is set. There is no boot job for the existing markers: the conversion that
uses the key fills it on first use, and a sweep can be added later if wanted.

The key is returned on the biomarker API and in the OpenAPI schema.

Refs #1095
2026-10-11 20:04:43 +02:00
kilian c2be166f72 Merge remote-tracking branch 'origin/main' into feat/lab-analyte-key-fill 2026-10-11 19:48:56 +02:00
dependabot[bot] bc7f3bf209 chore(deps): bump the production-weekly group across 1 directory with 2 updates (#1207)
Bumps the production-weekly group with 2 updates in the / directory: [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) and [nodemailer](https://github.com/nodemailer/nodemailer).


Updates `lucide-react` from 1.52.0 to 1.53.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.53.0/packages/lucide-react)

Updates `nodemailer` from 10.0.15 to 10.0.16
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v10.0.15...v10.0.16)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-weekly
- dependency-name: nodemailer
  dependency-version: 10.0.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-weekly
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 17:17:03 +00:00
dependabot[bot] d748db0e10 chore(deps-dev): bump the development-weekly group with 3 updates (#1210)
Bumps the development-weekly group with 3 updates: [@playwright/test](https://github.com/microsoft/playwright), [lefthook](https://github.com/evilmartians/lefthook) and [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn).


Updates `@playwright/test` from 1.63.0 to 1.64.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.63.0...v1.64.0)

Updates `lefthook` from 2.1.17 to 2.2.1
- [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.17...v2.2.1)

Updates `shadcn` from 4.21.3 to 4.21.4
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.21.4/packages/shadcn)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.64.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-weekly
- dependency-name: lefthook
  dependency-version: 2.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-weekly
- dependency-name: shadcn
  dependency-version: 4.21.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-weekly
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 16:48:18 +00:00
dependabot[bot] 3f08e3a97b chore(deps): bump actions/upload-artifact from 7.0.1 to 7.0.2 (#1208)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.1 to 7.0.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 16:35:07 +00:00
dependabot[bot] 0fed7f02e6 chore(deps): bump the codeql-action group with 3 updates (#1206)
Bumps the codeql-action group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `github/codeql-action/analyze` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `github/codeql-action/upload-sarif` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 16:19:49 +00:00
Marc-André Bombeck c6db4501f1 Merge pull request #1205 from MBombeck/release/v1.42.2
Release v1.42.2
v1.42.2
2026-10-11 16:10:10 +02:00
Marc-André Bombeck b9471c3a62 docs(changelog): describe the fitted timeline window and the ended-course rule 2026-10-11 15:35:04 +02:00
Marc-André Bombeck e833bd0949 Merge branch 'v1422/review2' into release/v1.42.2 2026-10-11 15:33:51 +02:00
Marc-André Bombeck a54b47428e test(timeline): match the zoom each journey opened with
The day-strip journey opens /timeline?zoom=all, so closing the day
returns there, not to bare /timeline. An unreadable range now opens on
the default window, which for this record is three months.
2026-10-11 15:25:05 +02:00
Marc-André Bombeck ab104ab633 refactor(medications): spread the intake-tracking filter at each auto-take reader
The shared eligibility filter hid TRACKED_INTAKE_WHERE from the
structural guard that checks every schedule and intake reader honours
the switch. Each reader now spreads it itself.
2026-10-11 15:23:57 +02:00
Marc-André Bombeck 5bfdc9d04b fix(jobs): one bad medication never stops the auto-take pass
A row whose follow-up threw, or whose stored time zone the runtime
refuses, aborted the pass for every other person. Each medication now
runs on its own: a failure is named in a warning, counted, and the next
one runs; an unusable zone falls back to the default as in other jobs.
2026-10-11 15:19:08 +02:00
Marc-André Bombeck 88aec7edb6 fix(search): key the in-flight search by actor and record
Two tabs searching two different records (one's own and a shared one)
cancelled each other, because a new search aborted any earlier one by
the same actor. The slot is now held per actor and record.
2026-10-11 15:17:43 +02:00
Marc-André Bombeck 24f06884b3 fix(reminders): an ended course reads as not reminding
The worker expands no slot past endsOn, so it never reminds for a
course that has ended, but remindersActive still read true for it. The
reminder gate now includes the course end on the person's clock, so the
published value, the worker and the admin check make the same decision.
2026-10-11 15:16:54 +02:00
Marc-André Bombeck 1ba422b87c fix(backup): restart the auto-take floor at the restore
A portable file carries no removed doses, so a restored medication with
automatic recording on had the pass fill a dose the person deleted in
the days before the export. The restored floor is now the later of the
file's value and the restore.
2026-10-11 15:14:43 +02:00
Marc-André Bombeck 38900656a3 fix(audit): name automatic dose recording in medication audit rows
Switching autoMarkTaken on or off now appears in the medication.update
row with its previous value, and medication.create records whether the
medication starts with doses recorded automatically.
2026-10-11 15:11:27 +02:00
Marc-André Bombeck fe782493a1 fix(medications): a take bound to another slot never claims this one
The auto-take pass left a slot alone when any take fell inside its band,
so the morning dose pinned and taken at 19:00 also hid the 20:00 dose.
Only a take attributed to no other expected slot counts as nearby now.
2026-10-11 15:10:01 +02:00
Marc-André Bombeck dc86c42bef feat(timeline): fit the default window to the record
Without a zoom in the URL the window now follows the record: one that
starts inside the last three months opens on three months clamped to
its first entry (at least four weeks up to today), an older one with
entries there opens on three months, an older one without opens on
everything, and an empty one keeps the empty state. The choice is made
from the first answer before anything is drawn, so the page does not
jump between windows.
2026-10-11 15:08:57 +02:00
Marc-André Bombeck 0ff1340ff4 fix(medications): never backfill days an edit reopens under auto-take
A start moved earlier, an end moved later or removed, or a medication
made active again reopened past slots, and the auto-take pass filled up
to a week of them as taken. The course window writer and the
re-activation paths now move the auto-take floor to the moment of the
edit.
2026-10-11 15:07:06 +02:00
Marc-André Bombeck 23e0cc35af fix(timeline): open on everything when the default window is empty
Opened without a zoom, three months that hold nothing while the record
has older entries now show everything instead of a dead end, without
writing the zoom into the URL. A window named in the URL that is empty
offers "Show everything" when the record has entries before it. An
unreadable ?zoom= or range opens on the same default as no zoom.
2026-10-11 15:04:14 +02:00
Marc-André Bombeck a4cccc11ba fix(medications): keep an auto-taken rolling cadence moving
A rolling cadence yields only lastIntakeAt + N, so a slot the auto-take
pass never records (before the switch-on floor, past the catch-up
window, inside a pause, skipped or removed by the person) held it still
for good. Step the cadence forward by N past every such slot, keeping
its rhythm, and feed the stepped anchor to the card, dashboard and
schedule-tool next due so they show the slot the pass will record.
2026-10-11 15:00:37 +02:00
Marc-André Bombeck 0375c2adc6 test(reminders): drop the fixture fields the merge doubled 2026-10-11 14:49:43 +02:00