Commit Graph
2943 Commits
Author SHA1 Message Date
Marc-André Bombeck 1561fc67b0 Merge branch 'wave/v1221-a' into release/v1.22.1 2026-06-27 18:07:00 +02:00
Marc-André Bombeck 11fd081f8c Merge branch 'wave/v1221-e' into release/v1.22.1 2026-06-27 18:06:56 +02:00
Marc-André Bombeck e5ee8f3b3e fix(settings): keep the section nav fixed while only the content pane scrolls 2026-06-27 17:58:26 +02:00
Marc-André Bombeck 611de0f274 fix(settings): stop the mobile section selector animating a scroll from the top 2026-06-27 17:58:22 +02:00
Marc-André Bombeck 89b8c0e6d7 refactor(admin): rename the availability section to Modules 2026-06-27 17:57:45 +02:00
Marc-André Bombeck ba497f4ae8 fix(insights): keep charts from crushing the layout in landscape
Fixed-pixel chart bands ate roughly half the screen when a small phone
was rotated to landscape. Swap the brittle heights for viewport-relative
clamps so the chart envelope holds its previous size on desktop and
portrait but shrinks toward a floor on short landscape viewports. Covers
the overview trends-row slot and the mood distribution, weekday, and
time-of-day charts (with matching skeleton heights). Chart series and
styling are untouched; only the container heights flex.
2026-06-27 17:56:55 +02:00
Marc-André Bombeck fa161f7f12 fix(insights): drop the duplicate average-per-night header in sleep
The Sleep view led with an average-per-night headline card that repeated
the same figure and label already shown in the average-sleep tile within
the sleep-rhythm grid row below it. Remove the leading card and let the
tile carry the readout; the stage-distribution bar and duration trend
chart stay in place.
2026-06-27 17:56:42 +02:00
Marc-André Bombeck 3f19b13fab fix(nav): keep the More-menu tile labels fully visible on narrow screens
The long single-word labels (de "Benachrichtigungen") clipped their last
character at the tile edge on a 320 px phone. Trim the grid gutters and the
tile padding/gap to hand each label more horizontal room, and cap the bottom
sheet at max-w-md, so the label wraps cleanly onto a second line instead of
running off the tile. Tile height stays uniform.
2026-06-27 17:53:12 +02:00
Marc-André Bombeck 9cef089192 fix(nav): even out the profile menu row spacing
The Theme sub-trigger inherited the shared sub-trigger padding (py-1.5,
no min-height) while every sibling row carries min-h-11 py-2, so the
Theme row sat a few pixels shorter and the menu read as unevenly spaced.
Match the item height on this instance only, leaving other dropdowns'
sub-triggers untouched.
2026-06-27 17:53:05 +02:00
Marc-André Bombeck 6d990b769c Merge pull request #369 from MBombeck/release/v1.22.0
release/v1.22.0
v1.22.0
2026-06-27 16:48:52 +02:00
Marc-André Bombeck 480ff18cf2 chore(release): v1.22.0 2026-06-27 16:37:35 +02:00
Marc-André Bombeck 8f05b5a382 merge: v1.22 QA reconcile (design polish, knip, correctness + grounding lows) 2026-06-27 16:31:05 +02:00
Marc-André Bombeck b217535b62 chore(v1.22): clear the QA punch-list across Coach, insights, settings
Code-review and design follow-ups before the v1.22 cut. No behaviour change
beyond the items below.

Coach / insights:
- Coach SSE route: a non-provider error raised inside the open producer (e.g. a
  DB failure persisting the reply) now records on the wide event and forwards to
  GlitchTip instead of masquerading as a provider outage; it still surfaces an
  error frame since the stream is open.
- Lab ↔ outcome correlations: hedge faint-tier pairs ("a faint hint, if
  anything … too small to lean on") so the narrated confidence never outruns the
  effect, matching the daily engine.
- Reminder sweep: mint the plan-review reminder and clear the plan's reviewDate
  in one transaction so a half-applied write can't duplicate the reminder.
- Fold the two raw provider-fallback runners onto one shared chain walker.

Design / accessibility:
- Lift sub-44px tap targets to the 44px mobile floor (suggested-action card,
  Coach bubble affordances), brighten the resting timestamp clock for non-text
  contrast, and route the lab chart height through the shared constant.
- Replace ad-hoc arbitrary type sizes with the scale token and static size-11
  icon buttons with the named icon-lg variant.
- Settings: add the missing About card-header descriptions, normalise the
  low-stock numeric widths, and relax the notification-status rows from nested
  cards to a divided list.

Housekeeping:
- Drop three unused exported types (and the now-orphaned source const), re-stamp
  v1.21.5 comments to v1.22, and register the live-judge eval harness as a knip
  entry so the script-only module resolves.
2026-06-27 16:29:58 +02:00
Marc-André Bombeck ee4fa55c1e fix(db): target the users table in the v1.22 AI-settings migration
The AI-settings migration altered "User" but the table is "users"; deploy
would abort with relation "User" does not exist. Correct the identifier and
make both v1.22 column additions rerun-safe with IF NOT EXISTS.
2026-06-27 15:46:03 +02:00
Marc-André Bombeck b900732802 merge: v1.22 es/fr/it/pl translations 2026-06-27 15:34:38 +02:00
Marc-André Bombeck 14f22a8d36 i18n: translate v1.22 keys into es, fr, it, pl
The v1.22 waves added new keys to en.json and de.json only, leaving the
es/fr/it/pl bundles short and failing the locale-integrity guard. Translate
the 70 missing keys per locale across the Coach suggested-action prompts,
conversation seeds, read-aloud controls, derived-metric subtitles, freshness
timestamps, biomarker hide/restore controls, the per-metric insights link, and
the saved-reminders settings panel. ICU placeholders are preserved and the new
keys follow the en.json ordering within their parent objects.
2026-06-27 15:33:45 +02:00
Marc-André Bombeck 7ff14864b9 merge: AI settings (coach timeout, local streaming, OCR provider) into v1.22 2026-06-27 15:22:55 +02:00
Marc-André Bombeck 7fb0b26157 merge: W9 coach roadmap B5/C1/C3 + C2 substrate into v1.22 2026-06-27 15:22:07 +02:00
Marc-André Bombeck b91649d6de feat(ai): UI-configurable response timeout, local streaming, dedicated OCR provider
Add three per-user AI settings, all configured in the app (no new env vars),
behind one migration (0198):

- Response timeout (seconds). A per-user upstream timeout threaded onto the
  provider call. A local/self-hosted backend can take more than the legacy
  60 s default to load a model on the first request, which timed the Coach
  out before the first token. Defaults to a generous ~180 s when unset;
  validated to 10–600 s.

- True token streaming for the local OpenAI-compatible client. The client now
  POSTs with stream:true and parses the SSE delta chunks, emitting tokens as
  they arrive, with a per-idle-gap timeout instead of a whole-call ceiling and
  a graceful fallback to the buffered path when a server cannot stream. The
  Coach route runs the provider call inside the response stream behind a
  keepalive heartbeat so a reverse proxy no longer idle-drops the connection
  during model load or generation. Grounding, refusal, and sentinel-stripping
  still run on the complete reply before any token reaches the client.

- Dedicated document-scan (Lab-OCR) provider. Off by default — the OCR path
  uses the main provider chain. When enabled, lab-report processing resolves a
  separate provider/model/key, with a hint when the chosen model is not
  vision-capable. The new key column is AES-256-GCM at rest and registered in
  the encrypted-columns registry and the rotation script.

i18n: EN + DE translations for the new Settings → AI strings (other locales
carry interim copies to keep locale parity green).
2026-06-27 15:21:40 +02:00
Marc-André Bombeck 7ad32f7ab1 feat(coach): adherence storyline, changepoint engine, signal-trust, n-of-1 substrate
Add the medication-class to target-vital map and the adherence-to-vital
storyline so the Coach can surface, association-only, when an adherence dip
lines up with a drift in the vital that medication targets. Never causal,
never a dose-change suggestion; dose questions defer to the prescriber.

Add an in-repo CUSUM / binary-segmentation changepoint detector over DAY
rollups with a high firing bar (persistence plus within-segment-spread
magnitude) so the Coach can name a dated, sustained level shift instead of a
vague "lately", and stays silent when no break clears the bar.

Surface which source recovery is read from when two sources materially
diverge, with the honest reason, instead of silently averaging.

Lay the n-of-1 experiment substrate: a nullable encrypted outcome column on
coach_plans (migration 0197), the review_due / reviewed lifecycle statuses
wired into the encrypted-column registry and rotation script, and a daily
worker that reads the experiment result back in the user's own before/after
numbers — association-only, honest-null, no cheerlead on an adverse trend,
and routing a worsened vital to the clinician. The user-visible read-back
stays behind COACH_EXPERIMENT_VERDICT until its review cases clear.

Extend the outbound dose screen to catch experiment-shaped dose changes
(halve / skip / stop a medication to test it). Add evaluation cases and unit
tests for each new surface.
2026-06-27 15:21:04 +02:00
Marc-André Bombeck 3b74b108a1 merge: W6 insight/coach narrative quality + grounding into v1.22 2026-06-27 14:42:25 +02:00
Marc-André Bombeck 10111852a4 feat(insights): localized Coach seeds, freshness labels, paragraph rendering
Route the composer seed questions through i18n so a German user no longer
gets an English opener when tapping Ask the Coach from a metric page or an
insight card. Replace the relative timestamp on the briefing and per-metric
cards with a freshness label that shows the time only for today, drops it for
yesterday, and renders a locale date honouring the user's date-format
preference for older entries. Render the briefing paragraph, the per-metric
and score assessments, and the period narrative through the shared paragraph
helper so longer prose shows real paragraphs instead of one run-on block.

Adds the insights.coach.seed.* and insights.updated* keys (English + German).
2026-06-27 14:40:18 +02:00
Marc-André Bombeck 5674f10b2f feat(coach): quote the real BP range, address by name, render action cards
Feed the user's own usual blood-pressure range — the median plus or minus
MAD band, computed from the readings already in the snapshot — into the Coach
context with a clause to quote those bounds verbatim, so a band is never
invented from window means. Thread the stored first name into the prompt and
surface it sparingly on roughly one turn in three, never as a rote greeting.
Add the context-layer rule that names the relevant context before a
suggestion, a single-chart-token allowance that works on every provider, and
a per-turn opener hint.

Wire the already-shipped suggested-action SSE frame to render: the streaming
hook collects it and the message thread shows the action card, mirroring the
reminder-suggestion path. Extend the eval golden set with band-verbatim,
chart-token, and no-overclaim cases, and pin a red-team case proving the
chart-token syntax cannot smuggle a dose past the outbound screen.
2026-06-27 14:40:07 +02:00
Marc-André Bombeck b393527cac feat(insights): verdict-first briefing, richer score cards, grounded opener rotation
Replace the fixed three-beat assessment skeleton with a menu of moves the
model orders freely, and reframe the daily briefing from per-signal
enumeration to a verdict-first read that weaves the few salient signals into
one associative story. Score cards now name both the helped and the hurt
contributors, translate the band into a forward read for the day, and close
on one grounded nudge; the deterministic fallback gains the same band-meaning
sentence so provider-less and demo accounts read as well.

Add a hash-seeded opener-archetype rotation (deterministic, testable) so
consecutive cards and days no longer open the same way, a day-rotated sampling
seed for the period narrative in place of the fixed reference seed, and a
shared paragraph formatting contract. Add a score-number grounding gate and
extend the briefing grounding allow-set to the glucose, labs, preventive-care
and workout aggregate blocks so real figures can be cited without opening the
gate. Gate the daily score warm on an input fingerprint so a quiet day skips
the model call.
2026-06-27 14:39:54 +02:00
Marc-André Bombeck 1c2e36c255 ops: whitelist ALLOW_LOCAL_AI_PRIVATE_HOSTS in the bundled compose
The local AI provider's private-host opt-in is read from the environment but
was missing from the compose environment whitelist, so the value never reached
the container. Add it (and document it in the env example) so a self-hoster can
point the local provider at an internal Ollama / LM Studio host.
2026-06-27 14:05:03 +02:00
Marc-André Bombeck e2c6e46f2a merge: W7 coach memory, reminders, action cards into v1.22 2026-06-27 13:39:45 +02:00
Marc-André Bombeck 44b4e6cf06 test(coach): cover reminder capture, sweep, and action endpoints
Add unit coverage for the closed when-grammar and `---REMEMBER---` capture
(propose-free active write, recall-only note, malformed drop, per-user cap),
the closed action-card allowlist (off-list and bad-interval rejection, no
auto-apply), the daily sweep (due flip, plan-review mint, undecryptable
skip), and the routes (owner-scoped no-IDOR 404, field-by-field build, the
off-allowlist 422, and the module gate).
2026-06-27 13:38:36 +02:00
Marc-André Bombeck 5331528076 feat(coach): surface reminders in-app and add the action card
Add a "Your reminders" panel under Settings → Coach that lists the stored
reminders, highlights the due ones, and gives the user view, confirm,
done/dismiss, and delete control over their own memory. Light the Coach
launcher dot when a reminder is due, alongside the existing nudge signal.

Add the generic confirm-card component for the suggested-action frame: a
plain-text preview with a localized per-action heading and a single confirm
tap that posts the validated params to the build endpoint.

Translate the new copy in English and German.
2026-06-27 13:38:25 +02:00
Marc-André Bombeck 6f407388b4 feat(coach): add reminder and suggested-action endpoints
Add the management surface for the reminder memory: list, create, confirm,
done/dismiss, and soft-delete, every query scoped to the session owner with
the same no-IDOR posture as the plans routes (a cross-user or unknown id is
a 0-count no-op, never a leaked existence). The note is the only writable
field; timing is resolved server-side from the closed grammar.

Add the confirm endpoint behind the generalised action card. It switches on
a closed action-type allowlist and builds each entity field-by-field from a
server-resolved spec: a preventive-care reminder whose schedule comes from a
closed interval catalog, or a reminder note. It never auto-applies, and the
allowlist excludes any medication or clinical change.

Regenerate the OpenAPI contract for the new routes.
2026-06-27 13:38:15 +02:00
Marc-André Bombeck 27a8a59d67 feat(coach): capture and resurface "remind me" reminders
Capture the casual "remember this / remind me about X" utterance the Coach
used to drop. The model emits a provider-neutral `---REMEMBER---` sentinel
that the chat route strips inline on EVERY turn — so a short conversation,
where the request usually happens, no longer waits for the long-chat memory
worker. The note is the user's own explicit request, so it is stored
silently as active; a validated closed grammar (ISO date, relative +Nd/+Nw,
or a context cue) sets the trigger, and a malformed block is dropped.

Surface the stored reminders two ways. A snapshot recall sub-block injects
near, overdue, and already-surfaced reminders so the Coach references them
unprompted next turn. A daily sweep flips overdue reminders to due and
mints a one-off review reminder from a plan whose review date has passed,
finally activating the plan review-date column. In-app surfacing only here;
push delivery follows later.

Also generalise the propose-then-confirm action card: a closed
`---SUGGEST-ACTION---` allowlist (a preventive-care item or a reminder note)
rides an additive SSE frame. Nothing is created until the user confirms.
2026-06-27 13:38:04 +02:00
Marc-André Bombeck 9b61769a32 feat(coach): add the CoachReminder episodic-memory model
Introduce a third Coach memory type alongside facts (durable traits) and
plans (standing if-then habits): a time-anchored "bring this back later"
reminder with a due moment and a done state. The free-text note is
AES-256-GCM ciphertext via the shared Bytes codec; metric, status, the
trigger fields and the dates stay plain so the sweep and the injection
picker rank and filter without a per-row decrypt.

Migration 0196 is purely additive (status and trigger_kind are app-side
closed enums, not DB enums). Register the new note_encrypted column in the
encrypted-columns registry and the rotation script so a key rotation can
never silently skip it.
2026-06-27 13:37:50 +02:00
Marc-André Bombeck fa82109ca7 merge: W8 cross-signal integration + early detection into v1.22 2026-06-27 13:35:55 +02:00
Marc-André Bombeck 0aaaaac081 feat(insights): surface early-detection and lab correlations to the correlations route and coach
Thread the rolling early-detection pass and the labs -> outcome pass through the
correlations route and the coach correlations reader, both of which already
build the same daily series. The route response gains an optional `emerging`
block (recent-window pairs not yet established, flagged provisional) and a
`labCorrelations` block; the coach reader gains matching `emerging` and
`labDrivers` fields. Regenerate the OpenAPI contract for the new response shape.
2026-06-27 13:34:43 +02:00
Marc-André Bombeck 2f22d66ef7 feat(insights): wire labs, preventive care, workouts and glucose into the briefing
The daily briefing's always-on feature payload carried only the core vitals,
mood, sleep, steps and medication compliance, leaving four captured domains
invisible to it. Add four blocks to the aggregated features:

- Labs: recent flagged biomarkers (out-of-range or trending vs the prior
  reading). Retired markers are excluded; qualitative results stay neutral with
  no fabricated verdict.
- Preventive care: due and overdue items off the server-authoritative next-due
  date, so an overdue screening can prompt an early nudge.
- Workouts: a provider-agnostic count, duration and distance aggregate over the
  trailing windows (the briefing previously saw activity only as step counts).
- Glucose: the present-and-trend block the other vitals carry, where glucose
  previously reached the briefing only as a single signal-of-the-day row.

The three extra reads are scoped to the wide briefing window; the tight
snapshot window that already carries its own context skips them. Every block is
omitted when its domain is empty rather than emitting zeros, and user-entered
labels are sanitised before they reach the prompt.
2026-06-27 13:34:43 +02:00
Marc-André Bombeck 138211b46a feat(insights): early-detection window and new correlation linkages
Add a rolling 14-21 day early-detection pass to the FDR correlation engine
so an emerging shift (illness pulling HRV and sleep down together) surfaces
before the 180-day retrospective scan accrues the n>=20 it needs. The recent
pass re-runs the same engine over the trailing window with a lower paired-day
floor, a tighter Benjamini-Hochberg target, and a no-faint effect bar, then
drops any pair the retrospective scan already established so a pattern is never
reported as both established and emerging.

Extend the curated behaviour x outcome matrix with three clinically meaningful
links: medication adherence -> next-day blood pressure (systolic and diastolic,
the latter previously never a candidate channel), sleep as a behaviour
(poor sleep -> next-day mood / HRV), and a separate point-vs-window labs ->
outcome pass (each draw against the contemporaneous outcome mean) since sparse
lab draws cannot meet a daily-lag floor. Every pass keeps FDR control, the
effect-size floor, and shrinkage; thin data degrades to absent, never a
fabricated link. All associations stay descriptive, never causal.
2026-06-27 13:34:30 +02:00
Marc-André Bombeck 5594d67900 merge: W5 coach panel UI (paragraphs, TTS, charts, deep-link) into v1.22 2026-06-27 13:33:14 +02:00
Marc-André Bombeck 9ef79b2357 fix(coach): deep-link the proactive nudge to its conversation
The bottom-right launcher opened a blank new chat on an unread proactive
message instead of the thread the Coach wrote. Return the owning
conversation id from the nudge-status endpoint, have the launcher open
/coach?c=<id> on the unread path, and stamp the same deep link on the push
metadata so the notification lands in the exact conversation rather than
relying on a most-recent heuristic.
2026-06-27 13:31:17 +02:00
Marc-André Bombeck 914fa8d36e feat(coach): add inline charts, read-aloud and message timestamps
Activate the dormant chart-token render path for the Coach: an allowlisted
metric:<TYPE> token in a settled assistant turn mounts the self-fetching
HealthChart for the user's own series, gated on the turn's grounded
provenance so a metric the reply never saw never renders and no data is
fabricated. Add a read-aloud toggle backed by the browser Speech Synthesis
API (no dependency, no egress) and a hover/tap timestamp tooltip on each
message bubble. Render message text through the shared paragraph helper.
2026-06-27 13:31:11 +02:00
Marc-André Bombeck 16ab522a06 feat(coach): render AI prose as real paragraphs with clickable Learn links
Add a shared, pure ProseBlocks helper that splits trusted plain text on
blank lines into <p> blocks and single newlines into <br>, with no markdown
library and no dangerouslySetInnerHTML. A catalog-whitelisted linkifier turns
grounded /learn/<slug> references into safe anchors via learn-more-link;
unknown slugs stay plain text. Rewrite StreamedProse to be streaming-safe:
completed paragraphs settle while only the growing tail animates word by
word.
2026-06-27 13:31:03 +02:00
Marc-André Bombeck 7d3c528157 merge: W1 insights surface (i-icon, subtitle, steps, type-click) into v1.22 2026-06-27 13:05:27 +02:00
Marc-André Bombeck 8a7a2bc2e8 Link a measurement type to its insights sub-page
Clicking a type on the measurements list now drills into that metric's
insights sub-page when one exists, falling back to the existing
?type= filtered list when it does not. A reverse type-to-slug map backs
the navigation, preferring the dedicated single-metric page so an
ambiguous type resolves correctly (PULSE to pulse, not blood-pressure;
WEIGHT to weight, not bmi). No API change.
2026-06-27 13:03:09 +02:00
Marc-André Bombeck 7861c9b642 Show the steps pill for layouts that predate its tile id
resolveInsightsLayout merged any default tile a saved layout did not
enumerate as hidden, so a late-added tile such as steps (introduced in
v1.12) was force-hidden on every read of an existing or iOS-synced
layout and its nav pill never appeared, even with step data present.
Appended tiles now inherit the default visibility instead, matching the
fresh-account default; the per-tile data floor still keeps an empty
metric out of the nav.
2026-06-27 13:02:48 +02:00
Marc-André Bombeck 07b65ade88 Add a subtitle slot to the insights overview section heading
Detail pages already render a one-line description under their heading;
the overview section headings had no slot for one. Add an optional
subtitle to SectionHeading, rendered in the same muted typography, and
wire it into the obvious top-level sections: Trends (folding its existing
right-aligned caption into the slot), Your health scores, and Your
vitals.
2026-06-27 13:02:41 +02:00
Marc-André Bombeck f5e9d875cc Remove the trailing info glyph across the insights surface
The metric sub-page heading carried a trailing info "i" that opened a
reference-guidance tooltip on every routed insights page, and four card
satellites repeated the pattern (mood statistical caveat, health-score
delta read, period-narrative provenance, derived-score provenance). Drop
the glyph and its disclosure everywhere. Where the disclosure held the
only copy of a descriptive line, that line now reads inline as a muted
caption so the information stays visible instead of hiding behind a hover
affordance.
2026-06-27 13:02:05 +02:00
Marc-André Bombeck 37bfd7bb4e merge: W3 labs biomarker list + hidden flag into v1.22 2026-06-27 12:49:43 +02:00
Marc-André Bombeck 0bde6545b6 fix(labs): shared primitives in lab views, drop value Learn link
Replace the hand-rolled native scan dropzone button with the shared
Button primitive, swap the inline-style chart height for a height
class, and remove the Learn link from the biomarker value view where it
does not belong.
2026-06-27 12:49:00 +02:00
Marc-André Bombeck 3ad59cde98 feat(labs): compact editable biomarker list with hide
Rebuild the biomarker manager as compact rows (name + unit inline,
reference range / panel on a muted sub-line) with a hide toggle, an
edit button, and a delete, replacing the one-card-per-marker layout.
Hidden markers collect in a separate restorable section. Filter hidden
markers out of the lab-entry picker, keeping the currently selected one
visible so an existing reading still resolves its label.

Touch targets meet the 44px mobile floor (size-11 sm:size-9).
2026-06-27 12:48:54 +02:00
Marc-André Bombeck e097e57c84 feat(labs): add a hidden flag to the biomarker catalog
Add a `hidden` boolean to the Biomarker model (migration
0195_biomarker_hidden) so a marker the user no longer needs can be
soft-removed instead of deleted: it leaves the active catalog and the
lab-entry pickers but keeps its readings and canonical unit/range.

Thread `hidden` through the DTO, the update schema, the PUT handler
(field-by-field), and the list endpoint (visible markers first). Regen
the OpenAPI contract.
2026-06-27 12:48:48 +02:00
Marc-André Bombeck 8ca5974ad6 merge: W4 vorsorge->checkups rename into v1.22 2026-06-27 12:48:34 +02:00
Marc-André Bombeck a9892e9586 refactor(checkups): rename the preventive-care slug to /checkups
Move the preventive-care surface from the German-derived slug /vorsorge to
the English /checkups and make the feature read consistently across locales.
The route directory, the nav entry, the dashboard card link, and the
onboarding tour step now point at /checkups. A permanent redirect keeps old
bookmarks, the PWA's cached navigation, and push deep-links resolving.

Display label is now "Checkups" in English and the equivalent term in
Spanish (Revisiones), French (Examens), Italian (Controlli), and Polish
(Badania); German keeps "Vorsorge". The internal id stays "vorsorge" so
saved layouts, module-order preferences, and tour state keep resolving
without a data migration.
2026-06-27 12:48:04 +02:00