diff --git a/.env.production.example b/.env.production.example
index ef4445e8b..c5f56207c 100644
--- a/.env.production.example
+++ b/.env.production.example
@@ -450,6 +450,19 @@ API_TOKEN_HMAC_KEY=""
# for the heaviest legitimate read (the live-aggregate analytics fallback on a
# large account). Set "0" to disable it and restore unbounded behaviour.
# DATABASE_STATEMENT_TIMEOUT_MS="60000"
+#
+# Postgres `work_mem` applied to every app session, as a Postgres size string.
+# Default 16MB. Large sorts and aggregates that exceed it spill to disk; each
+# sort step of each pooled connection may use this much memory, so raise it
+# with the pool size in mind.
+# DATABASE_WORK_MEM="16MB"
+#
+# Both settings above reach Postgres as the libpq `options` startup
+# parameter. PgBouncer refuses that parameter ("unsupported startup
+# parameter: options") unless it is listed in its ignore_startup_parameters.
+# Behind such a pooler, set this to 1, true or yes to send no session
+# settings at all; the pooler's or the database's own defaults then apply.
+# DATABASE_SESSION_OPTIONS_DISABLED="1"
# -----------------------------------------------------------------------------
@@ -499,6 +512,12 @@ API_TOKEN_HMAC_KEY=""
# TRUST_PROXY_HOPS="1"
# TRUST_CF_CONNECTING_IP="1"
+# Memory cap of the app container (read by docker-compose.yml, not by the app).
+# Node sizes its heap from it: 1536m (the default) gives 792 MB, 1g only 524 MB,
+# which a long-used account fills in normal operation. Lower it on a host with
+# 2 GB or less, raise it on a busy instance.
+# APP_MEMORY_LIMIT="1536m"
+
# -----------------------------------------------------------------------------
# Outbound requests HealthLog makes on its own -- optional switches
# -----------------------------------------------------------------------------
@@ -586,6 +605,16 @@ API_TOKEN_HMAC_KEY=""
# force the public-host guard, so an internal instance is not blocked).
# OPENMETEO_BASE_URL="https://archive-api.open-meteo.com"
# OPENMETEO_GEOCODING_URL="https://geocoding-api.open-meteo.com"
+#
+# Air quality, pollen and UV come from the Open-Meteo air-quality API, which
+# serves the Copernicus Atmosphere Monitoring Service (CAMS) models. CAMS data
+# is free to use with attribution, which the app shows next to the values.
+# Like the URLs above this one is optional and can point at a self-hosted
+# Open-Meteo instance. Set ENVIRONMENT_AIR_QUALITY_DISABLED to 1, true or yes
+# to stop the air-quality requests for the whole instance; weather and
+# daylight keep working, and no user can turn air quality back on.
+# OPENMETEO_AIR_QUALITY_URL="https://air-quality-api.open-meteo.com"
+# ENVIRONMENT_AIR_QUALITY_DISABLED="1"
# ── Native canvas renderer (document thumbnails, scanned-PDF rasterization) ──
# The bundled renderer's x64 build uses AVX2 CPU instructions. On x86-64 hosts
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 7d8e7f98f..68054df07 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,6 +10,43 @@ updates:
applies-to: security-updates
update-types:
- "patch"
+ # One weekly pull request per dependency kind instead of a trickle of
+ # single bumps: patch and minor updates travel together and pass the
+ # same checks once. Majors stay single so each gets its own review.
+ production-weekly:
+ dependency-type: "production"
+ update-types:
+ - "patch"
+ - "minor"
+ development-weekly:
+ dependency-type: "development"
+ update-types:
+ - "patch"
+ - "minor"
+ ignore:
+ # zod 4.5 made `z.iso.datetime()` require seconds, so a request carrying
+ # `2026-01-01T06:15Z` that passes today would be refused with a 422 on
+ # every route that validates a timestamp, and 4.6 rewrites nullable and
+ # primitive unions in the generated JSON Schema from `anyOf` to `type`
+ # arrays, which reshapes the OpenAPI document and every MCP tool schema.
+ # Both are contract changes for the web client, the iOS app and MCP
+ # callers, not a routine bump, so the move past 4.4 gets its own change
+ # with the timestamp producers audited. Patch releases on 4.4 still
+ # arrive.
+ - dependency-name: "zod"
+ versions: [">=4.5.0"]
+ # Next 16.4.0 breaks URL state after history traversal: with a sheet
+ # opened through `?doc=`, Back leaves it open, and `[` / `]` sometimes
+ # do not move `?day=`. Reproduced in three end-to-end journeys (3 of 40
+ # repeated runs on 16.4.0, 0 of 80 on 16.3.8 with everything else
+ # equal). 16.3.8 carries every published security fix, so the line
+ # waits for a 16.4 patch; the two packages that move with it wait too.
+ - dependency-name: "next"
+ versions: ["16.4.0"]
+ - dependency-name: "eslint-config-next"
+ versions: ["16.4.0"]
+ - dependency-name: "@next/bundle-analyzer"
+ versions: ["16.4.0"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
diff --git a/.github/workflows/coach-eval.yml b/.github/workflows/coach-eval.yml
index 8b37a5659..e7e4eb6e5 100644
--- a/.github/workflows/coach-eval.yml
+++ b/.github/workflows/coach-eval.yml
@@ -26,7 +26,7 @@ permissions:
jobs:
coach-eval:
name: Coach eval — deterministic floor (always) + live judge (opt-in)
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 15
# Map the opt-in secret to a job-level env so the live-judge steps can gate
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index a14ca324b..5b5fd09a6 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -36,7 +36,7 @@ concurrency:
jobs:
analyze:
name: Analyse javascript-typescript
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
@@ -60,7 +60,7 @@ jobs:
# minutes and can produce nothing actionable, because nobody edits
# it and the fix for anything found there is `pnpm prisma generate`.
- name: Initialise CodeQL
- uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
+ uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
languages: javascript-typescript
build-mode: none
@@ -75,6 +75,6 @@ jobs:
- public
- name: Analyse
- uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
+ uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
category: "/language:javascript-typescript"
diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
index 0323be4c6..99f71e007 100644
--- a/.github/workflows/dependabot-auto-merge.yml
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -11,7 +11,7 @@ permissions:
jobs:
auto-merge:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.actor == 'dependabot[bot]'
permissions:
contents: write
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index a34a34c9d..d488cdf37 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -39,7 +39,7 @@ concurrency:
jobs:
release-policy:
name: Validate release policy
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: read
@@ -121,7 +121,7 @@ jobs:
matrix:
include:
- platform: linux/amd64
- runner: ubuntu-latest
+ runner: ubuntu-24.04
cache_scope: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
@@ -296,7 +296,7 @@ jobs:
- name: Upload digest artifact
if: github.event_name != 'pull_request'
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: digest-${{ matrix.cache_scope }}
path: /tmp/digests/*
@@ -313,7 +313,7 @@ jobs:
# the registry instead of on one cross-emulated runner.
merge:
name: Merge multi-arch manifest
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.event_name != 'pull_request'
needs: [build]
timeout-minutes: 10
@@ -449,7 +449,7 @@ jobs:
promote:
name: Promote verified image to Coolify
needs: [merge, verify-published-image]
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 111624651..531dbc66c 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -24,7 +24,7 @@ jobs:
# mobile route sweep landed, and a slow runner tipped them over; three
# shards with a 30-minute cap leave room for both.
shard:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 30
strategy:
fail-fast: false
@@ -106,7 +106,7 @@ jobs:
- name: Upload Playwright HTML report
if: always()
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: playwright-report-shard-${{ matrix.shard }}
path: playwright-report/
@@ -117,7 +117,7 @@ jobs:
# job below. Only uploaded when the shard ran to completion, so a missing
# artifact there means a shard that did not finish, not an empty suite.
- name: Upload Playwright JSON report
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: playwright-json-shard-${{ matrix.shard }}
path: playwright-json/results.json
@@ -128,7 +128,7 @@ jobs:
# that name and answers for both shards: it fails when any shard failed or
# was cancelled, so a half-green matrix cannot satisfy the gate.
e2e:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
needs: shard
if: always()
timeout-minutes: 5
diff --git a/.github/workflows/env-check.yml b/.github/workflows/env-check.yml
index 11ec45835..a86c42a87 100644
--- a/.github/workflows/env-check.yml
+++ b/.github/workflows/env-check.yml
@@ -21,7 +21,7 @@ permissions:
jobs:
env-check:
name: pnpm check-env against .env.production.example
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Checkout
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index b75a9d306..9b683d0e0 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -22,7 +22,7 @@ jobs:
# job timeout only needs to catch a container that never boots. Each shard
# boots its own Postgres testcontainer.
shard:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 40
strategy:
fail-fast: false
@@ -67,7 +67,7 @@ jobs:
# carries that name and answers for every shard: it fails when any shard
# failed or was cancelled, so a half-green matrix cannot satisfy the gate.
integration:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
needs: shard
if: always()
timeout-minutes: 5
diff --git a/.github/workflows/knip.yml b/.github/workflows/knip.yml
index 94d7700e9..8d0e67a25 100644
--- a/.github/workflows/knip.yml
+++ b/.github/workflows/knip.yml
@@ -12,7 +12,7 @@ permissions:
jobs:
knip:
name: Dead-code gate
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
diff --git a/.github/workflows/no-todo-markers.yml b/.github/workflows/no-todo-markers.yml
index 501c2b7e6..d53f04fca 100644
--- a/.github/workflows/no-todo-markers.yml
+++ b/.github/workflows/no-todo-markers.yml
@@ -16,7 +16,7 @@ jobs:
# future contributor reintroduces the pattern.
grep:
name: Forbidden marker grep
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
diff --git a/.github/workflows/post-publish-verify.yml b/.github/workflows/post-publish-verify.yml
index 330be15b5..4f4bdf70f 100644
--- a/.github/workflows/post-publish-verify.yml
+++ b/.github/workflows/post-publish-verify.yml
@@ -45,7 +45,7 @@ permissions:
jobs:
verify:
name: Pull and smoke-test exact published image
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 10
env:
IMAGE_REF: ${{ inputs.image_ref }}
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 9fe3950d5..02f6bd497 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -25,7 +25,7 @@ permissions: read-all
jobs:
analysis:
name: Score supply-chain practices
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
# Write the findings into the repository's code-scanning alerts.
@@ -54,7 +54,7 @@ jobs:
publish_results: true
- name: Upload results
- uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
+ uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
sarif_file: scorecard-results.sarif
category: scorecard
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index 5037c89d7..4413b31a8 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -17,7 +17,7 @@ permissions:
jobs:
quality:
name: Lint, Typecheck & Test
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.event_name != 'schedule'
steps:
@@ -100,7 +100,7 @@ jobs:
# published image daily.
dependency-audit:
name: Dependency Audit
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
# Runs on pull requests too, but advisory: an advisory published against a
# dependency the branch did not touch must not block the branch. The push
# run on main and the daily schedule stay blocking, and a tag is only ever
@@ -130,7 +130,7 @@ jobs:
secret-scan:
name: Secret Scanning
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.event_name != 'schedule'
steps:
@@ -145,7 +145,7 @@ jobs:
docker-security:
name: Container Security
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
diff --git a/.github/workflows/synthetic-journey.yml b/.github/workflows/synthetic-journey.yml
index 2d096f9b0..268154596 100644
--- a/.github/workflows/synthetic-journey.yml
+++ b/.github/workflows/synthetic-journey.yml
@@ -45,7 +45,7 @@ permissions:
jobs:
journey:
name: Sign in, write a reading, read it back
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml
index 289b57daf..96fec397e 100644
--- a/.github/workflows/trivy.yml
+++ b/.github/workflows/trivy.yml
@@ -30,7 +30,7 @@ concurrency:
jobs:
filesystem:
name: Lockfile (including dev dependencies)
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
@@ -88,14 +88,14 @@ jobs:
skip-dirs: src/generated
- name: Upload results
- uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
+ uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
sarif_file: trivy-filesystem.sarif
category: trivy-filesystem
published-image:
name: Published image
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
@@ -145,7 +145,7 @@ jobs:
exit-code: "0"
- name: Upload results
- uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
+ uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
sarif_file: trivy-published-image.sarif
category: trivy-published-image
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53cb335cb..84c2a523f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,765 @@
# Changelog
+## [1.42.2] — 2026-10-11
+
+Actionable notifications, record search in the command palette, automatic
+intake recording, and fixes for Oura, non-UTC databases, documents and
+Google Health backfills.
+
+### Added
+
+- **Notification tap targets (#1203).** One map resolves a target path per
+ event type; ntfy carries it as `Click`, Telegram as an "Open in HealthLog"
+ button on public https (text link on plain http), web push as `url`,
+ webhook and Gotify as a URL field, email as a link line, APNs as `url`.
+ No target for guardian deliveries, discreet cycle mode or the silent sync
+ push. ntfy titles and action labels outside ASCII are sent RFC 2047
+ encoded.
+- **Notification content preference.** `notificationPrefs.content.detail`
+ (`generic` default, `detailed`) on `PATCH /api/auth/me/notification-prefs`;
+ with `detailed` the morning briefing title carries the score.
+- **Mood quick answers on ntfy.** Three actions post to
+ `POST /api/notifications/action/[token]`: an HMAC token bound to user and
+ answer, valid six hours, single use per reminder, rate limited before
+ verification, recorded once in the idempotency table so a purged entry
+ cannot be answered again. Mood entries record source `NOTIFICATION`. A
+ reminder tapped while acting in another record opens the mood page
+ without the entry sheet. ntfy messages are published as JSON.
+- **Record search (#1117).** `POST /api/search` (query in the body, so it
+ never appears in a URL or proxy log; read-level access, one search at a
+ time per account, 20 a minute) searches visits and
+ procedures, conditions, documents, vaccinations, lab results, medications,
+ symptoms, workouts and notes, decrypting encrypted text in bounded batches
+ with a shared time budget, and reports `truncated` when a bound stopped a
+ source. Matching is case-, accent- and umlaut-spelling-blind and shared
+ with the MCP `search` tool. The palette lists hits under "In your records";
+ each hit deep-links to its entry. Migration 0397 adds a partial index on
+ live readings that carry a note.
+- **Open in new tab (#1202)** for PDF and image originals in the document
+ vault.
+- **Record doses automatically.** `Medication.autoMarkTaken` (with
+ `autoMarkTakenSince`, migration 0395): a job every 15 minutes records each
+ due dose as taken from the moment the option is switched on, through the
+ schedule engine, never over a dose taken, skipped or deleted by hand, with
+ the same stock and rollup follow-ups as a manual take. Intakes carry
+ `autoRecorded`; no reminders are sent for such a medication. List and
+ detail responses carry the resolved `remindersActive`
+ (false also once a course has ended). Rolling cadences step past slots the
+ job will not record, and the card shows the same next slot.
+
+### Fixed
+
+- **Oura token exchange (#1204).** Codes are exchanged at
+ `moi.ouraring.com/oauth/v2/ext/oauth-token`; a code issued by that server is
+ never sent to the legacy endpoint. Refresh falls back to
+ `api.ouraring.com/oauth/token` once. The callback validates `iss` (RFC 9207).
+- **Non-UTC database time zone (#1200).** Every connection (Prisma, pg-boss,
+ lock and maintenance clients) runs with `TimeZone=UTC`, with a `SET`
+ fallback when session options are disabled; raw SQL reads the clock as
+ `NOW() AT TIME ZONE 'UTC'`; boot warns when the session zone is not UTC.
+- **Google Health backfill (#1194).** A slice that saved progress enqueues
+ the next slice and ends without `job.run.failed`; a slice without saved
+ progress still fails, and the chain stops after collections + 2 slices.
+- **Admin AI spend card** sums today's spend on each account's local day.
+- **Daily AI budget** is keyed on the account's local day.
+- **Day view:** Escape inside the date picker no longer closes the day.
+
+### Changed
+
+- Docked strips run the full height, the header search is a magnifier icon,
+ `g p` toggles the day panel.
+- Without a zoom in the URL the timeline fits its window to the record: three
+ months, clamped to the first entry (at least 28 days) for a young record,
+ and everything when the last three months are empty. An empty window offers
+ "Show everything" when older entries exist.
+- The explanatory line under day-linked charts is gone.
+
+### API
+
+- New: `POST /api/search`, `POST /api/notifications/action/{token}`.
+- Medication: `autoMarkTaken`, `autoMarkTakenSince`, `remindersActive`;
+ intakes and sync intake upserts: `autoRecorded`.
+- `notificationPrefs.content.detail` on the notification preferences.
+- APNs payloads carry `url` on every visible push.
+
+## [1.42.1] — 2026-10-10
+
+Fixes for Google Health syncing and the measurement maintenance run.
+
+### Fixed
+
+- **Google Health parked after a reconnect (#1194).** A backfill walk used
+ one access token throughout; after Google's one-hour expiry the walk saw
+ 401s and the connection was parked at `error_reauth` although the refresh
+ token was valid. Resources now take a token source that renews before
+ expiry and once more on a mid-walk 401; a 429 is waited out (Retry-After,
+ RetryInfo or backoff) and stays transient; parking follows only a refused
+ grant at the token endpoint, and an account parked by a data-call 401
+ recovers on its next run. One sync per account at a time: the hourly run
+ skips, a manual sync answers 409 `sync_in_progress`, backfill and sleep
+ repair wait. An interrupted backfill resumes from `backfill_progress`
+ (migration 0393).
+- **Overlapping Google Health sleep segments (#1195).** A batch keeps one
+ reading per natural key (the longer segment), a remaining collision is
+ settled through `reconcileExternalMeasurement`, and overlapping sessions
+ no longer clear each other's rows. Night totals use the union of segments.
+- **Measurement maintenance on Docker (#1192).** The maintenance connection
+ sets `max_parallel_maintenance_workers=0` and needs no `/dev/shm` segment;
+ the bundled `docker-compose.yml` sets `shm_size: 256mb` for the database.
+- **Dashboard spacing.** The tile row sat flush against the top card; a
+ `display: contents` wrapper had swallowed the gap.
+
+### API
+
+- `POST /api/google-health/sync` may answer 409 `sync_in_progress`.
+
+## [1.42.0] — 2026-10-09
+
+A day view behind every dated value, an optional timeline with life events,
+air quality and pollen per day, a Health Connect import, and the handover of
+a managed profile to the person it describes. The measurement table drops
+the samples the nightly fold used to keep, after a one-time repair of means
+an earlier fold computed from part of a day.
+
+### Added
+
+- **Day view (#613).** `?day=YYYY-MM-DD` opens one local day over the
+ current page: docked from 1280 px beside a fixed strip at the right edge
+ that opens and closes it and keeps it, with the last day, across pages; a
+ side sheet below that and a bottom sheet on a phone. On `/coach` it sits
+ next to the conversation list, each with its own strip. It shows the value
+ it was opened from, what ran through the day (one entry per medication
+ with the day's dose, the cycle day and phase), eight curated readings
+ against the 30-day usual range (the rest behind "All values"), the day's
+ scores (health score, readiness, recovery, sleep score, strain) and the
+ day's entries in clock order. Readings and scores link to their pages, and
+ the date in the header opens a calendar with Today. Every chart drawn in days opens it (on touch the tooltip offers the
+ day first), as do data tables, the measurement, mood and intake lists,
+ heatmaps, lab readings, documents, workouts, symptom and illness days and
+ the days a Coach answer read. Back and Escape close it, Alt with an arrow
+ steps a day. A planned visit shows what changed since the last one from
+ `GET /api/day/notable`. Not offered: the dashboard's today area, tile
+ charts, the cycle calendar, model text, future dates, the clinician share
+ link and the doctor-report PDF.
+- **Timeline module (#613).** Opt-in. One SVG time axis from 768 px with
+ lanes for life events, conditions, allergies, medications, vaccinations,
+ visits and procedures, labs and documents (one row per medication, dose
+ changes and pauses as segments of its bar), a year, month or week grid or
+ any range, and as many value lines as the record has types, as quarterly,
+ monthly, weekly or daily means by span (a short gap dashed, a longer one
+ left open, a mean from fewer than three readings drawn hollow, each point
+ with its own tooltip); a chronicle, newest first, on a phone. Switching the module on opens a readiness inventory that rates
+ each lane and links each gap; it stays available as Data coverage.
+- **Life events.** Title, kind (family, home, work, loss, other), a start
+ at day, month or year precision, optional end and note; title and note
+ encrypted at rest (migration 0387). Owner only in this release: no share
+ level or MANAGE grant reads or writes them, and no model ever receives
+ them. They travel in both backup purposes and are wiped with the account.
+- **Air quality, pollen and UV (#615).** The environment fetch adds the
+ Open-Meteo air-quality feed (CAMS, `domains=auto`): PM2.5, PM10, NO2,
+ SO2, CO, the ozone 8-hour high, European and US AQI, UV, dust, aerosol
+ depth and six pollen groups, folded into local days (means and maxima need
+ 18 of 24 hours) and stored as null where the feed has no value
+ (migration 0381). Pollen exists only in the European domain. A nightly
+ gap fill covers up to 90 days per account. PM2.5, the ozone high and the
+ highest pollen join the correlation channels. An account switch under
+ Location & weather (on by default), attributions beside the values, and a
+ dashboard chip on a day with high pollen, a warm night or very poor air.
+- **Air quality for the past.** A background job fills air quality, pollen
+ and UV for earlier local days with an entry: after the update, when the
+ home is set and when air quality is switched on. A day inside a location
+ period is fetched at that place, a day from the home's effective date on
+ at the home; a day before it with no period stays empty rather than
+ borrowing a place the person was not at. The source reaches back to 2013
+ in Europe and to August 2022 elsewhere, pollen from 2021 in Europe only;
+ older days stay empty. Nearby days share one
+ request of up to 90 days, a run holds itself to 300 of the account's 400
+ daily calls and half of each instance window, and a long history spreads
+ over several nights. Location & weather shows the progress (migration
+ 0391).
+- **`get_environment` and `get_day`** for the Coach and MCP.
+ `get_environment` is module-gated and carries no coordinate or place name;
+ `get_day` leaves out life events, notes and document names and honours
+ the person's Coach exclusions.
+- **Health Connect import (#972).** Upload the ZIP Android's Health Connect
+ exports under Export & Import. It is read in the background (read-only
+ SQLite, schema version 9 or later, 1 GiB upload cap, one import per
+ account): daily totals from the top-priority app, per-minute heart rate
+ for 90 days and hourly means before that, sleep stages, workouts, cycle
+ days, water and nutrients, under the new source `HEALTH_CONNECT`
+ (migrations 0379, 0380). Apps already connected directly are skipped and a
+ second import of the same file writes nothing.
+- **Managed profile handover (#959).** A guardian with a fresh second factor
+ creates a one-time link (1, 7 or 14 days, shown once with a QR code) and
+ proposes no access, view only or manage per guardian. The person opens
+ `/claim/`, sets a username, email and password, and decides each
+ guardian's access after signing in. Pending invitations are withdrawn;
+ disclaimer, onboarding and AI consent reset. Refused on `OIDC_ONLY`
+ instances (migrations 0383, 0390).
+- **Custom vaccines (#1005).** A record defines its own vaccine with the
+ antigens it covers, series length and booster interval; its doses count
+ into each antigen's series and settle boosters (migration 0386).
+- **Workout editing (#1162).** A hand-entered workout opens its form from
+ the detail page and saves as an overwrite of the same row.
+- **HRV RMSSD from Apple Health (#1110).** iOS 27's
+ `HKQuantityTypeIdentifierHeartRateVariabilityRMSSD` is stored as
+ `HRV_RMSSD` (1 to 300 ms) with its own source-priority ladder, charted
+ apart from SDNN.
+- **Apple Health arrival per type (#1173).** The Apple Health card shows
+ when each type last arrived, by which trigger (including the new
+ `manual` for Sync all) and when it last brought a new value
+ (migration 0384).
+- **Documents layout.** Preview cards or a compact list, months flowing
+ side by side (the default) or stacked, per account (migration 0388). Only
+ a field the person picked is stored, so an untouched one follows the
+ default.
+- **Measurement maintenance.** An admin card and
+ `POST /api/admin/maintenance/measurements` run `VACUUM (ANALYZE)` and a
+ `REINDEX INDEX CONCURRENTLY` per index, largest first.
+- **Query statistics.** The bundled database loads `pg_stat_statements`;
+ `CREATE EXTENSION` once to read it (`docs/ops/query-statistics.md`).
+- **Score history.** `/insights/health-score` charts the daily health score
+ with its usual band, broken where the score's recipe changed, from
+ `GET /api/insights/score-history`; readiness, recovery, sleep score,
+ stress and strain share one 7, 30, 90 days or all range picker.
+- **Top card switch.** The dashboard layout settings can hide the top card
+ (`todayCardVisible`).
+- **Keyboard shortcuts.** `g` then `d`, `m`, `l`, `t`, `i`, `c` or `s` opens
+ a page, `n` the add menu, `[` and `]` step the open day, and `?` (or the
+ account menu) lists them; ignored while typing in a field.
+- **Command palette.** Cmd+K (Ctrl+K) or the search field in the top bar
+ finds pages, Insights pages, every Settings card by its anchor, module
+ switches and actions (new entry, the Coach, a backup, today, the
+ shortcuts), filtered by modules, AI availability, role and a shared
+ record's grant; typo-tolerant, with recent places when empty.
+
+### Changed
+
+- **Correlations remove season and trend.** Every daily pair is correlated
+ on residuals after a linear trend and one annual harmonic are taken out
+ of both series (trend only under 120 days), with p-values on the
+ Pyper-Peterman effective sample size. Weather channels compare the mean
+ of the day before and the day itself with the same day's outcome. Some
+ earlier findings no longer qualify.
+- **Fold leftovers.** The nightly folds delete the raw samples they fold
+ instead of keeping them for 75 days. A re-sent raw sample whose hour or
+ day a live `stats:` row covers is a duplicate with reason `folded_window`,
+ in the batch route, the export import and the Health Connect import. A
+ background purge removes the backlog in batches of 5,000 per account; the
+ sync feed and a restore leave these deletions out. Health Connect heart
+ rate is folded like Apple Health's.
+- **Duplicate batches.** A batch of samples the server already holds is
+ answered without a write and queues no record detection or reminder
+ sweep; caches are evicted only for a foreground or manual sync or a
+ hand-entered reading, and marked stale otherwise.
+- **Locations encrypted.** The home location, travel periods and stored
+ environment days keep their coarse location sealed (migration 0382); a
+ background job seals existing rows.
+- **Lighter client.** Settings sections load on their own (611 to 403 KB
+ gz) and the shared shell no longer carries Zod (199 to 136 KB gz).
+- **Wellness scores** show their course on each tile and page; the sleep
+ score and readiness reads add `series`. The proxy caveat line is gone.
+- **Empty states and add actions.** An empty list carries the one add
+ action; the Insights pages add through the header plus.
+- **Touch and small screens.** 44 px targets on touch screens, a sidebar
+ that fits short windows without a native scrollbar, the phone shell on a
+ phone held sideways, 16 px fields, safe areas in the installed app and
+ sheets that stay above the on-screen keyboard.
+- **Provider pause.** From five consecutive hard failures a provider is
+ skipped for a window that starts at 30 minutes and doubles, capped at a
+ day; a dead Codex refresh token reads as a sign-in to renew.
+- **`work_mem`** is set per app connection from `DATABASE_WORK_MEM`
+ (16MB); `DATABASE_SESSION_OPTIONS_DISABLED` sends no session options for
+ hosts behind PgBouncer.
+- Volume queues keep finished jobs for 96 hours instead of seven days.
+
+### Fixed
+
+- **Means folded from part of a day.** The folds cut at a fixed offset
+ instead of the local day boundary, so a second run replaced a mean-type
+ day, and the boundary hour of heart rate, HRV and SpO2, with the mean of
+ its later part. Folds now stop at the local day boundary, and a one-time
+ repair per account recomputes each mean from its samples and writes only
+ values that differ (migration 0389). It covers the last 75 days of daily
+ means and the 75 days before the 90-day raw window of hourly means, where
+ every sample of the day is still kept. Older days stay as they are: their
+ remaining samples can be a fragment of the day, and a mean over them would
+ be worse than the stored one. The nightly folds follow the same rule for a
+ day or hour that already has a mean. The purge waits for the repair.
+- **Push after an update.** Reload on the new-version hint unregistered the
+ service worker, which ended the Web Push subscription; it now updates the
+ worker instead.
+- **Discord and Slack webhooks.** A Discord webhook URL answered the test
+ and every reminder with "Cannot send an empty message", and a Slack
+ incoming webhook refused the body too: the generic format carried neither
+ Discord's `content` nor Slack's `text`. It now sends both as plain text
+ (title and message, cut to Discord's 2000 characters), tells Discord to
+ resolve no mentions, and keeps `title`, `message`, `eventType` and
+ `priority` unchanged for Home Assistant and n8n rules. Discreet mode
+ covers the new fields.
+- **MCP deep links.** `/insights?metric=` and `/labs?analyte=` open the
+ metric's page and the marker's page.
+- **Apple Health status** no longer reads "waiting for first data" after an
+ export import.
+- **Strain** falls back to the device's day strain when no computed value
+ exists.
+- Failed briefing runs name each account's outcome, a briefing held back
+ for a causal claim gets one repair attempt instead of hourly retries, and
+ the tiered context converts stored times to the reader's zone.
+- Google Health re-reads two hours of intraday heart rate per sync, and the
+ first cycle of each local day a full day (#1023).
+- The capture sheet opens from the dashboard's add button on phones; the
+ installed app follows its own theme in the status bar and toasts.
+- `source-map-js` 1.2.2, `postcss-selector-parser` 7.1.6 and `smol-toml`
+ 1.9.0 close CVE-2026-93749, CVE-2026-104844 and GHSA-r4xh-jqrq-34v2 in the
+ build and development toolchain.
+
+### Removed
+
+- The "took it twice" and "skipped a dose" achievements.
+
+### API
+
+All changes are additive.
+
+- New routes: `/api/day/{date}`, `/api/day/index`, `/api/day/notable`,
+ `/api/timeline`, `/api/timeline/readiness`, `/api/life-events`,
+ `/api/vaccinations/custom`, `/api/environment/preferences`,
+ `/api/documents/inbound/layout`, `/api/import/health-connect-export`.
+- The measurement source enum gains `HEALTH_CONNECT`.
+- `/api/auth/me` adds `timezone`, `glucoseUnit` and `features.trackIntake`;
+ module availability lists `timeline`.
+- `POST /api/measurements/batch` accepts `syncTrigger: "manual"`; a re-sent
+ sample whose hour or day is already condensed returns `duplicate` with
+ reason `folded_window`.
+- `GET /api/integrations/healthkit` adds `lastReceivedAt`, `lastTrigger` and
+ `lastNewSampleAt` per type.
+- The sync feed lists only deletions a person makes, not those of the
+ condensing step.
+- `GET /api/environment` adds `airQuality` (with `history`, the progress of
+ the past-days backfill), `latestDay` and `attributions`.
+- `GET`/`PUT /api/auth/me/source-priority` add `inUse`.
+- A dose for a record-only medication is refused with 422
+ `medication.intake.notTracked`; failed passkey sign-ins answer with
+ `passkey.*` codes instead of a 500.
+- `/api/medications/compliance` adds `meta.aggregate` and `?days=`;
+ `/api/mood/insights` takes `?days=` and returns `stabilityWindowDays`.
+- Coach steps can name the tools `get_day` and `get_environment`, the
+ domains `day` and `environment`, and a `day` date.
+- Vaccination doses carry `customVaccineId`, correlation findings `nEff`,
+ workouts `storedAvgHr` and `storedMaxHr`, and the sleep score and
+ readiness reads a `series` of past values.
+- `/api/life-events*` answer only the record's owner; a delegate gets 403,
+ the day's `lifeEvents` section reads `not_shared` and the timeline has no
+ `life` lane.
+- `POST /api/environment/backfill` answers 429
+ `environment.backfill_rate_limited` (three an hour per account) and 409
+ `environment.backfill_pending` while one is queued.
+- `GET /api/environment` may return `lat` and `lon` as `null`.
+- The handover claim preview no longer carries `grantId`; a guardian's
+ `displayName` may be `null`.
+- The restore report adds the catalog `environmentLocationCiphertext`.
+- `get_day` names documents and visits by kind only.
+
+### Thanks
+
+@lutzkind for #613, #615 and #586, @CritLoren for #972 and #1005, @ckaotik
+for #959, @BernhardBuckel for #1110, @mills1975 for #1173 and @NebuPookins
+for #1162.
+
+## [1.41.2] — 2026-10-07
+
+More room for a Coach turn, prompt caching that holds across its rounds, and
+MCP tool results that match their schemas.
+
+### Changed
+
+- **Turn budget.** A turn on the person's own provider gets 165k tokens,
+ 200 s and 16 rounds (was 120k, 150 s, 12); a turn on the operator's budget
+ 55k tokens, 120 s and 8 rounds (was 40k, 90 s, 6). The final answer's
+ reserve keeps its share of each (1,650 tokens), the concurrent-turn slot
+ follows the longer wall time, and a turn shows up to 64 steps.
+
+### Fixed
+
+- **Cached input in the turn budget.** Codex requests carry a stable
+ `prompt_cache_key` and session id per conversation, as the official client
+ does, so later rounds of a turn read the prefix from the provider's cache
+ instead of paying for it again; `api.openai.com` gets the same key, gateways
+ do not. Anthropic usage now counts cache reads and cache writes in the
+ turn's total (they are reported beside `input_tokens`), so a cached round is
+ weighted at a tenth instead of almost nothing and a cache write in full.
+ Local and OpenAI-compatible servers that report
+ `prompt_tokens_details.cached_tokens` have it counted.
+- **MCP output schemas (#1170).** `list_metrics` declares the `availability`
+ its rows already returned, the Coach-backed reads declare `searchedWindow`
+ and `available`, and `get_intraday_pulse` declares each bucket's `min` and
+ `max`. The advertised
+ schemas close every object, so a strict client refused these results. Every
+ tool's real output is now validated against its advertised schema under a
+ strict 2020-12 validator. Thanks to @RyRyMason for #1170.
+- **Deleted readings in MCP discovery.** The `list_metrics` discovery rows and
+ the `search` clinical-signal probe count live rows only: a measurement type
+ whose readings were all deleted is no longer listed as present, and the
+ probes use the live-rows index instead of walking every deleted row.
+
+## [1.41.1] — 2026-10-07
+
+A calmer thinking row in the Coach.
+
+### Changed
+
+- **One thinking row.** While the Coach works, a single row shows a brain
+ icon and the current step in body text with a soft moving highlight and
+ animated dots ("Reading blood pressure…"); no running seconds. The interim
+ table line under it is gone; tables appear with the answer. Afterwards the
+ row reads "Thought process · N steps", stays closed unless opened, and
+ closes again when a turn finishes. Opened, the steps stand on a thin line,
+ each with its own icon and the reasoning summary under its title.
+
+### Fixed
+
+- **Coach in the demo.** A refused chat on a demo instance says that the
+ Coach doesn't answer in the demo instead of reporting an unreachable
+ provider; demo refusals carry `meta.errorCode: "demo.readOnly"`.
+
+## [1.41.0] — 2026-10-07
+
+The Coach reasons before it answers, shows what it is doing while it works,
+asks when a question is ambiguous, and remembers what it is told.
+
+### Added
+
+- **Reasoning.** Codex, OpenAI, Anthropic, OpenRouter, gateways and local
+ servers receive the requested reasoning in their documented dialect;
+ reasoning summaries stream to the Coach, signed or encrypted reasoning state
+ is carried between tool rounds and never stored, and a model that rejects a
+ reasoning parameter is retried without it (the downgrade is learned per
+ provider, endpoint and model for an hour, only from a 400 that names the
+ parameter). Thinking depth per person under Coach settings (Off, Low,
+ Medium, High; Medium by default); an operator switch and maximum under
+ Admin, Assistant; turns on the operator's budget are capped at Medium.
+ `/api/auth/me` publishes the resolved `coachReasoning`.
+- **Budgeted tool loop.** A turn runs against a token, time and round budget
+ (own provider 120k tokens, 150 s, 12 rounds; operator 40k, 90 s, 6 rounds)
+ with a no-progress brake and a reserved final round that keeps the tool
+ definitions with `tool_choice: none`. Every round is reserved against the
+ daily budget beforehand and settled afterwards. At most two concurrent turns
+ per person.
+- **Live trail.** One quiet status line per answer (`activity` frames), never
+ expanded by itself; the screened reasoning text is stored encrypted
+ (`coach_messages.trail_encrypted`) and served lazily by
+ `GET /api/insights/chat/{id}/messages/{messageId}/trail`.
+- **Comparisons.** `compare_series` (two periods or two metrics) and a
+ `compare` chart; tables arrive as interim results before the answer.
+- **Clarifications** as suggested replies in the thread, at most one in six
+ turns and three a day; a declined or limited question becomes a stated
+ assumption with a reply that changes it.
+- **Memory and plans.** Every turn carries an egress-checked memory block;
+ `remember_fact` saves preferences, goals and context with a visible note and
+ undo, health facts only after the person agrees; `propose_plan` suggests a
+ plan from a stated goal. Both are written only when the answer that shows
+ them is stored. Background extraction runs once a conversation is quiet for
+ 30 minutes; plan progress reaches the daily briefing. The memory list in
+ settings shows each fact's source and accepts edits.
+- **Background reasoning** for the daily briefing (scheduled and on request)
+ and the weekly and monthly narratives; status cards never reason.
+
+### Fixed
+
+- **Dose-change screen.** The outbound screen blocks the dose-change class
+ across all seven languages, including the person's own medication names,
+ drug classes, spelled-out amounts and "extra / one more"; plan texts go
+ through the same screen.
+- **Glucose by time of day.** The usual-range line, the MCP baseline and the
+ signals of the day compare glucose with the same hours of earlier days
+ instead of setting a morning reading against whole-day means.
+- **Usual-range line** names the date of a latest reading that is not from
+ today, and a steady value as one figure.
+- **Memory refresh** asks the Coach capability before deferring, so a
+ switched-off Coach does not re-queue it.
+- **Dependencies.** The MCP SDK moves to 1.32 (GHSA-6qxp-vccf-f47h) and
+ `proxy-addr` to 2.0.8 (GHSA-jqcg-44mw-7w3h).
+
+### Changed
+
+- **Answer voice.** The first sentence answers; 40 to 120 words; at most one
+ next step.
+- **Keyboard focus** is drawn in a neutral tone everywhere.
+
+### Migrations
+
+- `0376_ai_reasoning_controls`, `0377_coach_message_trail`,
+ `0378_coach_fact_source`: additive columns with constant defaults or
+ nullable; no table rewrite.
+
+## [1.40.6] — 2026-10-05
+
+The Coach reads long series again; "today" only from today's readings; the
+dashboard card fits a phone; text fields lose the purple focus ring.
+
+### Fixed
+
+- **Coach series without numbers.** Since 1.21.0 every Coach tool read one
+ shared per-turn snapshot capped at 24,000 characters; on accounts with many
+ series whole blocks became a placeholder and the tool still answered
+ `present: true`. A trimmed block is now re-read for the tool's own source,
+ a Coach tool's read of one source is condensed instead of emptied (a
+ summary from the daily values with the latest week against the first,
+ the newest days, monthly means, each block in its own fields), a block
+ without numbers
+ answers `retrieval_failed` to the Coach, and the Coach calls the table tool
+ itself for long histories. MCP reads are unchanged. The step list shows
+ the reading count; no stray comma on copy.
+- **"Today" from yesterday's readings.** Signals of the day, the vitals fact,
+ the lead line, the health status and the morning push only speak about
+ today from readings taken today in the profile timezone; a stored briefing
+ from an earlier day is no longer served.
+- **MCP schemas for strict clients (#1170).** `tools/list` advertises every
+ input and output schema as JSON Schema 2020-12 without a `$schema` key, so
+ clients that only accept 2020-12 load the tools; tool results are unchanged.
+- **Briefing for the local day.** The first read of a local day and the first
+ reading of a signal metric that day warm the briefing once (within the
+ existing daily limit), so users outside Europe and readings taken in the
+ morning get a current briefing; every surface resolves it the same way.
+- **Deltas** use each metric's display precision ("+34 bpm").
+
+### Changed
+
+- **Dashboard card on a phone.** A compact score ring at the top right beside
+ the lead; Today facts as label over value, full width. Desktop unchanged.
+- **Text-field focus.** No purple ring on inputs, textareas, selects and date
+ fields; focus shows as a neutral border (`--input-focus`). Invalid fields
+ keep their red border.
+- **Coach panel** no longer has a search field (the full conversation list
+ keeps its search).
+
+## [1.40.5] — 2026-10-05
+
+The Coach's conversation panel runs the full height of the window.
+
+### Changed
+
+- **Coach panel.** Docked, the panel runs from the top of the window to the
+ bottom, beside the top bar instead of below it, and its header line
+ continues the top bar's. Shut, nothing of it remains on screen.
+- **Top bar on the Coach.** It names the page and the open conversation, and
+ the panel toggle sits at its right end, against the panel.
+- **New chat.** A round button with a plus at the panel's bottom right.
+
+## [1.40.4] — 2026-10-05
+
+The Coach's conversation panel docks to the right edge; model and tokens
+move behind an info icon; no view chip for a result that already toggles.
+
+### Changed
+
+- **Coach panel.** Flush against the right edge, full height below the top
+ bar, its own scroll area; the shell renders `/coach` without its container
+ gutter. The toggle uses mirrored panel icons and moves with the panel.
+ Its buttons and rows go compact only beside a mouse or trackpad; with
+ touch they keep the 44 px target at any width, a tablet included.
+- **Answer row.** Copy, read aloud, retry, an info icon and the time in one
+ left-aligned group; the info icon shows model and token count on hover,
+ focus or tap, and Enter or Space keeps it open.
+- **Follow-ups.** No "show as chart/table" chip when the answer's result
+ already offers that toggle; also for stored answers.
+
+## [1.40.3] — 2026-10-05
+
+Small fixes to the Today overview and the numbers behind it.
+
+### Added
+
+- **`signalLine` on the daily digest.** The muted line under the lead is now
+ decided on the server: when the lead already talks about the top signal's
+ metric, only the delta remains, and the line is null when nothing is left.
+ Every client shows the same line.
+- **`basis` on health-status deviations.** `day` for the usual whole-day
+ range, `sameHours` for the usual range at this time of day.
+
+### Fixed
+
+- The Today lead is shown as whole sentences. A long sentence is no longer
+ cut mid-word; the headline or the deterministic sentence takes its place,
+ and only as a last resort is text shortened, at a word boundary.
+- Chart trend lines, legends and tooltips name every metric in the reader's
+ language instead of showing its internal name (resting heart rate among
+ them).
+- A personal usual range never collapses to a single value ("61–61 bpm"):
+ it keeps at least the measurement's own day-to-day spread, so the next
+ ordinary reading is not counted as outside it.
+- Glucose today is compared with the same hours of earlier days, up to the
+ time of today's latest reading. A fasting morning no longer reads as below
+ the range of whole days, and the range named is the usual one for this
+ time of day.
+- Encrypting stored text in place or rotating the encryption key no longer
+ makes old Coach conversations look recently active, and the Coach memory
+ summary no longer moves a conversation either. Measurements, mood entries
+ and every other encrypted record keep their sync position through the
+ same kind of rewrite.
+- Conversations an earlier release made look recently active move back to
+ their last message (migration 0375).
+
+## [1.40.2] — 2026-10-05
+
+A calmer Coach with conversations in a side panel and settings in place; the
+top of the dashboard says what matters today; documents filter by procedure.
+
+### Added
+
+- **Coach panel.** Conversations in a collapsible right panel (a sheet on a
+ phone), grouped by day, with rename, delete with undo and a new-chat
+ button; a gear opens quick settings (provider, model and reasoning where
+ they are the user's, and the lookback). `/coach?settings=data` opens it.
+- **Coach lookback as a real limit.** 7, 30 or 90 days, 12 months
+ (`lastYear`, new) or all; it limits the snapshot and every Coach tool, a
+ read beyond it answers `outside_reach`. MCP keeps its own windows.
+- **Today overview.** The digest gains `lead`, `today[]`, `restMode` and
+ `score.steadyWeeks`/`steadyAtLeast`: a lead line with content (deterministic
+ without AI), up to five facts (medications, an appointment today or
+ tomorrow, last night, vitals, cycle, Rest Mode), the ring with how long the
+ score has been steady. The morning push line carries no vital values.
+- **Procedure filter in the document archive (#1154).** `linkedProcedures` on
+ the usage endpoint; the list already accepted `encounterId`.
+
+### Changed
+
+- **Coach answers.** One action row (copy, read aloud, retry, time; tokens and
+ model); web thumbs and the evidence block removed; remember is an icon;
+ follow-up chips in the answer column; a scroll-to-bottom button; the
+ composer's "+" menu removed.
+- **Settings → Coach** summarises what the Coach sees and links into the
+ overlay.
+- Coach preferences parse field by field, so one invalid value no longer
+ drops the others.
+
+### Fixed
+
+- The medications fact counts the whole day on a 25-hour DST day.
+- Providers without credentials are no longer offered in the Coach overlay;
+ an operator-managed provider shows as such.
+- Document filter controls match the search field's height on a phone.
+
+## [1.40.1] — 2026-10-04
+
+Restore opens older encrypted values again; faster pages on a long-used
+server; a day of pulse is the mean of its hours; Apple Health imports leave
+out HealthLog's own samples; more memory for the app by default.
+
+### Fixed
+
+- **Restore key probe.** A legacy unprefixed ciphertext was read as a base64
+ string and refused a restore the key opens; legacy values open as strings,
+ probes come only from sections that count, binary probes use their AAD.
+ The drill checks every account of the newest date.
+- **Daily pulse (#1107, migration 0372).** A day is the mean of its local
+ hours' means and a window the mean of its days, in every reader; stored
+ pulse rollups are recomputed. Count, min and max stay over every reading.
+- **Apple Health import (#1109).** Records carrying HealthLog's origin
+ marker or a HealthLog id are left out and counted; cycle samples too.
+- **Blood pressure series.** One source per day for both values.
+- **Backups.** Heartbeat on both backup jobs; per-account attempts recorded
+ (migration 0373), interrupted accounts go last; the preview is built from
+ the writer's counts.
+- **Document previews.** A permanent render failure is recorded (migration 0374) instead of re-queued at every start.
+- **Umami.** Events from a tracker built with a custom endpoint are proxied
+ to the configured Umami host.
+- **Display.** Rest Mode shows its start date; the favicon is transparent;
+ the sidebar no longer flashes scrollbars while it expands.
+
+### Changed
+
+- **Performance.** One Prisma client per process instead of one per bundle;
+ the rollup coverage probe uses a loose index scan and concurrent readers
+ share one probe; timezone offsets are memoised per hour; chart text is
+ measured outside ``; Insights mounts lower sections near the
+ viewport; a nav prefetch is not re-sent on mount; MCP single-metric reads
+ build only the requested metric; the backup schema loads on first use.
+- **Lab list (#1057).** The range bar and name use a wide card.
+- **Compose.** `APP_MEMORY_LIMIT` defaults to `1536m` (792 MB heap).
+- Dependencies: next 16.3.8, eslint-config-next 16.3.8, zod-openapi 6.0.2,
+ testcontainers 12.2.0, p-limit 7.3.3.
+
+## [1.40.0] — 2026-10-04
+
+Back up the encryption key and refuse a key that cannot open the data;
+several courses and custom categories for medications; custom symptoms;
+document reads in the background; sick notes; lab unit refusal and charts
+that follow the source priority; blood pressure no longer loses its
+diastolic value; document previews work again.
+
+### Added
+
+- **Encryption key backup step (migration 0365).** Admin step with key id,
+ fingerprint and per-platform guidance (`HEALTHLOG_PLATFORM`), an optional
+ "check my copy" that compares and discards the pasted key, a reminder that
+ returns when the key changes.
+- **Boot key check.** A canary per key id, probed oldest values first across
+ string and Bytes columns; a proven mismatch answers 503
+ `encryption.key_mismatch` everywhere except `/api/health` and
+ `/api/version`, MCP included, and starts no worker; an unproven result
+ serves, warns and re-checks. `ENCRYPTION_KEY_CHECK=warn` as a last resort.
+- **Medication courses and custom categories (#1024, #1041, migration
+ 0368).** Several courses per medication; compliance, streak, cadence and
+ dose history count every course and never the gaps; existing dated
+ medications become one course each. Own categories as `custom:` with
+ `categoryLabel`; the runtime category table is now in the schema.
+- **Custom symptoms (#981, migration 0369).** Up to eight definitions, events
+ with a 0 to 10 intensity, a capture kind, one correlation channel per
+ symptom, a Coach block; gated on the Illness module.
+- **Document AI as background runs (#1090, migrations 0366 and 0370).**
+ Index, summary, suggestions and extraction accept `Prefer: respond-async`;
+ the lab scan always runs in the background; `GET /api/ai-runs/{id}`.
+- **`SICK_NOTE` document kind.**
+- **Natural durations (#1101)** in the sleep field.
+- **Reasoning effort per provider (#1126).** The Local and OpenAI-compatible
+ forms gain a Reasoning select (Default, Off, Low, Medium, High), stored on
+ the provider's chain entry; Default sends nothing, Off sends
+ `reasoning_effort: "none"`. A rejection of that field no longer switches
+ the endpoint's JSON mode off.
+
+### Fixed
+
+- **Blood pressure saved without its diastolic value (iOS #15, migration
+ 0371).** The iPhone app sends the two halves of a manual reading as two
+ requests under one `Idempotency-Key`, and the server answered the second
+ with the first one's cached response. The replay cache now stores a SHA-256
+ fingerprint of method, path and body (JSON canonicalised, a multipart
+ boundary normalised) and replays only the same request; a different body
+ under the same key is saved. Readings saved before this stay incomplete;
+ `docs/ops/blood-pressure-missing-diastolic.md` lists them.
+- **Document previews and scanned PDFs (#1124).** The image hoisted an older
+ `@napi-rs/canvas` binary next to the pinned loader, so every thumbnail
+ failed; the build now hoists the pinned version and fails on a mismatch.
+ PDFs render with pdfjs font, cmap and wasm data and a registered fallback
+ font instead of absent system fonts, and an all-blank render reports
+ `raster-failed`. The thumbnail job logs why a preview is missing.
+- **Portable backups.** Cycle free text, custom mood labels, visit notes
+ and vaccination notes are carried readable and re-encrypted on restore
+ instead of as the source server's ciphertext; custom mood tags restore
+ without mood entries; a round-trip guard fails on any ciphertext in a
+ portable file. A medication update and its course share one transaction.
+- **Lab unit mismatch (#1094).** A reading in another unit than its marker is
+ refused (`labs.unit.mismatch`) on every write path; a marker's unit is
+ locked while it has readings (`biomarkers.unit.locked`).
+- **Source priority in series (#1108).** Pulse, HRV and SpO2 series read the
+ ladder's source per day; unranked types keep every source.
+- **Sign-in over plain http (#1097).** Refused with
+ `auth.session.insecure_transport` before a password, code or challenge is
+ used; the sign-in page says so first.
+- **Chart window (#1102).** Ends at the end of today in the profile time zone.
+- **Workouts.** The insight cap survives a delete (migration 0367); a changed
+ `manual:` resubmit updates the workout and its paragraph.
+- **Display.** Clinician decimals in the owner's locale, stated dates east of
+ UTC+12, archive month grouping, lab list columns without a range, icon
+ picker touch targets and labels, symptom entry load errors.
+
+### Changed
+
+- Quick-entry forms load on demand; the four eager route baselines are
+ lowered to the new measurements.
+
## [1.39.9] — 2026-10-03
Workouts can be logged by hand, the body fat tile opens its own page, and
diff --git a/Dockerfile b/Dockerfile
index 6b871db84..475e51861 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -54,6 +54,9 @@ RUN NODE_OPTIONS=--max-old-space-size=8192 pnpm build
# name; see the note there for why guessing was wrong.
RUN node -p "require('/app/node_modules/pdfjs-dist/package.json').version" \
> /app/.pdfjs-version
+# The same for @napi-rs/canvas, whose native binary must match its JS loader.
+RUN node -p "require('/app/node_modules/@napi-rs/canvas/package.json').version" \
+ > /app/.canvas-version
# ── Stage 3: Production runner ─────────────────────────────
FROM node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2 AS runner
@@ -120,21 +123,41 @@ RUN node -e "require.resolve('pg-boss'); require.resolve('pg')"
# @napi-rs/canvas" → "Cannot polyfill DOMMatrix" → rasterize ReferenceError);
# (2) @napi-rs/canvas's own loader then resolves its platform binary. Hoist BOTH
# the canvas package and the musl binary package to /app/node_modules/@napi-rs
-# via symlink into the .pnpm store, and also drop the prebuilt .node into the
-# canvas dir so the loader's local-file fallback (`require('./skia..node')`)
-# is belt-and-suspenders. Per-arch buildx installs only the matching musl triple.
+# via symlink into the .pnpm store, and drop the prebuilt .node into the canvas
+# dir: the loader tries that local file (`require('./skia..node')`)
+# before the platform package, so it decides which binary runs.
+#
+# Hoist the version the app pins, by name, exactly like pdfjs-dist below. The
+# store holds a second, older canvas that pdf-parse's own pdfjs-dist pulls in,
+# and this step used to take whichever `find | head -1` returned first. It
+# returned the old binary, which then sat next to the new loader: rendering
+# still worked, but `loadImage` called an `Image.decode` the old binary does
+# not have, and every preview thumbnail failed with "image.decode is not a
+# function". Name the version, and fail the build when the binary that lands
+# in the canvas dir is not that version's.
+COPY --from=builder /app/.canvas-version /tmp/.canvas-version
RUN set -e; \
- CANVAS_DIR="$(find /app/node_modules/.pnpm -maxdepth 4 -type d -path '*@napi-rs+canvas@*/node_modules/@napi-rs/canvas' 2>/dev/null | head -1)"; \
- MUSL_DIR="$(find /app/node_modules/.pnpm -maxdepth 4 -type d -path '*@napi-rs+canvas-linux-*-musl@*/node_modules/@napi-rs/canvas-linux-*-musl' 2>/dev/null | head -1)"; \
- NODE_BIN="$(find /app/node_modules/.pnpm -maxdepth 5 -name 'skia.linux-*-musl.node' 2>/dev/null | head -1)"; \
- if [ -n "$CANVAS_DIR" ]; then \
- mkdir -p /app/node_modules/@napi-rs; \
- ln -sfn "$CANVAS_DIR" /app/node_modules/@napi-rs/canvas; \
- [ -n "$MUSL_DIR" ] && ln -sfn "$MUSL_DIR" "/app/node_modules/@napi-rs/$(basename "$MUSL_DIR")"; \
- [ -n "$NODE_BIN" ] && cp "$NODE_BIN" "$CANVAS_DIR/"; \
- chown -R nextjs:nodejs /app/node_modules/@napi-rs; \
- echo "canvas hoisted for pdfjs: $CANVAS_DIR (musl=$MUSL_DIR)"; \
- else echo "WARN: @napi-rs/canvas not found; PDF rasterization will degrade to local text"; fi
+ CANVAS_VERSION="$(cat /tmp/.canvas-version)"; \
+ rm -f /tmp/.canvas-version; \
+ CANVAS_DIR="/app/node_modules/.pnpm/@napi-rs+canvas@${CANVAS_VERSION}/node_modules/@napi-rs/canvas"; \
+ MUSL_DIR="$(find /app/node_modules/.pnpm -maxdepth 4 -type d -path "*@napi-rs+canvas-linux-*-musl@${CANVAS_VERSION}/node_modules/@napi-rs/canvas-linux-*-musl" 2>/dev/null | head -1)"; \
+ if [ ! -d "$CANVAS_DIR" ] || [ -z "$MUSL_DIR" ]; then \
+ echo "ERROR: pinned @napi-rs/canvas@${CANVAS_VERSION} or its musl binary did not reach the traced image."; \
+ echo "Present instead:"; \
+ find /app/node_modules/.pnpm -maxdepth 1 -name '@napi-rs+canvas*' 2>/dev/null; \
+ exit 1; \
+ fi; \
+ NODE_BIN="$(find "$MUSL_DIR" -maxdepth 1 -name 'skia.linux-*-musl.node' | head -1)"; \
+ [ -n "$NODE_BIN" ] || { echo "ERROR: no skia binary in $MUSL_DIR"; exit 1; }; \
+ mkdir -p /app/node_modules/@napi-rs; \
+ ln -sfn "$CANVAS_DIR" /app/node_modules/@napi-rs/canvas; \
+ ln -sfn "$MUSL_DIR" "/app/node_modules/@napi-rs/$(basename "$MUSL_DIR")"; \
+ cp "$NODE_BIN" "$CANVAS_DIR/"; \
+ chown -R nextjs:nodejs /app/node_modules/@napi-rs; \
+ BIN_VERSION="$(node -p "require('$MUSL_DIR/package.json').version")"; \
+ [ "$BIN_VERSION" = "$CANVAS_VERSION" ] || { echo "ERROR: canvas binary is $BIN_VERSION, loader is $CANVAS_VERSION"; exit 1; }; \
+ cmp -s "$NODE_BIN" "$CANVAS_DIR/$(basename "$NODE_BIN")" || { echo "ERROR: the canvas dir holds a different binary"; exit 1; }; \
+ echo "canvas hoisted for pdfjs: $CANVAS_DIR (binary $BIN_VERSION from $MUSL_DIR)"
# pdfjs-dist is `serverExternalPackages` (NOT bundled — the bundled copy's render
# path breaks in the standalone image), so the server does a runtime bare
@@ -224,7 +247,7 @@ RUN mkdir -p /opt/prisma-cli && \
npm pkg set 'overrides.mysql2=^3.22.0' && \
npm pkg set 'overrides.@hono/node-server=^1.19.15' && \
npm pkg set 'overrides.valibot=^1.4.2' && \
- npm install --omit=dev prisma@7.10.0 @prisma/engines@7.10.0 tsx@4.23.1 dotenv@17.4.2 && \
+ npm install --omit=dev prisma@7.10.0 @prisma/engines@7.10.0 tsx@4.23.1 dotenv@18.0.6 && \
ln -sfn /opt/prisma-cli/node_modules/.bin/tsx /usr/local/bin/healthlog-tsx && \
ln -sfn /opt/prisma-cli/node_modules/dotenv /app/node_modules/dotenv && \
ln -sfn /opt/prisma-cli/node_modules/prisma /app/node_modules/prisma && \
diff --git a/README.md b/README.md
index 806f6649d..ecc15faeb 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@
iOS TestFlight
-
+
7 chunks (1026 KB gz measured), pushing the aggregate to 3631 against the old 3500. The raise is entirely the new catalog; no route budget moved, and the four eager route ceilings are unchanged and still passing. Most of the aggregate is the dynamic locale-switch and fallback path that a first paint never loads (see issue #253), so this ceiling guards bytes almost no visitor requests; 3700 leaves ~69 KB for ordinary drift until that double-count is cleaned up. v1.38.14 re-measures the four eager routes and changes what the number means. The ceilings had been re-stated but never re-based since v1.37.4, so /insights and /measurements sat within a kilobyte of theirs and / and /insights/mood within one of theirs; e2e run 34043772954 failed on `/insights/page: 445 KB gz exceeds the 445 KB budget` with the other two at 459 of 460, and none of it was a regression anyone had made. A ceiling a route is already touching does not measure the route, it measures whichever dependency bump landed that week. The file states the measured value per route now, plus one drift allowance over it: measured on a local arm64 build at d48bbff01 as / 447, /insights 434, /measurements 433, /insights/mood 447, with a 20 KB allowance, so the effective ceilings are 467 / 454 / 453 / 467 — a raise for all four. The allowance covers the runner delta (the CI runner measures each route a little heavier than a local arm64 build) and ordinary week-to-week drift, while the step change the gate exists for — a statically imported message catalog, a duplicated chart library — is several times larger and still fails. Every run now prints the delta against the baseline, so a route that has crept 15 KB is legible in the log long before it is red; the flat ceiling could only ever say pass or fail. Re-base by re-measuring on a build with nothing else in it and writing the new numbers here with the reason, exactly as a raised ceiling was written before. Raised to 3724 at v1.39.4 on a measured delta, not a feature. The release branch alone measured 3699 KB gz (3698.9) against the 3700 cap, so the ordinary drift room the v1.37.32 raise left had been used up by the releases since; the review fixes on top of it added 1.3 KB gz across the whole build (3700.2), almost all of it new copy in the seven lazy locale catalogs (the total and reused-table labels, the step and chip wording) plus the result-panel states. The four eager routes measured the same on both builds (/ 458, /insights/mood 456, /insights 445, /measurements 442), all inside their ceilings. The aggregate cap moves from 3700 to 3724, the same 24 KB headroom as the re-measurements before it. Raised to 3749 at v1.39.9 on a measured delta. Main measured 3718 KB gz after v1.39.7 and v1.39.8 (unit-aware copy, the phone layouts, the time-zone work and the account-settings restore labels, almost all of it new strings in the seven lazy locale catalogs), leaving 6 KB of the v1.39.4 headroom; the manual workout entry (#1085: the form, its sheet, the delete action and their copy in seven locales) adds about 7 KB and measured 3725. The four eager routes stay inside their drift allowances (/ +12.5, /insights +12.1, /insights/mood +10.2 KB of 20 on main). The aggregate cap moves from 3724 to 3749, the same 24 KB headroom as the re-measurements before it. Raised to 3790 at v1.40.0 on a measured delta. With the background document reads on main (#1103: the run polling hook, the still-reading states and their copy in seven locales), the key backup step and the plain-http sign-in notice (#1104) and the v1.40 fixes (#1106: the duration reader, the marker unit lock, the chart window) each branch measured 3752 KB gz on its own against the 3749 cap; together they add a few KB more. The four eager routes stay inside their drift allowances (/ +18.4, /insights +16.5, /measurements +14.9, /insights/mood +14.7 KB of 20). The aggregate cap moves from 3749 to 3790, about 24 KB above the combined build, the same headroom as the re-measurements before it. Re-based at v1.40.0 when the quick-entry forms began loading on demand. Custom symptoms added a fifth quick-entry form, and with all five forms bundled eagerly every one of the four eager routes measured over its ceiling (/ 472, /insights 457, /insights/mood 468, /measurements 454 KB gz, each 21 to 25 KB over the baseline). Loading the forms on demand brings them to / 429, /insights 368, /insights/mood 379, /measurements 411: 43 to 90 KB less on first load. The four route baselines move down to those measured values, so the gain is protected by the same 20 KB allowance rather than spent. The aggregate moves the other way, 3774 to 4077, for the reason described at v1.37.27: the aggregate counts each emitted chunk once per route group, and on-demand form chunks land in every route group that can open a quick-entry sheet. Bundling the five forms into one shared on-demand module measured the same (4078), so the split is not the cause. The aggregate cap moves from 3790 to 4100. Bytes on disk rose; bytes a reader downloads fell on every eager route.",
+ "$comment": "Client-bundle budgets, enforced by scripts/check-bundle-budget.mjs (--check) after `next build` in CI. Values are KB gzip and include every emitted lazy route and locale chunk, while route budgets include the shared rootMainFiles baseline. Re-measured at v1.37.2: / 455, /insights 443, /measurements 441, /insights/mood 453, total 3328, baseline 130, lazy catalogs 849. Every per-route ceiling is unchanged. The /insights overview crept a kilobyte over its 445 ceiling when the ECG teaser began importing its overview cap from the classification module, dragging that module's label map and helpers into the eager page; the cap now lives in its own value-only module, and the Health Score's single provenance entry is split out of the full metric-provenance map, so the overview imports the bare number and citation it renders rather than every metric's label map behind them. Both are back under 445 with room to spare. The 42 KB aggregate rise from 3286 (v1.35.0) is all locale-catalog strings: v1.36 and v1.37 added the encounter, mood-context, mood-prognosis, practitioner, workout-batch and account-sharing surfaces, and their copy lands across the six locale catalogs the whole-build sum counts independently (804 to 849 KB). The non-catalog client chunks are flat against v1.35.0, so nothing a person downloads on a given route grew beyond its own ceiling. The aggregate cap therefore moves from 3310 to 3352 (24 KB headroom, matching the previous margin); per-route ceilings, the static-catalog guard and the one-Recharts-chunk guard remain unchanged. v1.37.4's honest-error-state charge then added recoverable read-failure handling to thirteen surfaces (the shared QueryErrorCard and QueryErrorRow primitives plus an isError branch per surface) and rewrote the MCP scope note and the managed-record error copy across the six locales; the measured total rose to 3404 while every per-route ceiling stayed met (/page 457, /insights 445, /measurements 443, /insights/mood 455). The aggregate cap moves from 3352 to 3428, keeping the same margin; nothing a person downloads on a given route grew beyond its ceiling. v1.37.15's async-truth charge then added the refresh settling state with its two-locale-key copy, the index-outcome plain-words line (seven keys across the six catalogs) and the reindex-count wiring; the measured total rose to 3429 while every per-route ceiling stayed met (/page 456, /insights 445, /measurements 442, /insights/mood 455). The aggregate cap moves from 3428 to 3452, keeping the same margin; nothing a person downloads on a given route grew beyond its ceiling. v1.37.27 found /insights sitting a few hundred bytes under 445 on main and a few hundred over it on the release branch — the page had been at its ceiling for three releases and the release only had to breathe on it. The weight it was carrying was not its own: the authenticated shell imported one destination predicate from the `@/lib/record-settings` barrel, and the barrel also re-exports the managed settings-patch schemas, so Zod, the module registry and three validation modules rode into the eager bundle of every authenticated route for a function that reads a URL slug. The shell imports the `classification` leaf now. Measured across all 585 client-rendered routes: none grew by a byte, 100 fell, by 4.7 KB at the median and 8.1 KB at the most; /insights 444 to 437, /measurements 442 to 435, / 456 to 450, /insights/mood 455 to 450. The whole-build sum moved the other way, 3451 to 3476, and that direction is the metric rather than the change: the aggregate counts each emitted chunk once, so a graph that leaves the chunk group every route shares gets counted again wherever it lands — here inside the deferred settings chunks that actually use it. Bytes on disk rose 25 KB; bytes a reader downloads only ever fell. The aggregate cap moves from 3452 to 3500, the same 24 KB headroom as the four re-measurements before it. Per-route ceilings keep their old values rather than tightening to the new floor: the CI runner measures each route about 1.5 KB heavier than a local arm64 build does, and a ceiling set from a local number would fail on a build that had not regressed. The static-catalog guard and the one-Recharts-chunk guard remain unchanged. Raised to 3700 at v1.37.32 for the seventh interface language (Korean): the lazy message catalogs went 6->7 chunks (1026 KB gz measured), pushing the aggregate to 3631 against the old 3500. The raise is entirely the new catalog; no route budget moved, and the four eager route ceilings are unchanged and still passing. Most of the aggregate is the dynamic locale-switch and fallback path that a first paint never loads (see issue #253), so this ceiling guards bytes almost no visitor requests; 3700 leaves ~69 KB for ordinary drift until that double-count is cleaned up. v1.38.14 re-measures the four eager routes and changes what the number means. The ceilings had been re-stated but never re-based since v1.37.4, so /insights and /measurements sat within a kilobyte of theirs and / and /insights/mood within one of theirs; e2e run 34043772954 failed on `/insights/page: 445 KB gz exceeds the 445 KB budget` with the other two at 459 of 460, and none of it was a regression anyone had made. A ceiling a route is already touching does not measure the route, it measures whichever dependency bump landed that week. The file states the measured value per route now, plus one drift allowance over it: measured on a local arm64 build at d48bbff01 as / 447, /insights 434, /measurements 433, /insights/mood 447, with a 20 KB allowance, so the effective ceilings are 467 / 454 / 453 / 467 — a raise for all four. The allowance covers the runner delta (the CI runner measures each route a little heavier than a local arm64 build) and ordinary week-to-week drift, while the step change the gate exists for — a statically imported message catalog, a duplicated chart library — is several times larger and still fails. Every run now prints the delta against the baseline, so a route that has crept 15 KB is legible in the log long before it is red; the flat ceiling could only ever say pass or fail. Re-base by re-measuring on a build with nothing else in it and writing the new numbers here with the reason, exactly as a raised ceiling was written before. Raised to 3724 at v1.39.4 on a measured delta, not a feature. The release branch alone measured 3699 KB gz (3698.9) against the 3700 cap, so the ordinary drift room the v1.37.32 raise left had been used up by the releases since; the review fixes on top of it added 1.3 KB gz across the whole build (3700.2), almost all of it new copy in the seven lazy locale catalogs (the total and reused-table labels, the step and chip wording) plus the result-panel states. The four eager routes measured the same on both builds (/ 458, /insights/mood 456, /insights 445, /measurements 442), all inside their ceilings. The aggregate cap moves from 3700 to 3724, the same 24 KB headroom as the re-measurements before it. Raised to 3749 at v1.39.9 on a measured delta. Main measured 3718 KB gz after v1.39.7 and v1.39.8 (unit-aware copy, the phone layouts, the time-zone work and the account-settings restore labels, almost all of it new strings in the seven lazy locale catalogs), leaving 6 KB of the v1.39.4 headroom; the manual workout entry (#1085: the form, its sheet, the delete action and their copy in seven locales) adds about 7 KB and measured 3725. The four eager routes stay inside their drift allowances (/ +12.5, /insights +12.1, /insights/mood +10.2 KB of 20 on main). The aggregate cap moves from 3724 to 3749, the same 24 KB headroom as the re-measurements before it. Raised to 3790 at v1.40.0 on a measured delta. With the background document reads on main (#1103: the run polling hook, the still-reading states and their copy in seven locales), the key backup step and the plain-http sign-in notice (#1104) and the v1.40 fixes (#1106: the duration reader, the marker unit lock, the chart window) each branch measured 3752 KB gz on its own against the 3749 cap; together they add a few KB more. The four eager routes stay inside their drift allowances (/ +18.4, /insights +16.5, /measurements +14.9, /insights/mood +14.7 KB of 20). The aggregate cap moves from 3749 to 3790, about 24 KB above the combined build, the same headroom as the re-measurements before it. Re-based at v1.40.0 when the quick-entry forms began loading on demand. Custom symptoms added a fifth quick-entry form, and with all five forms bundled eagerly every one of the four eager routes measured over its ceiling (/ 472, /insights 457, /insights/mood 468, /measurements 454 KB gz, each 21 to 25 KB over the baseline). Loading the forms on demand brings them to / 429, /insights 368, /insights/mood 379, /measurements 411: 43 to 90 KB less on first load. The four route baselines move down to those measured values, so the gain is protected by the same 20 KB allowance rather than spent. The aggregate moves the other way, 3774 to 4077, for the reason described at v1.37.27: the aggregate counts each emitted chunk once per route group, and on-demand form chunks land in every route group that can open a quick-entry sheet. Bundling the five forms into one shared on-demand module measured the same (4078), so the split is not the cause. The aggregate cap moves from 3790 to 4100. Bytes on disk rose; bytes a reader downloads fell on every eager route. Raised to 4155 at v1.40.2 on a measured delta. The Coach redesign (the right conversation panel, its row menu and the quick-settings overlay, all loaded on demand), the answer action row, the dashboard's Today overview, the document procedure filter and their copy in seven locales took the whole-build sum from 4080 to 4129 KB gz; most of it is new strings in the seven lazy locale catalogs and the on-demand Coach chunks, which a first paint does not load. The four eager routes stay well inside their drift allowances (/ +1.9, /measurements +1.9, /insights/mood +1.4, /insights +1.0 KB of 20). The aggregate cap moves from 4100 to 4155, about 26 KB above the build, the same headroom as before; per-route baselines are unchanged. Raised to 4182 at v1.42.0 on a measured delta. Main measured 4146 KB gz (lazy catalogs 1177, other chunks 2969); the release branch's shared contract change measured 4158 (lazy catalogs 1188, other chunks 2969): the whole 12 KB is new strings in the seven lazy locale catalogs (the Health Connect import card, air quality and pollen, the managed-profile handover and claim, own vaccines, the lab conversion note, the per-type Apple Health list), and no eager route moved (/ +3.6, /measurements +1.5, /insights/mood +1.1, /insights +0.7 KB of 20). The aggregate cap moves from 4155 to 4182, about 24 KB above the build, the same headroom as before; per-route baselines are unchanged. Re-based at v1.42.0 when the settings sections and Zod left the shared bundles. The settings section route imported all twenty-one sections statically; each now loads on its own, and /settings/[section] measured 611 KB gz before and 403 after. The account-access parse that runs on every boot was the shell's only Zod import, about 63 KB gz on every route; it is plain TypeScript now, held to the published schema by an equivalence test, and the layout entry fell from 199 to 136 KB gz. The canonical unit table moved out of the measurement validation module, which took Zod off the routes whose only reason for it was the display transform: /insights 369 to 304, /medications 428 to 365, /labs 400 to 337. Across the 627 client routes 62 fell, none grew by more than 2 KB, and the per-route sum fell by 1844 KB gz. The /insights baseline moves down to 304 and /settings/[section] joins the watched routes at 403, so both gains are held by the same 20 KB allowance rather than spent. The aggregate moves the other way, 4167 to 4311, for the reason described at v1.37.27 and v1.40.0: the aggregate counts each emitted chunk once, and modules that used to sit in one shared chunk are now emitted into each route group that still needs them (the measurement validation module and the unit table into about twenty-five each, the settings sections into their own chunks). Bytes on disk rose; bytes a reader downloads fell on every route that moved. The aggregate cap moves from 4182 to 4335, about 24 KB above the build, the same headroom as before. Raised to 4400 at v1.42.0 on a measured delta. The release branch after its tickets merge measured 4365 KB gz (lazy catalogs 1206, other chunks 3159), already 30 KB over the 4335 cap; the day view and timeline contract adds 10 more (catalogs 1215, other 3160), almost all of it the base copy for the day, the timeline and life events in seven locales. The aggregate cap moves from 4335 to 4400, about 25 KB above the build, the same headroom as before; per-route baselines are unchanged. Raised to 4470 at v1.42.0 on a measured delta. The release branch with the day view, the timeline and their server measured 4386 KB gz (lazy catalogs 1228, other chunks 3158); giving every chart drawn in days the same doors to its day (one shared hook for the click, the touch tooltip's button, the open day's line, the row of day dots and the caption, a shared calendar adapter, a data table under each newly linked chart) measured 4446 (catalogs 1228, other chunks 3218). The 60 KB is the aggregate's double count again: the hook, the data table and the day link are emitted into each chart's own chunk group rather than once. The eager routes moved by a kilobyte or a few (/ +1.6, /measurements +1.7, /settings/[section] +0.8, /insights/mood +4.6, /insights +1.5 KB) and stay inside their allowances. The aggregate cap moves from 4400 to 4470, about 24 KB above the build, the same headroom as before; per-route baselines are unchanged. Raised to 4760 at v1.42.0 on a measured delta, the other way round from a feature: the day panel and the date field's calendar left the first load. The day view (its scores, sections and calendar) now loads when a day first opens, not with the shell, and the date field loads react-day-picker and the seven date-fns locales when its popover first opens. Every watched route fell: / 460 to 420, /measurements 439 to 398, /insights/mood 438 to 395, /settings/[section] 432 to 387, /insights 360 to 317 KB gz, all five back under their unchanged ceilings. The aggregate rose from 4426 to 4736 for the reason described at v1.37.27 and v1.40.0: while the day view sat in the shell, the small primitives it shares with the pages (card, table, popover, switch, input, badge, the chart day links) were emitted once into the shared chunk; with the day view on demand, each route group that uses them gets its own copy, and the aggregate counts every copy. Bytes on disk rose; bytes a reader downloads fell on every route. The aggregate cap moves from 4470 to 4760, about 24 KB above the build, the same headroom as before; per-route baselines are unchanged.",
"routeBaselineKbGz": {
"/page": 429,
- "/insights/page": 368,
+ "/insights/page": 304,
"/measurements/page": 411,
- "/insights/mood/page": 379
+ "/insights/mood/page": 379,
+ "/settings/[section]/page": 403
},
"routeDriftAllowanceKbGz": 20,
- "totalClientKbGz": 4100,
+ "totalClientKbGz": 4760,
"maxRechartsChunks": 1
}
diff --git a/docker-compose.yml b/docker-compose.yml
index d4022f1a1..eaacd11d4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -72,6 +72,11 @@ x-healthlog-env: &healthlog_env
# (default 60000; 0 disables it and restores the unbounded behaviour).
# Raise it if a very large account's analytics fallback is being cut off.
DATABASE_STATEMENT_TIMEOUT_MS: "${DATABASE_STATEMENT_TIMEOUT_MS:-}"
+ # Postgres work_mem for every app session (default 16MB).
+ DATABASE_WORK_MEM: "${DATABASE_WORK_MEM:-}"
+ # 1 sends neither of the two settings above (they ride the libpq `options`
+ # startup parameter, which PgBouncer refuses by default).
+ DATABASE_SESSION_OPTIONS_DISABLED: "${DATABASE_SESSION_OPTIONS_DISABLED:-}"
WITHINGS_WEBHOOK_SECRET: "${WITHINGS_WEBHOOK_SECRET:-}"
# Overrides the derived `${NEXT_PUBLIC_APP_URL}/api/withings/callback`;
# set it when the URL registered with Withings differs from the app's own
@@ -244,6 +249,11 @@ x-healthlog-env: &healthlog_env
# the coordinate lookups off a third party.
OPENMETEO_BASE_URL: "${OPENMETEO_BASE_URL:-}"
OPENMETEO_GEOCODING_URL: "${OPENMETEO_GEOCODING_URL:-}"
+ # Air quality, pollen and UV (CAMS via Open-Meteo). Same hosted default and
+ # self-host option as above; ENVIRONMENT_AIR_QUALITY_DISABLED=1 stops the
+ # air-quality requests for the whole instance.
+ OPENMETEO_AIR_QUALITY_URL: "${OPENMETEO_AIR_QUALITY_URL:-}"
+ ENVIRONMENT_AIR_QUALITY_DISABLED: "${ENVIRONMENT_AIR_QUALITY_DISABLED:-}"
# OIDC SSO login (self-hosted IdP: Authentik/Keycloak/Authelia/etc).
# All three of ISSUER_URL/CLIENT_ID/CLIENT_SECRET together enable the
# "Sign in with SSO" button; set none to leave it disabled. OIDC_ONLY
@@ -467,13 +477,15 @@ services:
options:
max-size: "10m"
max-file: "3"
- # Conservative memory ceiling for a small host. HEALTHLOG_PROCESS_TYPE=all
- # bundles the AI/rollup worker into this same process, and unbounded that
- # workload can grow large enough under load to starve Postgres of memory
- # on a shared host. 1g is a starting point, not a measured ceiling — raise
- # it with APP_MEMORY_LIMIT in .env if the app gets OOM-killed under real
- # load.
- mem_limit: "${APP_MEMORY_LIMIT:-1g}"
+ # Memory ceiling for the app. HEALTHLOG_PROCESS_TYPE=all bundles the
+ # AI/rollup/backup worker into this same process, and unbounded that
+ # workload can grow large enough to starve Postgres on a shared host.
+ # Node sizes its heap from this limit: 1g gives a 524 MB heap, which a
+ # long-used account fills in normal operation, so garbage collection
+ # stalls requests for seconds and a weekly backup can crash the process.
+ # 1536m gives 792 MB. On a host with 2 GB or less, lower it with
+ # APP_MEMORY_LIMIT in .env; on a busy instance, raise it.
+ mem_limit: "${APP_MEMORY_LIMIT:-1536m}"
healthcheck:
test:
[
@@ -511,7 +523,7 @@ services:
# options:
# max-size: "10m"
# max-file: "3"
- # mem_limit: "${WORKER_MEMORY_LIMIT:-1g}"
+ # mem_limit: "${WORKER_MEMORY_LIMIT:-1536m}"
db:
# Pinned to the concrete minor instead of the floating `16-alpine`
@@ -531,6 +543,21 @@ services:
# (concrete pin, active watch) from being dropped.
image: postgres:16.15-alpine
container_name: healthlog-db
+ # Loads pg_stat_statements so slow queries can be found from measurements
+ # instead of guesses. Loading the library only collects statistics; the
+ # view appears after a one-time `CREATE EXTENSION pg_stat_statements;`
+ # (docs/ops/query-statistics.md). Changing this line restarts the
+ # database once on the next `compose up`.
+ command:
+ - postgres
+ - -c
+ - shared_preload_libraries=pg_stat_statements
+ - -c
+ - pg_stat_statements.track=top
+ # Docker gives a container 64 MB of /dev/shm by default. Postgres keeps
+ # the shared memory of parallel queries and parallel maintenance there,
+ # and a larger plan fails with "could not resize shared memory segment".
+ shm_size: 256mb
# cap_drop: [ALL] is deliberately NOT applied here (unlike `app`): the
# official Postgres entrypoint starts as root, chowns the data
# directory, and drops privileges to the `postgres` user itself, which
diff --git a/docs/CONTRIBUTING-CLIENTS.md b/docs/CONTRIBUTING-CLIENTS.md
index 2f5660022..e2f627e8c 100644
--- a/docs/CONTRIBUTING-CLIENTS.md
+++ b/docs/CONTRIBUTING-CLIENTS.md
@@ -19,7 +19,7 @@ Every JSON route answers the same shape: `{ data, error }`, with `meta` on the e
- Refresh tokens rotate per device and are one-time-use. Reusing an old one revokes that device's whole token family, on purpose. Persist the newest pair atomically and never retry a refresh with a token that already succeeded once.
- Bearer token scopes are fail-closed. A token minted for one purpose does not open unrelated routes, and a route that does not declare a scope refuses narrow tokens outright. Expect `403` to be a normal, recoverable answer, not an anomaly.
- Admin surfaces are cookie-session only, by construction. A native client cannot be an admin client, whatever scopes its token carries. Do not build admin features.
-- Mutations accept an `Idempotency-Key` header. Replays answer with the original result plus `X-Idempotent-Replay`, so retrying on a dropped connection is safe. Use it for anything a user would notice twice.
+- Mutations accept an `Idempotency-Key` header. A repeat of the same key with the same body answers with the original result plus `X-Idempotent-Replay`, so retrying on a dropped connection is safe. A different body under the same key is treated as a new request, not a replay; still, give each distinct write its own key. Use it for anything a user would notice twice.
## Data semantics worth knowing before you sync
diff --git a/docs/README.md b/docs/README.md
index b3e5c1186..dfe00df1a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -34,6 +34,10 @@ notes, and machine-readable specs that ship alongside the source:
managed profiles for somebody with no login and the guardians who look
after them, what revocation does and does not undo, and which trust
properties of the instance are unchanged by any of it.
+- [`using/`](./using/) — notes on using the app that sit close to the
+ code. [`using/search.md`](./using/search.md) covers the command palette's
+ record search: opening it, what it finds, how matching works, and its
+ limits on very large and shared records.
- [`migration/`](./migration/) — migration notes. Currently only
v1.3 to v1.4; later releases are covered by `CHANGELOG.md` and
.
diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index 25431a0bd..1754488a8 100644
--- a/docs/api/openapi.yaml
+++ b/docs/api/openapi.yaml
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
- version: 1.39.9
+ version: 1.42.2
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
@@ -162,12 +162,14 @@ paths:
pattern: ^[A-Za-z0-9_\-:.]{8,128}$
minLength: 8
maxLength: 128
- description: "Makes this write safe to retry. Send the same key again and the first response is replayed rather than the
- work repeated, marked with `X-Idempotent-Replay: true`. A key that does not match the pattern is IGNORED
- rather than refused — the write then proceeds unprotected and looks exactly like a request that carried no
- key, so validate the key on your side. Scoped per user and per (method, path): the same key on a different
- route is a different key. While a first request under this key is still in flight, a second is refused with
- 409 rather than queued."
+ description: "Makes this write safe to retry. Send the same key with the same body again and the first response is
+ replayed rather than the work repeated, marked with `X-Idempotent-Replay: true`. The body is compared as
+ JSON with key order ignored; a different body under the same key is not a retry and runs as a new write,
+ while the first response stays stored for retries of the first request. A key that does not match the
+ pattern is IGNORED rather than refused — the write then proceeds unprotected and looks exactly like a
+ request that carried no key, so validate the key on your side. Scoped per user and per (method, path): the
+ same key on a different route is a different key. While a first request under this key is still in flight, a
+ second with the same body is refused with 409 rather than queued."
requestBody:
required: true
content:
@@ -1850,6 +1852,98 @@ paths:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a4
"429": *a5
+ /api/auth/claim/preview:
+ post:
+ security: []
+ tags:
+ - Auth
+ summary: Preview a managed-profile handover link
+ description: "What a handover link would hand over: the profile's display name, the link's expiry, and each guardian's
+ proposal. Anonymous; the token travels in the body. An unknown, expired, used or withdrawn token, and a link
+ whose creator lost access, all answer the same 404. Rate-limited per source address."
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ProfileClaimPreviewRequest"
+ responses:
+ "200":
+ description: The preview.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ProfileClaimPreviewEnvelope"
+ "401": *a1
+ "403":
+ description: "The instance allows single sign-on only (`meta.errorCode: profile_claim.oidc_only_unsupported`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "404":
+ description: "No usable link behind this token (`meta.errorCode: profile_claim.invalid`). Every failure class answers
+ identically."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "409":
+ description: "The request carries a signed-in session (`meta.errorCode: auth.already_authenticated`); sign out first."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
+ /api/auth/claim:
+ post:
+ security: []
+ tags:
+ - Auth
+ summary: Claim a managed profile
+ description: "Turns the managed profile behind a handover link into the claimant's own account, in one transaction: sets
+ the username, email and password on the profile's record, clears the managed marker, withdraws pending
+ invitations, applies each guardian's proposed access, and resets the disclaimer, onboarding and AI consent so
+ the new owner gives them personally. The new owner then decides each guardian's access on first sign-in
+ (`/api/account/handover-decision`). No data moves. Signs the new owner in. Anonymous; the token travels in the
+ body, every token failure answers the same 404, and five attempts per fifteen minutes per source address are
+ allowed."
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ProfileClaimRequest"
+ responses:
+ "201":
+ description: Claimed; the response sets the session cookie.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ProfileClaimEnvelope"
+ "401": *a1
+ "403":
+ description: "The instance allows single sign-on only (`meta.errorCode: profile_claim.oidc_only_unsupported`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "404":
+ description: "No usable link behind this token (`meta.errorCode: profile_claim.invalid`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "409":
+ description: "The request carries a signed-in session (`meta.errorCode: auth.already_authenticated`), or the username or
+ email is taken (`meta.errorCode: profile_claim.taken`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
/api/auth/login:
post:
security: []
@@ -2550,7 +2644,15 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/PasskeyLoginVerifyResponse"
- "401": *a1
+ "401":
+ description: The sign-in did not prove anything (v1.42.0 codes; before, the first case answered 500). `meta.errorCode` =
+ `passkey.challenge.expired` when the challenge is gone — it expired after five minutes, was already used, or
+ was begun for a different account — so start a new sign-in; `passkey.verification.failed` when the assertion
+ was checked and refused (signature, origin, relying-party id or challenge mismatch).
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
"403":
description: Passkey login is disabled — the operator runs `OIDC_ONLY=true`. `meta.errorCode` = `oidc_only`.
content:
@@ -2558,12 +2660,20 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"404":
- description: The assertion resolved to a user row that no longer exists.
+ description: "`meta.errorCode` = `passkey.unknown` (v1.42.0): no passkey with this credential id is registered on this
+ server — it was removed here, or belongs to another server. Tell the person to sign in another way. Without
+ a code: the assertion resolved to a user row that no longer exists."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422":
+ description: "`challengeId` or `credential` is missing, or (`meta.errorCode` = `passkey.response.invalid`, v1.42.0)
+ `credential` is not a WebAuthn authentication response."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "422": *a4
"429": *a5
/api/auth/refresh:
post:
@@ -4142,6 +4252,7 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
- in: query
name: valueMin
schema:
@@ -4396,7 +4507,12 @@ paths:
(`measurement.batch.invalid`, with every issue under `details.issues`), or
(`measurement.batch.source_not_permitted`) carried an entry naming a source, any source, under a narrow
`measurements:write` credential. Nothing was written in any of the three cases, and all three are permanent
- for the batch as sent: resending it unchanged is refused the same way."
+ for the batch as sent: resending it unchanged is refused the same way. Which issues belong to an entry:
+ every issue caused by one entry has a `path` that starts `entries.` (the entry's index in the request,
+ then the field, e.g. `entries.3.startDate`, or just `entries.4` when the entry is not an object). Those are
+ the only per-entry issues, so a client may drop the named entries and resend the rest. An issue whose `path`
+ is `\"\"` (the body is not an object), `entries` (missing, not an array, or empty) or `syncTrigger` is about
+ the batch itself, and no split recovers it."
content:
application/json:
schema:
@@ -5030,11 +5146,14 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422":
- description: Body validation failed (multi-issue envelope, breadcrumbed as
+ description: "Body validation failed (multi-issue envelope, breadcrumbed as
`medications.intake.update.validation-failed`), or `meta.errorCode` =
`medications.intake.injection_site.disallowed` when the site is outside the medication's effective allowed
set, or `medications.intake.force_slot.invalid` when `forceSlotInstant` is not a real slot of the
- medication. The latter two carry no issue list — branch on the code.
+ medication, or (v1.42.0) `medication.intake.notTracked` when the event is still open and its medication is
+ kept as a record only (`trackIntake: false`): resolving or snoozing it would record a new dose that is
+ counted nowhere. Correcting an event that is already taken or skipped stays open. The coded refusals carry
+ no issue list — branch on the code."
content:
application/json:
schema:
@@ -6296,7 +6415,16 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"409": *a7
- "422": *a4
+ "422":
+ description: "Body validation failed (multi-issue envelope), or `meta.errorCode` names the refusal:
+ `medications.intake.injection_site.disallowed`, `medications.intake.force_slot.invalid`, or (v1.42.0)
+ `medication.intake.notTracked` when the medication is kept as a record only (`trackIntake: false`). Its
+ stored history stays; a new dose is refused rather than stored and counted nowhere. The check runs after
+ ownership, so a foreign medication is still a 404."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
"429": *a12
get:
tags:
@@ -6620,13 +6748,36 @@ paths:
tags:
- Medications
summary: Batched adherence read for every medication of the caller
- description: Returns one compact row per non-PRN medication. A scheduled medication with no local schedule remains in
- the array with applicable=false and reason NO_LOCAL_SCHEDULE. compliance7/compliance30 stay non-null as all-zero
+ description: >-
+ Returns one compact row per non-PRN medication. A scheduled medication with no local schedule remains in the
+ array with applicable=false and reason NO_LOCAL_SCHEDULE. compliance7/compliance30 stay non-null as all-zero
compatibility placeholders for released clients; aware clients must ignore those percentages when
applicable=false. complianceDisplay is null.
+
+
+ v1.42.0, additive: `meta.aggregate` carries one figure for the whole account (`MedicationComplianceAggregate`,
+ weighted by expected doses across the applicable medications; `null` when none applies), so a client never
+ averages the rows itself. `days` (30, 60, 90, 180 or 365) adds a `complianceN` block in the same shape to every
+ row and to the aggregate, for a report that covers its own window; `meta.days` echoes it. Any other `days` is a
+ 422 rather than a rounded window.
+ parameters:
+ - in: query
+ name: days
+ schema:
+ description: Report window in days. 30 is already `compliance30`; the others add `compliance60` / `compliance90` /
+ `compliance180` / `compliance365`.
+ type: string
+ enum:
+ - "30"
+ - "60"
+ - "90"
+ - "180"
+ - "365"
+ description: Report window in days. 30 is already `compliance30`; the others add `compliance60` / `compliance90` /
+ `compliance180` / `compliance365`.
responses:
"200":
- description: One adherence row per medication.
+ description: One adherence row per medication, and the account aggregate in `meta`.
content:
application/json:
schema:
@@ -7576,7 +7727,7 @@ paths:
type: object
properties:
steps:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7596,8 +7747,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
activeEnergy:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7617,8 +7769,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
walkingRunningDistance:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7638,8 +7791,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
flightsClimbed:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7659,8 +7813,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
sleep:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7680,8 +7835,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
weight:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7701,8 +7857,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
bloodPressure:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7722,8 +7879,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
pulse:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7743,8 +7901,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
bodyFat:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7764,8 +7923,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
bodyTemperature:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7785,8 +7945,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
spo2:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7806,8 +7967,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
hrv:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7827,8 +7989,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
restingHeartRate:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7848,8 +8011,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
vo2Max:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7869,8 +8033,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
skinTemperature:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7890,8 +8055,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
respiratoryRate:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7911,8 +8077,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
recovery:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7932,8 +8099,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
stress:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7953,8 +8121,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
sleepDebt:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7974,11 +8143,34 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
+ hrvRmssd:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
metricPriority:
type: object
properties:
steps:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -7998,8 +8190,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
activeEnergy:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8019,8 +8212,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
walkingRunningDistance:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8040,8 +8234,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
flightsClimbed:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8061,8 +8256,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
sleep:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8082,8 +8278,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
weight:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8103,8 +8300,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
bloodPressure:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8124,8 +8322,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
pulse:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8145,8 +8344,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
bodyFat:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8166,8 +8366,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
bodyTemperature:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8187,8 +8388,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
spo2:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8208,8 +8410,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
hrv:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8229,8 +8432,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
restingHeartRate:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8250,8 +8454,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
vo2Max:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8271,8 +8476,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
skinTemperature:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8292,8 +8498,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
respiratoryRate:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8313,8 +8520,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
recovery:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8334,8 +8542,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
stress:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8355,8 +8564,9 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
sleepDebt:
- maxItems: 8
+ maxItems: 16
type: array
items:
type: string
@@ -8376,6 +8586,29 @@ paths:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
+ hrvRmssd:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
deviceTypePriority:
type: object
properties:
@@ -8590,9 +8823,10 @@ paths:
summary: Update per-user notification preferences
description: 'Deep-merges the supplied partial shape over the persisted row — a PATCH touching only one category leaves
the siblings intact. Always returns the fully-resolved next state so clients can hard-set their optimistic
- update. `medication.clientManaged: true` (or `deliveryDefault: "client"`) suppresses server-side
- MEDICATION_REMINDER APNs; `medication.lowStockRunwayDays` (1–60, nullable, default 7) tunes the low-stock alert
- — null switches it off. Rate-limit 60/min per user.'
+ update. `medication.clientManaged: true` (or `deliveryDefault: "client"`) keeps the MEDICATION_REMINDER that
+ announces a dose off APNs and leaves the follow-ups for a dose still open on it (v1.42.0);
+ `medication.lowStockRunwayDays` (1–60, nullable, default 7) tunes the low-stock alert — null switches it off.
+ Rate-limit 60/min per user.'
requestBody:
required: true
content:
@@ -9427,6 +9661,49 @@ paths:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a4
"429": *a5
+ /api/insights/chat/{id}/messages/{messageId}/trail:
+ get:
+ tags:
+ - Insights
+ summary: Read the trail text of one Coach message
+ description: "v1.41 — the text of one assistant message's trail: the screened reasoning titles and summaries by trail
+ entry (model-written), the fact texts the turn recalled, and a pending fact proposal (the person's own data).
+ The trail's structure (phases, statuses, counts) is plaintext on the message's `metricSource.activity`; this
+ route serves only the text, decrypted for the account that owns the conversation. Fetch lazily when the person
+ opens the trail. A mixed read: while the Coach's text may not be shown for the record, `entries` is empty and
+ the `ai` state rides beside it, but `recalled` and `proposal` are still served, since they are the person's
+ data. While the medications module is off, recalled facts and a proposal that concern a medication are left out.
+ `trail` is null when the message has no trail or nothing in it may be served. A foreign or unknown conversation
+ or message id maps to 404 (never 403). Auth via cookie or a full-access Bearer token."
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Conversation id.
+ - name: messageId
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Assistant message id within the conversation.
+ responses:
+ "200":
+ description: The trail text, or null when there is none or nothing in it may be served.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CoachMessageTrailResponse"
+ "401": *a1
+ "404":
+ description: Conversation or message not found, or not owned by the caller.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
/api/insights/chat/{id}:
get:
tags:
@@ -9766,11 +10043,12 @@ paths:
tags:
- Insights
summary: List the caller's durable Coach facts
- description: "v1.11.1 — returns the active facts the Coach has extracted about the caller (highest-confidence then
- newest first), each decrypted on the fly. The GDPR 'what do you know about me' surface. Never AI-gated: the
- facts stay readable and erasable while the Coach is unavailable for any reason. Auth via cookie or Bearer; the
- owner is always narrowed from the session, never the body. Undecryptable rows are omitted rather than failing
- the read."
+ description: "v1.11.1 — returns the active facts the Coach keeps about the caller (highest-confidence then newest
+ first), each decrypted on the fly. The GDPR 'what do you know about me' surface. v1.41 — each fact carries its
+ source, the message it came from and when it was last used in a turn; a health fact still waiting for the
+ caller's confirmation is not listed. Never AI-gated: the facts stay readable and erasable while the Coach is
+ unavailable for any reason. Auth via cookie or Bearer; the owner is always narrowed from the session, never the
+ body. Undecryptable rows are omitted rather than failing the read."
responses:
"200":
description: The caller's active facts.
@@ -9781,6 +10059,51 @@ paths:
"401": *a1
"422": *a4
"429": *a5
+ post:
+ tags:
+ - Insights
+ summary: Remember one of the caller's own Coach messages
+ description: "v1.41 — the remember button. Saves the named user message as a fact with `source: \"user\"`: the text is
+ the message the server stored, filed as `medication` or `condition` when it names one, `context` otherwise. The
+ tap is the caller's confirmation, so a health message is saved too. A message the caller does not own, or an
+ assistant message, is a 404. Saving the same fact again returns the existing one with `created: false`. Gated on
+ the Coach module, never on AI. Auth via cookie or Bearer."
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CoachFactCreateRequest"
+ responses:
+ "200":
+ description: The caller's memory already held this fact.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CoachFactAlreadyKnown"
+ "201":
+ description: The fact was saved.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CoachFactCreated"
+ "401": *a1
+ "403":
+ description: 'Module disabled for this account: the user (or operator) has the module turned off. `meta.errorCode` =
+ `module.disabled` and `meta.module` carries the disabled module key (e.g. "sleep"). Returned even for a
+ valid Bearer token. Clients hide the whole module surface end-to-end rather than retry.'
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "404":
+ description: Message not found or not one of the caller's own.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
delete:
tags:
- Insights
@@ -9798,6 +10121,44 @@ paths:
"422": *a4
"429": *a5
/api/insights/coach/facts/{id}:
+ patch:
+ tags:
+ - Insights
+ summary: Edit one Coach fact's wording
+ description: "v1.41 — rewrites the text of a fact the caller owns (re-encrypted at rest) and files it by its new
+ wording: text that names a medication moves to `medication`, so the medications module keeps filtering it; other
+ health wording moves a non-health category to `condition`; otherwise the category stays. A health fact the
+ caller edited counts as confirmed (`source: \"user\"`). A proposal still waiting for the caller's confirmation,
+ an unknown, cross-user or deleted id are all a 404. Never AI-gated. Auth via cookie or Bearer."
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Fact id.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CoachFactPatchRequest"
+ responses:
+ "200":
+ description: The fact after the edit.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CoachFactUpdated"
+ "401": *a1
+ "404":
+ description: Fact not found or not owned by the caller.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
delete:
tags:
- Insights
@@ -10738,6 +11099,60 @@ paths:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a4
"429": *a5
+ /api/admin/maintenance/measurements:
+ get:
+ tags:
+ - Admin
+ summary: Where measurement table maintenance stands
+ description: "The newest maintenance run and its state, whether the compaction-tombstone purge is still at work, and the
+ table's size right now. The admin card polls it while a run is queued or running. Cookie auth only:
+ `requireAdmin()` refuses every Bearer caller."
+ responses:
+ "200":
+ description: The status.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AdminMeasurementMaintenanceStatusResponse"
+ "401": *a1
+ "422": *a4
+ "429": *a5
+ post:
+ tags:
+ - Admin
+ summary: Queue measurement table maintenance
+ description: "Queues one `VACUUM (ANALYZE)` and `REINDEX INDEX CONCURRENTLY` pass over the `measurements` table, for
+ after a large purge. Runs in the background worker, one statement at a time; pick a quiet window, because a
+ rebuild needs free disk the size of the index it rebuilds. At most one run at a time. Cookie auth only:
+ `requireAdmin()` refuses every Bearer caller. Runbook: `docs/ops/measurement-maintenance.md`."
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AdminMeasurementMaintenanceRequest"
+ responses:
+ "202":
+ description: Queued, or already queued.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/AdminMeasurementMaintenanceResponse"
+ "401": *a1
+ "403":
+ description: Caller is not an admin.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
+ "503":
+ description: No background worker is bound, so nothing would run it.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
/api/admin/notifications/reminder-check:
post:
tags:
@@ -11209,30 +11624,6 @@ paths:
"403": *a11
"422": *a4
"429": *a5
- /api/insights/coach/seeded-question:
- get:
- tags:
- - Insights
- summary: Today's suggested Coach opener
- description: "Resolves today's single most notable derived wellness signal into a tappable opener for the Coach's
- blank-chat hero, using the same confidence-gated detector the daily briefing uses. The selection happens
- SERVER-side and the client renders it rather than recomputing. `signal: null` is the ordinary answer and means
- the hero keeps its neutral greeting — never a fabricated opener. Two different causes produce that null and are
- indistinguishable on the wire: nothing crossed the notability gate, or the account has turned proactive
- suggestions off, in which case the detector does not run at all. The opener exists only to open the Coach, so it
- is served only while the `coach` AI capability is available; otherwise the answer is the same neutral `signal:
- null` and `ai` says why. Never refused for an AI reason. Shared analytics-read budget. Cookie or Bearer auth;
- not delegable."
- responses:
- "200":
- description: The opener, or the neutral null.
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/CoachSeededQuestionEnvelope"
- "401": *a1
- "422": *a4
- "429": *a5
/api/insights/narrative:
get:
tags:
@@ -11955,6 +12346,56 @@ paths:
"403": *a11
"422": *a4
"429": *a5
+ /api/insights/score-history:
+ get:
+ tags:
+ - Insights
+ summary: A score's daily history
+ description: "v1.42 — the daily course of the health score, readiness or the sleep score over a trailing window, one
+ point per local day, for the history chart on the score's page. Each point is the value the day view shows for
+ that day: the stored health-score day, the nightly readiness blend filed on its wake day, the sleep score of the
+ night that ended that morning. The health score's points carry `seamBreak` where the recipe changed, and the
+ chart draws no line across it. `band` is the usual range behind the newest point. Readiness is gated on the
+ `recovery` module and the sleep score on `sleep` (403 `module.disabled`); the health score is core. Pure reads,
+ no AI. Delegable at MANAGE level over the whole record. Additive. Cookie or Bearer auth."
+ parameters:
+ - in: query
+ name: score
+ schema:
+ type: string
+ enum:
+ - HEALTH_SCORE
+ - READINESS
+ - SLEEP_SCORE
+ description: The score to chart. `HEALTH_SCORE` reads the stored day scores, `READINESS` the nightly readiness blend,
+ `SLEEP_SCORE` the per-night sleep score. The nightly recovery, stress and strain scores are stored
+ measurements and chart through the measurement series instead.
+ required: true
+ description: The score to chart. `HEALTH_SCORE` reads the stored day scores, `READINESS` the nightly readiness blend,
+ `SLEEP_SCORE` the per-night sleep score. The nightly recovery, stress and strain scores are stored
+ measurements and chart through the measurement series instead.
+ - in: query
+ name: days
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 3650
+ description: Trailing window in local calendar days ending today (1–3650). The web chart sends 7, 30, 90, or 3650 for
+ its "All" tab. Out of range is a 422, never a silent clamp.
+ required: true
+ description: Trailing window in local calendar days ending today (1–3650). The web chart sends 7, 30, 90, or 3650 for
+ its "All" tab. Out of range is a 422, never a silent clamp.
+ responses:
+ "200":
+ description: The score's daily points and usual range.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ScoreHistoryResponseEnvelope"
+ "401": *a1
+ "403": *a11
+ "422": *a4
+ "429": *a5
/api/insights/glp1-plateau:
get:
tags:
@@ -12272,7 +12713,20 @@ paths:
window the prior aggregate is served immediately while one background recompute warms a new one, so an active
logger never re-pays the cold compute. Module-gated on `mood`; a disabled module answers 403 `module.disabled`
even for a valid Bearer token. Cookie or Bearer auth; the caller is always resolved as themselves, so this read
- cannot be delegated to a shared record — which makes it the one mood surface a delegate cannot reach."
+ cannot be delegated to a shared record — which makes it the one mood surface a delegate cannot reach. v1.42.0 —
+ `days` cuts `stability` to the period a client shows; every other block is unchanged by it."
+ parameters:
+ - in: query
+ name: days
+ schema:
+ description: "Trailing window for `stability`, in days. Omitted: 365. Any other value is a 422."
+ type: string
+ enum:
+ - "30"
+ - "90"
+ - "180"
+ - "365"
+ description: "Trailing window for `stability`, in days. Omitted: 365. Any other value is a 422."
responses:
"200":
description: The aggregate bundle.
@@ -12649,6 +13103,7 @@ paths:
- WEB
- TELEGRAM
- DAYLIO
+ - NOTIFICATION
- in: query
name: from
schema:
@@ -13751,6 +14206,86 @@ paths:
"401": *a1
"422": *a4
"429": *a5
+ /api/import/health-connect-export:
+ post:
+ tags:
+ - Import
+ summary: Upload a Health Connect export for background import
+ description: "Takes the ZIP the Android Health Connect app writes (it holds `health_connect_export.db`) and hands it to
+ a background worker. The response is **202 with a `jobId`**; poll `GET
+ /api/import/health-connect-export/status`. Body is `multipart/form-data` with the file under `file`, streamed to
+ disk after authentication like the Apple Health upload. Re-uploading the same bytes resolves to the still-viable
+ job and returns `idempotent: true`. Imported rows carry `source = HEALTH_CONNECT`. Cookie or wildcard Bearer."
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ file:
+ type: string
+ description: The Health Connect export ZIP, under the field name `file`.
+ required:
+ - file
+ responses:
+ "202":
+ description: Accepted and queued, or matched to a job already carrying these bytes.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HealthConnectImportKickoffEnvelope"
+ "400":
+ description: The request carried no body at all.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "401": *a1
+ "409":
+ description: An import for this account (Health Connect or Apple Health) is already queued or running. The upload was
+ discarded. `errorCode` is `import.apple_health.busy` for either kind, `jobId` names the running job.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "413":
+ description: The declared `Content-Length` exceeds the 1 GiB upload cap.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422":
+ description: "The multipart body could not be read: no `file` part, a malformed body, or more bytes than the cap.
+ Whether the ZIP is a readable Health Connect export is decided by the worker and reported on the job
+ (`status: failed`, `failureReason`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "429": *a5
+ "503":
+ description: No background worker is bound, or the queue did not take the job, so nothing would run the import.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ /api/import/health-connect-export/status:
+ get:
+ tags:
+ - Import
+ summary: The caller's latest Health Connect import
+ description: The most recent Health Connect import job of the caller, or `null` when there has never been one.
+ responses:
+ "200":
+ description: The latest job, or null.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HealthConnectImportStatusEnvelope"
+ "401": *a1
+ "422": *a4
+ "429": *a5
/api/import/csv:
post:
tags:
@@ -14546,7 +15081,7 @@ paths:
schema:
$ref: "#/components/schemas/OcrTextExtractRequest"
responses:
- "202": &a36
+ "202": &a38
description: Queued for the background worker. Every refusal the route can answer quickly (capability, provider, rate
limit, budget, validation) has already been answered; anything that goes wrong from here is reported on the
run.
@@ -15303,6 +15838,118 @@ paths:
"404": *a31
"422": *a4
"429": *a5
+ /api/vaccinations/custom:
+ get:
+ tags:
+ - Records
+ summary: List the record's own vaccines
+ description: The vaccine definitions the record's owner added for products the shipped catalogue does not list, by name.
+ Removed definitions are excluded. Readable under a `profile` grant like the dose list.
+ responses:
+ "200":
+ description: The definitions, by name.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ListCustomVaccinesEnvelope"
+ "401": *a1
+ "403": *a11
+ "422": *a4
+ "429": *a5
+ post:
+ tags:
+ - Records
+ summary: Add an own vaccine
+ description: Defines a vaccine in the same shape a catalogue entry resolves to, so the series and booster logic treat
+ both alike. Names are unique per record, compared without case; adding a name the person removed earlier brings
+ that definition back with the new values (its doses were let go on removal and stay so). Audits as
+ `vaccination.custom.create`.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateCustomVaccineRequest"
+ responses:
+ "201":
+ description: Created.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateCustomVaccineEnvelope"
+ "401": *a1
+ "403": *a11
+ "409": &a32
+ description: A live definition on the record already carries this name, compared without case
+ (`vaccination.custom.name-taken`).
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a12
+ /api/vaccinations/custom/{id}:
+ patch:
+ tags:
+ - Records
+ summary: Edit an own vaccine
+ description: Changes one definition; every dose logged against it reads the change. Owner-scoped; an unknown or removed
+ id 404s. Audits as `vaccination.custom.update`, naming a rename without quoting it.
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: string
+ required: true
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UpdateCustomVaccineRequest"
+ responses:
+ "200":
+ description: Updated.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UpdateCustomVaccineEnvelope"
+ "401": *a1
+ "403": *a11
+ "404": &a33
+ description: No live definition with this id on the record (`vaccination.custom.not-found`).
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "409": *a32
+ "422": *a4
+ "429": *a5
+ delete:
+ tags:
+ - Records
+ summary: Remove an own vaccine
+ description: "Soft-deletes one definition and lets go of every dose that named it. A dose keeps a name: one without
+ wording of its own takes the definition's name as its `vaccineName` first. Removing twice succeeds. Audits as
+ `vaccination.custom.delete` with the number of doses let go."
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: string
+ required: true
+ responses:
+ "200":
+ description: Removed.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteCustomVaccineEnvelope"
+ "401": *a1
+ "403": *a11
+ "404": *a33
+ "422": *a4
+ "429": *a5
/api/vaccinations:
get:
tags:
@@ -15371,6 +16018,13 @@ paths:
$ref: "#/components/schemas/CreateVaccinationEnvelope"
"401": *a1
"403": *a11
+ "404":
+ description: A practice, visit or own vaccine the body names is not the record's (`vaccination.practitioner-not-found`,
+ `vaccination.encounter-not-found`, `vaccination.custom-vaccine-not-found`).
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
"409": *a7
"422": *a4
"429": *a12
@@ -15395,7 +16049,7 @@ paths:
$ref: "#/components/schemas/GetVaccinationEnvelope"
"401": *a1
"403": *a11
- "404": &a32
+ "404": &a34
description: Vaccination not found (or owned by another user).
content:
application/json:
@@ -15430,7 +16084,7 @@ paths:
$ref: "#/components/schemas/UpdateVaccinationEnvelope"
"401": *a1
"403": *a11
- "404": *a32
+ "404": *a34
"422": *a4
"429": *a5
delete:
@@ -15455,7 +16109,7 @@ paths:
$ref: "#/components/schemas/DeleteVaccinationEnvelope"
"401": *a1
"403": *a11
- "404": *a32
+ "404": *a34
"422": *a4
"429": *a5
/api/vaccinations/{id}/restore:
@@ -15480,7 +16134,7 @@ paths:
$ref: "#/components/schemas/RestoreVaccinationEnvelope"
"401": *a1
"403": *a11
- "404": *a32
+ "404": *a34
"422": *a4
"429": *a5
/api/vaccinations/{id}/booster:
@@ -15546,7 +16200,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "404": *a32
+ "404": *a34
"422": *a4
"429": *a5
/api/vaccinations/suggest:
@@ -15604,7 +16258,7 @@ paths:
schema:
$ref: "#/components/schemas/LinkVaccinationEnvelope"
"401": *a1
- "404": *a32
+ "404": *a34
"422": *a4
"429": *a5
delete:
@@ -15633,7 +16287,7 @@ paths:
schema:
$ref: "#/components/schemas/UnlinkVaccinationEnvelope"
"401": *a1
- "404": *a32
+ "404": *a34
"422": *a4
"429": *a5
/api/illness/episodes:
@@ -15717,7 +16371,7 @@ paths:
$ref: "#/components/schemas/GetIllnessEpisodeEnvelope"
"401": *a1
"403": *a11
- "404": &a33
+ "404": &a35
description: Illness episode not found (or owned by another user).
content:
application/json:
@@ -15752,7 +16406,7 @@ paths:
$ref: "#/components/schemas/UpdateIllnessEpisodeEnvelope"
"401": *a1
"403": *a11
- "404": *a33
+ "404": *a35
"422": *a4
"429": *a5
delete:
@@ -15776,7 +16430,7 @@ paths:
$ref: "#/components/schemas/DeleteIllnessEpisodeEnvelope"
"401": *a1
"403": *a11
- "404": *a33
+ "404": *a35
"422": *a4
"429": *a5
/api/illness/episodes/{id}/restore:
@@ -15804,7 +16458,7 @@ paths:
$ref: "#/components/schemas/RestoreIllnessEpisodeEnvelope"
"401": *a1
"403": *a11
- "404": *a33
+ "404": *a35
"409": *a7
"422": *a4
"429": *a5
@@ -15837,7 +16491,7 @@ paths:
$ref: "#/components/schemas/ResolveIllnessEpisodeEnvelope"
"401": *a1
"403": *a11
- "404": *a33
+ "404": *a35
"422": *a4
"429": *a5
/api/illness/episodes/{id}/day-logs:
@@ -15888,7 +16542,7 @@ paths:
$ref: "#/components/schemas/GetIllnessDayLogEnvelope"
"401": *a1
"403": *a11
- "404": *a33
+ "404": *a35
"422": *a4
"429": *a5
post:
@@ -15925,7 +16579,7 @@ paths:
$ref: "#/components/schemas/UpsertIllnessDayLogCreatedEnvelope"
"401": *a1
"403": *a11
- "404": *a33
+ "404": *a35
"422": *a4
"429": *a5
/api/illness/episodes/{id}/correlation:
@@ -15952,7 +16606,7 @@ paths:
schema:
$ref: "#/components/schemas/IllnessCorrelationEnvelope"
"401": *a1
- "404": *a33
+ "404": *a35
"422": *a4
"429": *a5
/api/illness/insights:
@@ -16168,7 +16822,7 @@ paths:
$ref: "#/components/schemas/GetAllergyEnvelope"
"401": *a1
"403": *a11
- "404": &a34
+ "404": &a36
description: Allergy not found (or owned by another user).
content:
application/json:
@@ -16202,7 +16856,7 @@ paths:
$ref: "#/components/schemas/UpdateAllergyEnvelope"
"401": *a1
"403": *a11
- "404": *a34
+ "404": *a36
"422": *a4
"429": *a5
delete:
@@ -16226,7 +16880,7 @@ paths:
$ref: "#/components/schemas/DeleteAllergyEnvelope"
"401": *a1
"403": *a11
- "404": *a34
+ "404": *a36
"422": *a4
"429": *a5
/api/family-history:
@@ -16300,7 +16954,7 @@ paths:
$ref: "#/components/schemas/GetFamilyHistoryEnvelope"
"401": *a1
"403": *a11
- "404": &a35
+ "404": &a37
description: Family history entry not found (or owned by another user).
content:
application/json:
@@ -16334,7 +16988,7 @@ paths:
$ref: "#/components/schemas/UpdateFamilyHistoryEnvelope"
"401": *a1
"403": *a11
- "404": *a35
+ "404": *a37
"422": *a4
"429": *a5
delete:
@@ -16358,7 +17012,7 @@ paths:
$ref: "#/components/schemas/DeleteFamilyHistoryEnvelope"
"401": *a1
"403": *a11
- "404": *a35
+ "404": *a37
"422": *a4
"429": *a5
/api/documents/inbound:
@@ -16408,7 +17062,8 @@ paths:
required: false
schema:
type: string
- description: Only documents linked to this visit.
+ description: Only documents linked to this visit. The procedure filter sends a `linkedProcedures` id from the usage
+ read; an id that is not one of the caller's visits matches nothing.
- name: year
in: query
required: false
@@ -16742,8 +17397,11 @@ paths:
summary: Storage usage + effective limits
description: "The caller's vault usage and effective limits: `usedBytes` (every non-purged row — tombstones inside the
30-day undo grace still count), `quotaBytes` (per-user override ?? instance default), `maxFileBytes`
- (admin-tunable per-file cap), `acceptedExtensions` for picker `accept` lists, and `linkedEpisodes` (episodes
- carrying at least one live document link — the condition-filter chips). Read this before offering an upload."
+ (admin-tunable per-file cap), `acceptedExtensions` for picker `accept` lists, `linkedEpisodes` (episodes
+ carrying at least one live document link — the condition-filter chips), and `linkedProcedures` (visits of kind
+ PROCEDURE carrying at least one live document link, newest first — the procedure-filter choices; pass one's
+ `encounterId` to the list's `encounterId` filter). `linkedProcedures` is empty when a sharing grant does not
+ cover the visits section. Read this before offering an upload."
responses:
"200":
description: Usage + limits.
@@ -16755,6 +17413,46 @@ paths:
"403": *a11
"422": *a4
"429": *a5
+ /api/documents/inbound/layout:
+ get:
+ tags:
+ - Documents
+ summary: Get the vault presentation
+ description: The resolved card/list view and month arrangement for the vault, with defaults filled in when nothing has
+ been chosen. Inside a shared record this answers the owner's choice.
+ responses:
+ "200":
+ description: The resolved presentation.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DocumentsLayoutEnvelope"
+ "401": *a1
+ "403": *a11
+ "422": *a4
+ "429": *a5
+ put:
+ tags:
+ - Documents
+ summary: Change the vault presentation
+ description: "Preserve-when-absent: a body carrying only `view` keeps the stored arrangement and vice versa. Returns the
+ full resolved presentation. Refused while acting inside somebody else's record."
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DocumentsLayoutPutBody"
+ responses:
+ "200":
+ description: The presentation as stored.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DocumentsLayoutSavedEnvelope"
+ "401": *a1
+ "422": *a4
+ "429": *a5
/api/documents/inbound/bulk:
post:
tags:
@@ -16979,7 +17677,7 @@ paths:
required: true
schema:
type: string
- - &a37
+ - &a39
name: Prefer
in: header
required: false
@@ -17004,9 +17702,9 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/InboundExtractEnvelope"
- "202": *a36
+ "202": *a38
"401": *a1
- "403": &a38
+ "403": &a40
description: "AI refusal: the `documentAi` capability is unavailable for this record. The envelope carries
`meta.errorCode`, `meta.capability` and `meta.reason` (one of `AiUnavailableReason`).
`assistant.disabled.documentAi` — the operator turned off reading documents, or every AI feature (the master
@@ -17030,7 +17728,7 @@ paths:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a4
"429": *a5
- "503": &a39
+ "503": &a41
description: "`ai.unavailable` — the inputs behind the AI capability could not be read, and the answer failed closed.
Nothing was sent. Retry later."
content:
@@ -17060,7 +17758,7 @@ paths:
required: true
schema:
type: string
- - *a37
+ - *a39
requestBody:
required: false
content:
@@ -17096,12 +17794,12 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/DocumentSuggestEnvelope"
- "202": *a36
+ "202": *a38
"401": *a1
- "403": *a38
+ "403": *a40
"422": *a4
"429": *a5
- "503": *a39
+ "503": *a41
/api/documents/inbound/{id}/summary:
post:
tags:
@@ -17151,7 +17849,7 @@ paths:
type: boolean
default: false
description: With `persist=true`, explicitly replace an existing stored summary.
- - *a37
+ - *a39
requestBody:
required: false
content:
@@ -17187,12 +17885,12 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/DocumentSummaryEnvelope"
- "202": *a36
+ "202": *a38
"401": *a1
- "403": *a38
+ "403": *a40
"422": *a4
"429": *a5
- "503": *a39
+ "503": *a41
/api/documents/inbound/{id}/index:
post:
tags:
@@ -17218,7 +17916,7 @@ paths:
required: true
schema:
type: string
- - *a37
+ - *a39
requestBody:
required: false
content:
@@ -17254,12 +17952,12 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/DocumentIndexEnvelope"
- "202": *a36
+ "202": *a38
"401": *a1
- "403": *a38
+ "403": *a40
"422": *a4
"429": *a5
- "503": *a39
+ "503": *a41
/api/documents/inbound/{id}/chat:
post:
tags:
@@ -17299,10 +17997,10 @@ paths:
type: string
description: "SSE frames: `data: \\n\\n`. See the operation description for the per-`type` frame shapes."
"401": *a1
- "403": *a38
+ "403": *a40
"422": *a4
"429": *a5
- "503": *a39
+ "503": *a41
get:
tags:
- Documents
@@ -17365,10 +18063,10 @@ paths:
schema:
$ref: "#/components/schemas/DocumentReindexEnvelope"
"401": *a1
- "403": *a38
+ "403": *a40
"422": *a4
"429": *a5
- "503": *a39
+ "503": *a41
/api/documents/inbound/{id}/facts/{factId}:
patch:
tags:
@@ -17475,7 +18173,7 @@ paths:
schema:
$ref: "#/components/schemas/CreateCustomMetricResponse"
"401": *a1
- "409": &a41
+ "409": &a43
description: A custom metric with this name already exists for the caller.
content:
application/json:
@@ -17504,7 +18202,7 @@ paths:
$ref: "#/components/schemas/GetCustomMetricResponse"
"401": *a1
"403": *a11
- "404": &a40
+ "404": &a42
description: Custom metric not found (or owned by another user).
content:
application/json:
@@ -17538,8 +18236,8 @@ paths:
schema:
$ref: "#/components/schemas/UpdateCustomMetricResponse"
"401": *a1
- "404": *a40
- "409": *a41
+ "404": *a42
+ "409": *a43
"422": *a4
"429": *a5
delete:
@@ -17562,7 +18260,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteCustomMetricResponse"
"401": *a1
- "404": *a40
+ "404": *a42
"422": *a4
"429": *a5
/api/custom-metrics/{id}/entries:
@@ -17606,7 +18304,7 @@ paths:
$ref: "#/components/schemas/ListCustomMetricEntriesEnvelope"
"401": *a1
"403": *a11
- "404": *a40
+ "404": *a42
"422": *a4
"429": *a5
post:
@@ -17635,7 +18333,7 @@ paths:
schema:
$ref: "#/components/schemas/CreateCustomMetricEntryResponse"
"401": *a1
- "404": *a40
+ "404": *a42
"409": *a7
"422": *a4
"429": *a12
@@ -17671,7 +18369,7 @@ paths:
schema:
$ref: "#/components/schemas/UpdateCustomMetricEntryResponse"
"401": *a1
- "404": &a42
+ "404": &a44
description: Custom metric entry not found (or owned by another user).
content:
application/json:
@@ -17704,7 +18402,7 @@ paths:
schema:
$ref: "#/components/schemas/DeleteCustomMetricEntryResponse"
"401": *a1
- "404": *a42
+ "404": *a44
"422": *a4
"429": *a5
/api/custom-metrics/{id}/entries/restore:
@@ -18116,7 +18814,7 @@ paths:
schema:
$ref: "#/components/schemas/NutrientOverviewEnvelope"
"401": *a1
- "403": &a43
+ "403": &a45
description: >-
The opt-in `nutrients` module is off for this account (errorCode `module.disabled`). Stop syncing and do not
retry; offer the enable-in-settings hint. Only run the HealthKit read-authorization flow while the module is
@@ -18171,7 +18869,7 @@ paths:
schema:
$ref: "#/components/schemas/NutrientDailySeriesEnvelope"
"401": *a1
- "403": *a43
+ "403": *a45
"422": *a4
"429": *a5
/api/nutrients/water:
@@ -18200,7 +18898,7 @@ paths:
schema:
$ref: "#/components/schemas/NutrientWaterWriteEnvelope"
"401": *a1
- "403": *a43
+ "403": *a45
"409": *a7
"422": *a4
"429": *a5
@@ -18702,26 +19400,26 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/WithingsCredentialsUpdateEnvelope"
- "400": &a44
+ "400": &a46
description: Body is not parseable JSON.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"401": *a1
- "413": &a45
+ "413": &a47
description: Body exceeds 64 KiB.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "415": &a46
+ "415": &a48
description: Content-Type is not `application/json`.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "422": &a47
+ "422": &a49
description: "`clientId` and `clientSecret` are both required and each must be 1–200 characters. The envelope carries a
single fixed message, not per-field issues."
content:
@@ -18907,11 +19605,11 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/FitbitCredentialsUpdateEnvelope"
- "400": *a44
+ "400": *a46
"401": *a1
- "413": *a45
- "415": *a46
- "422": *a47
+ "413": *a47
+ "415": *a48
+ "422": *a49
"429": *a5
delete:
tags:
@@ -19054,11 +19752,11 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/GoogleHealthCredentialsUpdateEnvelope"
- "400": *a44
+ "400": *a46
"401": *a1
- "413": *a45
- "415": *a46
- "422": *a47
+ "413": *a47
+ "415": *a48
+ "422": *a49
"429": *a5
delete:
tags:
@@ -19150,6 +19848,14 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"401": *a1
+ "409":
+ description: Another sync of this account is already running (the history backfill after a reconnect, or the hourly
+ poll), so none was started. `meta.errorCode` = `sync_in_progress`. `GET /api/google-health/sync/status`
+ reports the running one.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
"413":
description: Body exceeds 64 KiB.
content:
@@ -19211,11 +19917,11 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/OuraCredentialsUpdateEnvelope"
- "400": *a44
+ "400": *a46
"401": *a1
- "413": *a45
- "415": *a46
- "422": *a47
+ "413": *a47
+ "415": *a48
+ "422": *a49
"429": *a5
delete:
tags:
@@ -19291,21 +19997,21 @@ paths:
schema:
$ref: "#/components/schemas/OuraTestEnvelope"
"401": *a1
- "422": &a48
+ "422": &a50
description: No usable connection for this provider — nothing stored, or the stored grant could no longer be resolved
into an access token. `meta.errorCode` = `not_configured`.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "429": &a49
+ "429": &a51
description: More than 5 requests in 60 s from this user. `meta.errorCode` = `rate_limited_self` — this is the local
limiter, distinct from a `rate_limited` 502 which is the provider's.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "502": &a50
+ "502": &a52
description: "The probe did not complete. `meta.errorCode` names the class: `credentials_rejected` (the provider
answered 401/403 — the stored grant is gone), `rate_limited` (the PROVIDER answered 429, which is not this
endpoint's own limiter), `upstream_error` (5xx), `timeout` (no answer within 5 s), `connection_failed`
@@ -19350,11 +20056,11 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/PolarCredentialsUpdateEnvelope"
- "400": *a44
+ "400": *a46
"401": *a1
- "413": *a45
- "415": *a46
- "422": *a47
+ "413": *a47
+ "415": *a48
+ "422": *a49
"429": *a5
delete:
tags:
@@ -19430,9 +20136,9 @@ paths:
schema:
$ref: "#/components/schemas/PolarTestEnvelope"
"401": *a1
- "422": *a48
- "429": *a49
- "502": *a50
+ "422": *a50
+ "429": *a51
+ "502": *a52
/api/strava/credentials:
get:
tags:
@@ -19468,11 +20174,11 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/StravaCredentialsUpdateEnvelope"
- "400": *a44
+ "400": *a46
"401": *a1
- "413": *a45
- "415": *a46
- "422": *a47
+ "413": *a47
+ "415": *a48
+ "422": *a49
"429": *a5
delete:
tags:
@@ -19550,9 +20256,9 @@ paths:
schema:
$ref: "#/components/schemas/StravaTestEnvelope"
"401": *a1
- "422": *a48
- "429": *a49
- "502": *a50
+ "422": *a50
+ "429": *a51
+ "502": *a52
/api/integrations/status:
get:
tags:
@@ -19588,7 +20294,7 @@ paths:
$ref: "#/components/schemas/FitbitResumeEnvelope"
"401": *a1
"422": *a4
- "429": *a49
+ "429": *a51
/api/integrations/fitbit/test:
post:
tags:
@@ -19606,9 +20312,9 @@ paths:
schema:
$ref: "#/components/schemas/FitbitIntegrationTestEnvelope"
"401": *a1
- "422": *a48
- "429": *a49
- "502": *a50
+ "422": *a50
+ "429": *a51
+ "502": *a52
/api/integrations/google-health/resume:
post:
tags:
@@ -19625,7 +20331,7 @@ paths:
$ref: "#/components/schemas/GoogleHealthResumeEnvelope"
"401": *a1
"422": *a4
- "429": *a49
+ "429": *a51
/api/integrations/google-health/test:
post:
tags:
@@ -19658,9 +20364,9 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "422": *a48
- "429": *a49
- "502": *a50
+ "422": *a50
+ "429": *a51
+ "502": *a52
/api/integrations/whoop/resume:
post:
tags:
@@ -19677,7 +20383,7 @@ paths:
$ref: "#/components/schemas/WhoopResumeEnvelope"
"401": *a1
"422": *a4
- "429": *a49
+ "429": *a51
/api/integrations/whoop/test:
post:
tags:
@@ -19694,9 +20400,9 @@ paths:
schema:
$ref: "#/components/schemas/WhoopIntegrationTestEnvelope"
"401": *a1
- "422": *a48
- "429": *a49
- "502": *a50
+ "422": *a50
+ "429": *a51
+ "502": *a52
/api/integrations/withings/resume:
post:
tags:
@@ -19714,7 +20420,7 @@ paths:
$ref: "#/components/schemas/WithingsResumeEnvelope"
"401": *a1
"422": *a4
- "429": *a49
+ "429": *a51
/api/integrations/withings/test:
post:
tags:
@@ -19732,8 +20438,8 @@ paths:
schema:
$ref: "#/components/schemas/WithingsIntegrationTestEnvelope"
"401": *a1
- "422": *a48
- "429": *a49
+ "422": *a50
+ "429": *a51
"502":
description: "The probe did not complete. The shared codes apply (`credentials_rejected`, `rate_limited`,
`upstream_error`, `timeout`, `connection_failed`) and this path adds one of its own:
@@ -19928,6 +20634,168 @@ paths:
"401": *a1
"422": *a4
"429": *a5
+ /api/managed-profiles/{id}/handover:
+ get:
+ tags:
+ - Account sharing
+ summary: Whether a handover link is open
+ description: "The profile's open handover link, if any, and whether this instance allows creating one. Never returns the
+ link: only its hash is stored. Cookie-only, no step-up."
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: string
+ required: true
+ responses:
+ "200":
+ description: The status.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ManagedProfileHandoverStatusEnvelope"
+ "401": *a1
+ "404":
+ description: "No such managed profile, or the caller is not one of its Guardians (`meta.errorCode:
+ managed_profile.not_found`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
+ post:
+ tags:
+ - Account sharing
+ summary: Create a handover link for a managed profile
+ description: "Mints the one-time link that lets the person a managed profile describes take it over as their own
+ account. No data moves: claiming sets credentials on the profile's own record. Claiming applies each guardian's
+ proposal at once; the new owner then confirms or changes it on first sign-in, and until they do the proposal
+ holds. Creating a new link withdraws an open one. Pending invitations on the profile are withdrawn when it is
+ claimed. Cookie-only and step-up gated, like every act on a managed profile; ten an hour per caller. Refused on
+ an instance that allows single sign-on only."
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: string
+ required: true
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateManagedProfileHandoverRequest"
+ responses:
+ "201":
+ description: The link, shown once.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ManagedProfileHandoverEnvelope"
+ "401": *a1
+ "403":
+ description: "The instance allows single sign-on only (`meta.errorCode: profile_claim.oidc_only_unsupported`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "404":
+ description: "No such managed profile, or the caller is not one of its Guardians (`meta.errorCode:
+ managed_profile.not_found`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422":
+ description: "The body is invalid, or a proposal names a grant that is not an active guardian of this profile
+ (`meta.errorCode: managed_profile.handover.unknown_guardian`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "429": *a5
+ delete:
+ tags:
+ - Account sharing
+ summary: Withdraw the open handover link
+ description: Withdraws the profile's open handover link, if there is one; the link then answers like any unknown one.
+ Cookie-only; no step-up, because withdrawing only takes something away.
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: string
+ required: true
+ responses:
+ "200":
+ description: Withdrawn, or there was no open link.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/RevokeManagedProfileHandoverEnvelope"
+ "401": *a1
+ "404":
+ description: "No such managed profile, or the caller is not one of its Guardians (`meta.errorCode:
+ managed_profile.not_found`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
+ /api/account/handover-decision:
+ get:
+ tags:
+ - Account sharing
+ summary: The handover decision waiting for a new owner
+ description: "After claiming a managed profile, the new owner decides each former guardian's access. This answers that
+ decision, or `pending: null` when none is waiting. Refused while acting on another account."
+ responses:
+ "200":
+ description: The waiting decision, or null.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PendingHandoverDecisionEnvelope"
+ "401": *a1
+ "422": *a4
+ "429": *a5
+ post:
+ tags:
+ - Account sharing
+ summary: Decide each former guardian's access
+ description: Records the new owner's final decision per former guardian. A changed level ends the guardian's grant and
+ writes a new one; `manage` can restore access a guardian held before the handover. After this, access changes
+ through the ordinary grant endpoints. Refused while acting on another account.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HandoverDecisionRequest"
+ responses:
+ "200":
+ description: Decided.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/HandoverDecisionEnvelope"
+ "401": *a1
+ "409":
+ description: "No decision is waiting (`meta.errorCode: managed_profile.handover.no_pending`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422":
+ description: "The body is invalid, or a decision names somebody who was not a guardian at the claim (`meta.errorCode:
+ managed_profile.handover.unknown_guardian`)."
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "429": *a5
/api/auth/me:
get:
tags:
@@ -19973,7 +20841,7 @@ paths:
schema:
$ref: "#/components/schemas/RecordActivityEnvelope"
"401": *a1
- "403": &a51
+ "403": &a53
description: Refused. `meta.errorCode` is `sharing.not_permitted` when the request was made while acting on another
account (grant management is never delegable), or `sharing.access.denied` when the caller may not act as the
account they named. The latter is byte-identical for an account that does not exist and one that granted
@@ -19998,7 +20866,7 @@ paths:
schema:
$ref: "#/components/schemas/AccountGrantListEnvelope"
"401": *a1
- "403": *a51
+ "403": *a53
"422": *a4
"429": *a5
post:
@@ -20100,7 +20968,7 @@ paths:
schema:
$ref: "#/components/schemas/AcceptedAccountGrantEnvelope"
"401": *a1
- "403": *a51
+ "403": *a53
"404":
description: No such invitation for this account.
content:
@@ -20144,7 +21012,7 @@ paths:
schema:
$ref: "#/components/schemas/RevokedAccountGrantEnvelope"
"401": *a1
- "403": *a51
+ "403": *a53
"404":
description: No such grant on this account's record.
content:
@@ -20185,7 +21053,7 @@ paths:
schema:
$ref: "#/components/schemas/RenouncedAccountGrantEnvelope"
"401": *a1
- "403": *a51
+ "403": *a53
"404":
description: No such grant held by this account.
content:
@@ -20567,7 +21435,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"401": *a1
- "403": *a51
+ "403": *a53
"409":
description: "The record this browser session is on is not the record the request asserted. `meta.errorCode` is
`sharing.session.changed`. It is not a refusal of access — the caller may well be entitled to both records —
@@ -20765,6 +21633,56 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
+ /api/notifications/action/{token}:
+ post:
+ tags:
+ - Notifications
+ summary: Log a quick answer tapped inside a notification
+ description: "Called by a notification app (ntfy `http` action), never by a signed-in client: the signed path token is
+ the only credential. The token is bound to one user and one answer, expires six hours after the reminder was
+ sent, and is single-use across all answers offered on that reminder. Rate-limited per client address before the
+ token is read. Writes a mood entry with source `NOTIFICATION`; refused with 403 when the mood module is off."
+ parameters:
+ - in: path
+ name: token
+ schema:
+ type: string
+ description: Signed action token from the notification's URL.
+ required: true
+ description: Signed action token from the notification's URL.
+ responses:
+ "200":
+ description: The answer was logged.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/NotificationActionResponse"
+ "401":
+ description: Invalid token (forged, altered, or unknown user).
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "403":
+ description: The mood module is turned off for this account.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "409":
+ description: This reminder was already answered.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "410":
+ description: The answer expired.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
/api/notifications/preferences:
get:
tags:
@@ -20897,7 +21815,7 @@ paths:
schema:
$ref: "#/components/schemas/McpConnectionListEnvelope"
"401": *a1
- "403": &a52
+ "403": &a54
description: The operator has disabled the API instance-wide. Every route in this module refuses while that setting is
off, for a cookie session as well as for a Bearer token; it is not a per-user permission and retrying will
not help.
@@ -20930,7 +21848,7 @@ paths:
schema:
$ref: "#/components/schemas/McpConnectionRevokeEnvelope"
"401": *a1
- "403": *a52
+ "403": *a54
"404":
description: No live connection with this id for this caller — also the answer for an already-revoked one, so a repeat
revoke is 404 rather than an idempotent 200.
@@ -20957,7 +21875,7 @@ paths:
schema:
$ref: "#/components/schemas/McpConnectorTokenListEnvelope"
"401": *a1
- "403": *a52
+ "403": *a54
"422": *a4
"429": *a5
post:
@@ -20990,7 +21908,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"401": *a1
- "403": *a52
+ "403": *a54
"413":
description: Body exceeds 16 KiB.
content:
@@ -21033,7 +21951,7 @@ paths:
schema:
$ref: "#/components/schemas/McpConnectorTokenRevokeEnvelope"
"401": *a1
- "403": *a52
+ "403": *a54
"404":
description: No `health:read` token with this id for this caller. An already-revoked token is still found, so a repeat
revoke answers 200.
@@ -21091,7 +22009,7 @@ paths:
description: Returns the named family's resolved settings for the record the session is acting on. Cookie session or
Bearer, while acting on a managed record the caller guards.
parameters:
- - &a53
+ - &a55
name: family
in: path
required: true
@@ -21135,7 +22053,7 @@ paths:
ride the audit row. Body capped at 64 KiB. Cookie session or Bearer, while acting on a managed record the caller
guards.
parameters:
- - *a53
+ - *a55
requestBody:
required: true
content:
@@ -21272,6 +22190,32 @@ paths:
"403": *a13
"422": *a4
"429": *a5
+ /api/environment/preferences:
+ patch:
+ tags:
+ - Environment
+ summary: Change the environment switches
+ description: "Turns the air-quality part of the module on or off for this account (on by default). Off means the nightly
+ fetch skips the air-quality feed, and the overview, the Coach, MCP and the correlations leave the air-quality
+ values out; days already stored keep theirs. Turning it on queues a refresh, and the nightly gap fill catches up
+ on older days. Module-gated. Strict body: an unknown key is a 422."
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/EnvironmentPreferencesRequest"
+ responses:
+ "200":
+ description: The stored switches.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/EnvironmentPreferencesEnvelope"
+ "401": *a1
+ "403": *a19
+ "422": *a4
+ "429": *a5
/api/environment:
get:
tags:
@@ -21363,6 +22307,12 @@ paths:
The span is capped at 730 days, and the worker re-checks the cap independently. Body cap 4 KiB. Module-gated,
and it draws on the shared analytics-read rate bucket.
+
+
+ Every backfill draws on the instance-wide Open-Meteo budget, so one account is held to three a rolling hour (429
+ `environment.backfill_rate_limited`), at most one queued backfill per twenty-minute slot (409
+ `environment.backfill_pending`), and, inside the worker, its own share of the daily request budget; days the
+ share does not reach are filled by the nightly run.
requestBody:
required: true
content:
@@ -21380,8 +22330,9 @@ paths:
"401": *a1
"403": *a19
"409":
- description: No home location is set, so there is nothing to resolve the days against. `meta.errorCode` =
- `environment.no_home`.
+ description: No home location is set, so there is nothing to resolve the days against (`meta.errorCode` =
+ `environment.no_home`), or a backfill for this account is already queued in the current twenty-minute slot
+ (`environment.backfill_pending`).
content:
application/json:
schema:
@@ -21633,7 +22584,10 @@ paths:
"422":
description: "Validation failed, with every issue listed rather than the first — an integration operator should see the
whole shape at once. An empty or unstable `idempotencyKey` fails here: the column is unique table-wide, so
- an empty key would resolve to whichever unrelated row stored it first."
+ an empty key would resolve to whichever unrelated row stored it first. Also 422 with `meta.errorCode` =
+ `medication.intake.notTracked` (v1.42.0) when the named medication is kept as a record only (`trackIntake:
+ false`); checked after the token's scope, so a token without this medication's scope learns nothing about
+ it."
content:
application/json:
schema:
@@ -21672,13 +22626,13 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/AdminBackupDocument"
- "403": &a54
+ "403": &a56
description: Caller is not an admin. `requireAdmin()` is cookie-only, so a Bearer token lands here whatever its scope.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "404": &a55
+ "404": &a57
description: No stored copy with this id.
content:
application/json:
@@ -21752,8 +22706,8 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "403": *a54
- "404": *a55
+ "403": *a56
+ "404": *a57
"409":
description: A restore of the same account is already queued or running (`meta.errorCode` = `backup.restore.active`,
`meta.jobId` names it), the stored copy was replaced by a newer one while it was being checked
@@ -21797,7 +22751,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/AdminBackupRestoreJobListResponse"
- "403": *a54
+ "403": *a56
/api/admin/backups/restores/{jobId}:
get:
tags:
@@ -21820,7 +22774,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/AdminBackupRestoreJobResponse"
- "403": *a54
+ "403": *a56
"404":
description: No restore job with this id.
content:
@@ -21839,7 +22793,7 @@ paths:
request leaves the server. Needs neither the Documents module nor the list: with the list unset, `available` is
false and any connection still stored is listed (with `originAllowed: false`) so it can be removed. Cookie
session only; not delegable."
- security: &a56
+ security: &a58
- cookieAuth: []
responses:
"200":
@@ -21849,7 +22803,7 @@ paths:
schema:
$ref: "#/components/schemas/DocumentSourcesStatusEnvelope"
"401": *a1
- "403": &a57
+ "403": &a59
description: "Refused before any request to the source: `documents.sources.browserOnly` for a Bearer caller,
`sharing.not_permitted` while acting on another person's record (a delegate or guardian never uses the
owner's archive), or `module.disabled` when the Documents module is off."
@@ -21872,9 +22826,9 @@ paths:
`documents.sources.tokenRequired`), so a stored token is never sent to an address it was not saved for. A Papra
organization id is letters, digits, `_` and `-`. The token is stored encrypted (AES-256-GCM) and never returned.
Ten saves and tests a minute per person. Cookie session only; not delegable."
- security: *a56
+ security: *a58
parameters:
- - &a58
+ - &a60
name: system
in: path
required: true
@@ -21913,8 +22867,8 @@ paths:
schema:
$ref: "#/components/schemas/DocumentSourceConnectionEnvelope"
"401": *a1
- "403": *a57
- "404": &a59
+ "403": *a59
+ "404": &a61
description: "`documents.sources.unavailable`: the operator has not listed any origin in `DOCUMENT_SOURCE_ORIGINS`, so
the picker is off. `documents.sources.notConnected`: no connection is saved for this system.
`documents.sources.notFound`: the source has no such document, or (Papra) no such organization, including
@@ -21933,7 +22887,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"429": *a5
- "502": &a60
+ "502": &a62
description: "The source could not answer usefully, with the reason in `meta.errorCode` and, when the source answered at
all, its HTTP status in `meta.upstreamStatus` (its body is never passed on): `documents.sources.unreachable`
(no connection, timeout, or a destination refused at dial time), `documents.sources.redirected` (the source
@@ -21952,9 +22906,9 @@ paths:
description: Deletes the caller's connection for this system, and its token with it. Works even after the operator
removed the origin from the list, and with the Documents module switched off. `disconnected` is false when there
was nothing to delete. Cookie session only; not delegable.
- security: *a56
+ security: *a58
parameters:
- - *a58
+ - *a60
responses:
"200":
description: Removed, or there was nothing to remove.
@@ -21963,7 +22917,7 @@ paths:
schema:
$ref: "#/components/schemas/DocumentSourceDisconnectEnvelope"
"401": *a1
- "403": *a57
+ "403": *a59
"422": *a4
"429": *a5
/api/documents/sources/{system}/test:
@@ -21974,9 +22928,9 @@ paths:
description: One request to the source with the stored token. Answers `ok` with the round-trip time and stamps
`lastVerifiedAt`, or the reason in `meta.errorCode`. Shares the save's allowance of ten a minute. Cookie session
only; not delegable.
- security: *a56
+ security: *a58
parameters:
- - *a58
+ - *a60
responses:
"200":
description: The source answered.
@@ -21985,9 +22939,9 @@ paths:
schema:
$ref: "#/components/schemas/DocumentSourceTestEnvelope"
"401": *a1
- "403": *a57
- "404": *a59
- "422": &a61
+ "403": *a59
+ "404": *a61
+ "422": &a63
description: "`documents.sources.originNotAllowed`: the base address's origin is not on the operator's list (checked on
every request, so a connection saved earlier is cut off once its origin is removed), or another validation
failure."
@@ -21996,7 +22950,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"429": *a5
- "502": *a60
+ "502": *a62
/api/documents/sources/{system}/tags:
get:
tags:
@@ -22004,9 +22958,9 @@ paths:
summary: Tags of a connected archive
description: The source's tags for the picker's filter, the first 250 by name, in the source's own ids. Shares the
search allowance of sixty a minute. Cookie session only; not delegable.
- security: *a56
+ security: *a58
parameters:
- - *a58
+ - *a60
responses:
"200":
description: The tags.
@@ -22015,11 +22969,11 @@ paths:
schema:
$ref: "#/components/schemas/DocumentSourceTagsEnvelope"
"401": *a1
- "403": *a57
- "404": *a59
- "422": *a61
+ "403": *a59
+ "404": *a61
+ "422": *a63
"429": *a5
- "502": *a60
+ "502": *a62
/api/documents/sources/{system}/search:
get:
tags:
@@ -22033,9 +22987,9 @@ paths:
under that source key: `new`, `imported` with the vault document's `documentId`, or `deleted` (deleted in
HealthLog; importing it again stores nothing). Nothing is stored. Sixty searches a minute, shared with the tag
read. Cookie session only; not delegable."
- security: *a56
+ security: *a58
parameters:
- - *a58
+ - *a60
- name: q
in: query
required: false
@@ -22076,11 +23030,11 @@ paths:
schema:
$ref: "#/components/schemas/DocumentSourceSearchEnvelope"
"401": *a1
- "403": *a57
- "404": *a59
- "422": *a61
+ "403": *a59
+ "404": *a61
+ "422": *a63
"429": *a5
- "502": *a60
+ "502": *a62
/api/documents/sources/{system}/import:
post:
tags:
@@ -22099,9 +23053,9 @@ paths:
fetched), including a duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the
person's own setting. Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new
document, 200 otherwise. Cookie session only; not delegable."
- security: *a56
+ security: *a58
parameters:
- - *a58
+ - *a60
requestBody:
required: true
content:
@@ -22159,7 +23113,7 @@ paths:
schema:
$ref: "#/components/schemas/DocumentSourceImportEnvelope"
"401": *a1
- "403": *a57
+ "403": *a59
"404":
description: "`documents.sources.unavailable`: the operator has not listed any origin in `DOCUMENT_SOURCE_ORIGINS`, so
the picker is off. `documents.sources.notConnected`: no connection is saved for this system.
@@ -22190,7 +23144,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "422": *a61
+ "422": *a63
"429":
description: "`documents.inbound.rateLimited` (the person's hourly upload allowance, with the rate-limit headers) or
`documents.inbound.uploadBusy` (too many uploads in progress; `Retry-After: 1`)."
@@ -22198,7 +23152,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
- "502": *a60
+ "502": *a62
/api/symptoms/definitions:
get:
tags:
@@ -22222,7 +23176,7 @@ paths:
schema:
$ref: "#/components/schemas/ListSymptomDefinitionsEnvelope"
"401": *a1
- "403": &a62
+ "403": &a64
description: >-
`illness.disabled`: the record has the illness module switched off (or the operator turned it off
server-wide); symptoms ride that module.
@@ -22265,7 +23219,7 @@ paths:
schema:
$ref: "#/components/schemas/CreateSymptomDefinitionEnvelope"
"401": *a1
- "403": *a62
+ "403": *a64
"409": *a7
"422": *a4
"429": *a5
@@ -22295,8 +23249,8 @@ paths:
schema:
$ref: "#/components/schemas/UpdateSymptomDefinitionEnvelope"
"401": *a1
- "403": *a62
- "404": &a63
+ "403": *a64
+ "404": &a65
description: "`symptoms.definition.notFound`: no definition with this id on the record."
content:
application/json:
@@ -22330,8 +23284,8 @@ paths:
schema:
$ref: "#/components/schemas/DeleteSymptomDefinitionEnvelope"
"401": *a1
- "403": *a62
- "404": *a63
+ "403": *a64
+ "404": *a65
"422": *a4
"429": *a5
/api/symptoms/events:
@@ -22380,7 +23334,7 @@ paths:
schema:
$ref: "#/components/schemas/ListSymptomEventsEnvelope"
"401": *a1
- "403": *a62
+ "403": *a64
"422": *a4
"429": *a5
post:
@@ -22406,8 +23360,8 @@ paths:
schema:
$ref: "#/components/schemas/CreateSymptomEventEnvelope"
"401": *a1
- "403": *a62
- "404": *a63
+ "403": *a64
+ "404": *a65
"409": *a7
"422": *a4
"429": *a5
@@ -22437,8 +23391,8 @@ paths:
schema:
$ref: "#/components/schemas/UpdateSymptomEventEnvelope"
"401": *a1
- "403": *a62
- "404": &a64
+ "403": *a64
+ "404": &a66
description: "`symptoms.event.notFound`: no occurrence with this id on the record."
content:
application/json:
@@ -22465,8 +23419,359 @@ paths:
schema:
$ref: "#/components/schemas/DeleteSymptomEventEnvelope"
"401": *a1
- "403": *a62
- "404": *a64
+ "403": *a64
+ "404": *a66
+ "422": *a4
+ "429": *a5
+ /api/day/index:
+ get:
+ tags:
+ - Records
+ summary: Days with entries
+ description: Which local days in a window hold anything, by section, and which carry a notable observation. At most 366
+ days per call. Days are cut in the record's own time zone.
+ parameters:
+ - in: query
+ name: from
+ schema:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ required: true
+ description: A local calendar date, `YYYY-MM-DD`.
+ - in: query
+ name: to
+ schema:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ required: true
+ description: A local calendar date, `YYYY-MM-DD`.
+ responses:
+ "200":
+ description: The days in the window that hold entries.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DayIndexEnvelope"
+ "401": *a1
+ "403": *a11
+ "422": *a4
+ "429": *a5
+ /api/day/notable:
+ get:
+ tags:
+ - Records
+ summary: Since the last visit
+ description: "What stood out and what changed in a window, for the preparation of the next visit: deterministic
+ observations (a value highest or lowest for at least three months, a first reading, a regular reading that
+ stopped for two weeks or more) and context changes (dose changes, medication and course starts and ends, pauses,
+ illness onsets and recoveries, vaccinations, procedures, lab days). Without `from` the window starts at the last
+ completed visit the caller may see (else 90 days back); without `to` it ends today; at most 1096 days. Keys and
+ the record's own names only, no judgement. Days are cut in the record's own time zone. Sections the caller's
+ grant does not cover are left out."
+ parameters:
+ - in: query
+ name: from
+ schema:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - in: query
+ name: to
+ schema:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ responses:
+ "200":
+ description: The window, its observations and its changes.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DayNotableWindowEnvelope"
+ "401": *a1
+ "403": *a11
+ "422": *a4
+ "429": *a5
+ /api/day/{date}:
+ get:
+ tags:
+ - Records
+ summary: One day across the record
+ description: "Everything on one local calendar day: what ran through it (medications, an illness, a cycle phase, a
+ trip), the readings in its window with the person's usual range, what happened on it, and deterministic notable
+ observations as keys with parameters. Sections of a switched-off module are left out; sections the caller's
+ grant does not cover are named in `sections`. Life events and the environment are the owner's only: for a
+ delegate at any level they are named `not_shared`. Read live from the record, never from the UTC rollups. A date
+ that is not a real calendar date answers 422."
+ parameters:
+ - in: path
+ name: date
+ schema:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ required: true
+ description: A local calendar date, `YYYY-MM-DD`.
+ responses:
+ "200":
+ description: The day.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DayEnvelope"
+ "401": *a1
+ "403": *a11
+ "422": *a4
+ "429": *a5
+ /api/timeline:
+ get:
+ tags:
+ - Records
+ summary: The record over the years
+ description: Lanes of conditions, allergies, medications, vaccinations, visits, lab days, documents and life events, the
+ standing items without a start, and monthly (or finer, by zoom) value series. Empty lanes are not sent; lanes of
+ a switched-off module never are. The `life` lane (life events and travel periods) is the owner's only and is
+ never sent to a delegate. Requires the opt-in `timeline` module.
+ parameters:
+ - in: query
+ name: zoom
+ schema:
+ default: all
+ description: "`range` is a chosen stretch: it requires `from` and `to`, at most 15 years apart, and its value series
+ average quarters, months, weeks or days by its length."
+ type: string
+ enum:
+ - all
+ - year
+ - quarter
+ - range
+ description: "`range` is a chosen stretch: it requires `from` and `to`, at most 15 years apart, and its value series
+ average quarters, months, weeks or days by its length."
+ - in: query
+ name: from
+ schema:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - in: query
+ name: to
+ schema:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - in: query
+ name: values
+ schema:
+ description: Comma-separated value series to include, as `MeasurementType` names or `MOOD`. A name given twice counts
+ once, so the list is bounded by the names that exist; an unknown name answers 422.
+ type: string
+ description: Comma-separated value series to include, as `MeasurementType` names or `MOOD`. A name given twice counts
+ once, so the list is bounded by the names that exist; an unknown name answers 422.
+ responses:
+ "200":
+ description: The timeline for the requested zoom and window.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TimelineEnvelope"
+ "401": *a1
+ "403": &a67
+ description: >-
+ `module.disabled` with `meta.module = timeline`: the record has the timeline switched off (it is opt-in), or
+ the operator turned it off server-wide.
+
+
+ Refused: this request named a record the caller may not act on (`meta.errorCode` = `sharing.access.denied`).
+ Byte-identical for an account that does not exist, an account that granted nothing, a grant whose sections
+ do not reach this surface, and a read grant on a request that writes — the response carries no reason, so it
+ is not an account-enumeration oracle. The reason reaches the record owner's activity feed and the operator's
+ audit trail instead. On the cookie transport the same code answers one further case: a session that has
+ entered a shared record and sends a request without its record-context assertion, which is a client older
+ than the fence — leave the record and reload. A request that names no record but the caller's own never
+ reaches this response, so a client that never switches and never sends the per-request account selector will
+ not see it.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
+ /api/timeline/readiness:
+ get:
+ tags:
+ - Records
+ summary: What the timeline can show
+ description: "One row per lane: carries, thin or empty, a count, a detail as a message key with parameters, and the gaps
+ with one in-app link each. The verdict is one of two words, never a score. The `life` lane is reported to the
+ owner only. Requires the opt-in `timeline` module."
+ responses:
+ "200":
+ description: The readiness inventory.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TimelineReadinessEnvelope"
+ "401": *a1
+ "403": *a67
+ "422": *a4
+ "429": *a5
+ /api/life-events:
+ get:
+ tags:
+ - Records
+ summary: List life events
+ description: "The account's own life events, oldest first; soft-deleted ones are excluded. Title and note are decrypted
+ for the caller. Owner-only: no delegate reads them at any share level. Life events are never sent to a model."
+ responses:
+ "200":
+ description: The life events.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/LifeEventListEnvelope"
+ "401": *a1
+ "403": &a68
+ description: >-
+ Owner-only in v1.42: no share reaches life events, not a `profile` share, a whole-record share or MANAGE,
+ and not a guardian acting for a managed profile.
+
+
+ Refused: the request was made while the session is acting on another account (`meta.errorCode` =
+ `sharing.not_permitted`). This operation resolves the CALLER and never a named record, so under a switch it
+ refuses instead of quietly answering with the caller's own — leave the shared record first. No grant at any
+ level opens it.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
+ post:
+ tags:
+ - Records
+ summary: Add a life event
+ description: "A dated anchor in the person's life. `precision` says how much of the date is known and applies to both
+ ends: at MONTH a date is the first of its month, at YEAR the first of its year. `endDate`, when given, is not
+ before `startDate`. Title (1-120 characters) and note (up to 2000) are encrypted at rest. Owner-only, like every
+ life-event route."
+ parameters:
+ - *a6
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateLifeEventRequest"
+ responses:
+ "201":
+ description: Created.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateLifeEventEnvelope"
+ "401": *a1
+ "403": *a68
+ "409": *a7
+ "422": *a4
+ "429": *a12
+ /api/life-events/{id}:
+ patch:
+ tags:
+ - Records
+ summary: Edit a life event
+ description: A partial edit; an omitted key leaves the column untouched. The merged event must still satisfy the create
+ rules (dates aligned to the precision, end not before start).
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: string
+ required: true
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UpdateLifeEventRequest"
+ responses:
+ "200":
+ description: Updated.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UpdateLifeEventEnvelope"
+ "401": *a1
+ "403": *a68
+ "404": &a69
+ description: No such life event on this account.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ErrorEnvelope"
+ "422": *a4
+ "429": *a5
+ delete:
+ tags:
+ - Records
+ summary: Delete a life event
+ description: Soft-deletes one life event.
+ parameters:
+ - in: path
+ name: id
+ schema:
+ type: string
+ required: true
+ responses:
+ "200":
+ description: Deleted.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DeleteLifeEventEnvelope"
+ "401": *a1
+ "403": *a68
+ "404": *a69
+ "422": *a4
+ "429": *a5
+ /api/search:
+ post:
+ tags:
+ - Records
+ summary: Search the record
+ description: "One search over the record: visits and procedures, conditions, documents (title, file name and indexed
+ text), vaccinations, lab results (analyte names and notes), medications, symptoms, workouts by sport, and the
+ notes on mood entries, readings and illness days. Hits are grouped by kind, the best group first, and each
+ carries the in-app path that opens the record. Matching ignores case and accents and reads German umlauts typed
+ as ae, oe, ue or ss. A source whose module the record has switched off is not read. The search reads each source
+ newest first under a per-source row cap and a time budget; `truncated` says when that cut it short. The query
+ travels in the JSON body, never in the URL, and is never logged. Rate limited per acting account (20 a minute),
+ and one search at a time: a newer search by the same account stops the one before it, which then answers 409
+ `search.superseded`. A grant scoped to some sections of the record does not open it, like the timeline."
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ q:
+ type: string
+ minLength: 2
+ maxLength: 100
+ description: What to look for. Matched without regard to case or accents, and German umlauts may be typed as ae, oe, ue
+ or ss.
+ limit:
+ default: 5
+ description: Most hits per group.
+ type: integer
+ minimum: 1
+ maximum: 20
+ required:
+ - q
+ responses:
+ "200":
+ description: The hits, grouped by kind.
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/SearchEnvelope"
+ "401": *a1
+ "403": *a11
"422": *a4
"429": *a5
/api/bugreport:
@@ -23345,6 +24650,47 @@ components:
server-side recovery.
required:
- passphrase
+ ProfileClaimPreviewRequest:
+ type: object
+ properties:
+ token:
+ type: string
+ minLength: 1
+ maxLength: 80
+ required:
+ - token
+ additionalProperties: false
+ description: "`token` is the `hlp_` token from the handover link. Sent in the body, never in a URL."
+ ProfileClaimRequest:
+ type: object
+ properties:
+ token:
+ type: string
+ minLength: 1
+ maxLength: 80
+ username:
+ type: string
+ minLength: 3
+ maxLength: 30
+ pattern: ^[a-zA-Z0-9_-]+$
+ email:
+ type: string
+ maxLength: 254
+ format: email
+ pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
+ password:
+ type: string
+ minLength: 12
+ maxLength: 1024
+ required:
+ - token
+ - username
+ - email
+ - password
+ additionalProperties: false
+ description: "`token` is the `hlp_` token from the handover link, in the body. `username` follows the registration rules
+ and may not start with `managed-`; `email` is required, because the account needs a way back in; `password`
+ follows the registration password policy."
LoginPasswordRequest:
type: object
properties:
@@ -24174,16 +25520,19 @@ components:
items:
$ref: "#/components/schemas/AppleHealthBatchEntry"
syncTrigger:
- description: "Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, or a push wake).
- Recorded on the ingest wide event and kept on the account as `lastSyncTrigger` — plus `lastBackgroundSyncAt`
- for a `background` or `push` trigger — both readable from `GET /api/integrations/healthkit`. It does not
- affect dedup, attribution, or how any sample is stored. Optional and backward-compatible: pre-#66 clients
- omit it."
+ description: 'Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, a push wake, or
+ a user-initiated `manual` "Sync all" run, since v1.42). Recorded on the ingest wide event and kept on the
+ account as `lastSyncTrigger` — plus `lastBackgroundSyncAt` for a `background` or `push` trigger — both
+ readable from `GET /api/integrations/healthkit`. Since v1.42 it is also recorded per measurement type the
+ batch carried (Apple Health rows only), served as `lastReceivedAt` / `lastTrigger` / `lastNewSampleAt` on
+ each `metricFreshness` entry of that read. It does not affect dedup, attribution, or how any sample is
+ stored. Optional and backward-compatible: pre-#66 clients omit it.'
type: string
enum:
- foreground
- background
- push
+ - manual
required:
- entries
description: Apple Health batch ingest. ≤500 entries per call; idempotent via `Idempotency-Key`.
@@ -24992,6 +26341,15 @@ components:
does not know the field cannot have seen the stored schedule, which is served in `recordedSchedules`). Name
`trackIntake` to change the schedule of such a medication."
type: boolean
+ autoMarkTaken:
+ description: "v1.42.2 — record each scheduled dose as taken automatically. Default false. When true the server records a
+ taken intake (`autoRecorded: true`) for every scheduled slot once its time has passed, from the moment the
+ option was switched on (`autoMarkTakenSince`); slots before that are never filled. A slot the person already
+ took, skipped or removed is left alone, and the person can still change or skip any dose by hand. While on,
+ no medication reminder is sent for this medication on any channel; a client that arms local reminders should
+ not arm them either. Has no effect on an as-needed medication or one whose intake tracking is off. Omitted
+ on update leaves the current value."
+ type: boolean
schedules:
type: array
items:
@@ -25395,6 +26753,15 @@ components:
does not know the field cannot have seen the stored schedule, which is served in `recordedSchedules`). Name
`trackIntake` to change the schedule of such a medication."
type: boolean
+ autoMarkTaken:
+ description: "v1.42.2 — record each scheduled dose as taken automatically. Default false. When true the server records a
+ taken intake (`autoRecorded: true`) for every scheduled slot once its time has passed, from the moment the
+ option was switched on (`autoMarkTakenSince`); slots before that are never filled. A slot the person already
+ took, skipped or removed is left alone, and the person can still change or skip any dose by hand. While on,
+ no medication reminder is sent for this medication on any channel; a client that arms local reminders should
+ not arm them either. Has no effect on an as-needed medication or one whose intake tracking is off. Omitted
+ on update leaves the current value."
+ type: boolean
schedules:
type: array
items:
@@ -25604,7 +26971,7 @@ components:
- ko
today:
description: "Optional override of the reference date used to resolve relative phrases (\"tomorrow\", \"next Monday\").
- Format: `YYYY-MM-DD`. Defaults to the server's UTC day."
+ Format: `YYYY-MM-DD`. Defaults to the account's own day in its time zone."
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
required:
@@ -25855,6 +27222,28 @@ components:
minimum: 10
maximum: 600
- type: "null"
+ localReasoningEffort:
+ description: Reasoning setting of the Local provider entry, stored on that entry of the provider chain. `none` = Off,
+ `null` = Default (nothing sent). Omitted leaves it untouched.
+ anyOf:
+ - type: string
+ enum:
+ - none
+ - low
+ - medium
+ - high
+ - type: "null"
+ compatReasoningEffort:
+ description: Reasoning setting of the OpenAI-compatible gateway entry, stored on that entry of the provider chain.
+ `none` = Off, `null` = Default (nothing sent). Omitted leaves it untouched.
+ anyOf:
+ - type: string
+ enum:
+ - none
+ - low
+ - medium
+ - high
+ - type: "null"
description: "Partial update of the calling user's AI-provider configuration. An omitted key is left untouched; `null`
(or an empty string, on the string fields) clears the column. At least one recognised key must be present, or
the write is refused. Key material is write-only: it is encrypted on the way in and never returned. Unknown keys
@@ -25905,7 +27294,10 @@ components:
- last7days
- last30days
- last90days
+ - lastYear
- allTime
+ description: "How far back the Coach may read, in every answer: snapshot, retrieval tools and summaries alike. `allTime`
+ (the default) is no limit. Formerly only the starting window; `lastYear` came with the limit."
dataClusters:
maxItems: 10
type: array
@@ -25948,6 +27340,16 @@ components:
description: v1.39.4 — offer up to three follow-up chips under the Coach's latest answer. Absent means on; only `false`
switches them off.
type: boolean
+ reasoning:
+ description: "v1.41 — how hard the Coach thinks before it answers: `off`, `low`, `medium` or `high`. Absent means
+ `medium`. The operator may switch reasoning off or cap the level; the resolved value is published on `GET
+ /api/auth/me` as `coachReasoning`."
+ type: string
+ enum:
+ - off
+ - low
+ - medium
+ - high
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
@@ -26053,6 +27455,8 @@ components:
type: boolean
vaccinations:
type: boolean
+ timeline:
+ type: boolean
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
@@ -26121,8 +27525,6 @@ components:
enum:
- weekly
- biweekly
- ambientSuggestions:
- type: boolean
nudgeAiComposed:
type: boolean
measurementReminder:
@@ -26130,6 +27532,14 @@ components:
properties:
clientManaged:
type: boolean
+ content:
+ type: object
+ properties:
+ detail:
+ type: string
+ enum:
+ - generic
+ - detailed
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
@@ -26635,6 +28045,40 @@ components:
maxLength: 8
required:
- messageId
+ memoryDecision:
+ type: object
+ properties:
+ messageId:
+ type: string
+ minLength: 1
+ maxLength: 64
+ proposalId:
+ type: string
+ minLength: 1
+ maxLength: 64
+ accept:
+ type: boolean
+ required:
+ - messageId
+ - proposalId
+ - accept
+ planDecision:
+ type: object
+ properties:
+ messageId:
+ type: string
+ minLength: 1
+ maxLength: 64
+ planId:
+ type: string
+ minLength: 1
+ maxLength: 64
+ accept:
+ type: boolean
+ required:
+ - messageId
+ - planId
+ - accept
required:
- message
description: "Inbound Coach turn. `message` is the user's turn (1–4 000 chars). `conversationId` is omitted to start a
@@ -26832,6 +28276,31 @@ components:
- version
description: PUT body for the Insights layout — the layout fields plus the optional optimistic-concurrency base token
(`baseUpdatedAt`). Omit the token for the legacy unconditional write.
+ CoachFactCreateRequest:
+ type: object
+ properties:
+ messageId:
+ type: string
+ minLength: 1
+ maxLength: 64
+ required:
+ - messageId
+ additionalProperties: false
+ description: "v1.41 — the remember button: the id of one of the caller's own Coach messages. Never the text: the server
+ reads the message it stored. Strict: unknown keys 422."
+ CoachFactPatchRequest:
+ type: object
+ properties:
+ fact:
+ type: string
+ minLength: 3
+ maxLength: 160
+ required:
+ - fact
+ additionalProperties: false
+ description: "v1.41 — the fact's new wording (3..160 characters). Only the text: the server reads the new wording to
+ file it (a fact that names a medication moves to `medication`, other health wording moves a non-health fact to
+ `condition`) and a health fact the caller wrote counts as confirmed. Strict: unknown keys 422."
UpdateEmergencyProfileRequest:
type: object
properties:
@@ -27051,6 +28520,18 @@ components:
required:
- rating
description: Per-message helpful/unhelpful feedback (v1.4.23 H7). Optional `reason` is free-form prose, capped at 200 chars.
+ AdminMeasurementMaintenanceRequest:
+ type: object
+ properties:
+ vacuum:
+ default: true
+ description: Run `VACUUM (ANALYZE)` on `measurements`.
+ type: boolean
+ reindex:
+ default: true
+ description: Rebuild every index of `measurements` with `REINDEX INDEX CONCURRENTLY`, largest first, one at a time.
+ type: boolean
+ additionalProperties: false
InsightsPregenerateRequest:
type: object
properties: {}
@@ -27071,6 +28552,7 @@ components:
properties:
chain:
minItems: 1
+ maxItems: 7
type: array
items:
type: object
@@ -27096,15 +28578,30 @@ components:
maximum: 9007199254740991
enabled:
type: boolean
+ reasoningEffort:
+ description: Only on a `local` or `openai-compatible` entry; refused on any other. Omitted keeps the value already
+ stored for that provider, `null` restores Default, `none` / `low` / `medium` / `high` are sent as
+ `reasoning_effort`.
+ anyOf:
+ - type: string
+ enum:
+ - none
+ - low
+ - medium
+ - high
+ - type: "null"
required:
- providerType
- enabled
+ additionalProperties: false
description: The whole chain, in the order it should be walked. At least one entry and at most one per known provider
type; a repeated type is refused.
required:
- chain
+ additionalProperties: false
description: Replaces the account's provider chain wholesale. No credential is written or read here — only which
- provider types exist, in what order, and which are enabled.
+ provider types exist, in what order, which are enabled, and the reasoning setting of the Local and
+ OpenAI-compatible entries.
InsightsFeedbackRequest:
type: object
properties:
@@ -27442,6 +28939,7 @@ components:
- WEB
- TELEGRAM
- DAYLIO
+ - NOTIFICATION
externalId:
description: "iOS-side source-stable id for idempotent dedup on the `(userId, source, externalId)` key. Must be stable
across app restarts — an object description or memory address (``, `0x12ab34cd`) is a new value
@@ -27962,6 +29460,7 @@ components:
- WEB
- TELEGRAM
- DAYLIO
+ - NOTIFICATION
externalId:
type: string
minLength: 1
@@ -28328,9 +29827,10 @@ components:
- type: string
const: ""
format:
- description: Body shape. `generic` sends HealthLog's own JSON envelope; `gotify` sends the body Gotify's `POST /message`
- expects, with an integer priority. Omitted keeps the stored choice, which is `generic` for a config saved
- before the choice existed.
+ description: Body shape. `generic` sends HealthLog's own JSON envelope (`title`, `message`, `eventType`, `priority`,
+ plus `content` and `text` so a Discord or Slack webhook URL accepts it as is); `gotify` sends the body
+ Gotify's `POST /message` expects, with an integer priority. Omitted keeps the stored choice, which is
+ `generic` for a config saved before the choice existed.
type: string
enum:
- generic
@@ -29239,6 +30739,120 @@ components:
additionalProperties: false
description: Partial edit; an omitted key leaves the column untouched, an explicit null clears it, and a body naming
nothing is a 422.
+ CreateCustomVaccineRequest:
+ type: object
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 100
+ components:
+ minItems: 1
+ maxItems: 26
+ type: array
+ items:
+ type: string
+ enum:
+ - tetanus
+ - diphtheria
+ - pertussis
+ - polio
+ - measles
+ - mumps
+ - rubella
+ - varicella
+ - hib
+ - hepatitis-a
+ - hepatitis-b
+ - influenza
+ - pneumococcal
+ - herpes-zoster
+ - rsv
+ - covid-19
+ - fsme
+ - hpv
+ - meningococcal-acwy
+ - meningococcal-b
+ - rotavirus
+ - typhoid
+ - rabies
+ - yellow-fever
+ - japanese-encephalitis
+ - cholera
+ typicalSeriesDoses:
+ anyOf:
+ - type: integer
+ minimum: 1
+ maximum: 10
+ - type: "null"
+ boosterIntervalMonths:
+ anyOf:
+ - type: integer
+ minimum: 1
+ maximum: 600
+ - type: "null"
+ required:
+ - name
+ - components
+ additionalProperties: false
+ description: A new definition. `components` lists one to all of the catalogue's antigen slugs, each once — a disease
+ outside that list is logged as a free-text dose instead. `typicalSeriesDoses` (1–10) and `boosterIntervalMonths`
+ (1–600) are optional; the interval prefills the booster offer after a dose.
+ UpdateCustomVaccineRequest:
+ type: object
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 100
+ components:
+ minItems: 1
+ maxItems: 26
+ type: array
+ items:
+ type: string
+ enum:
+ - tetanus
+ - diphtheria
+ - pertussis
+ - polio
+ - measles
+ - mumps
+ - rubella
+ - varicella
+ - hib
+ - hepatitis-a
+ - hepatitis-b
+ - influenza
+ - pneumococcal
+ - herpes-zoster
+ - rsv
+ - covid-19
+ - fsme
+ - hpv
+ - meningococcal-acwy
+ - meningococcal-b
+ - rotavirus
+ - typhoid
+ - rabies
+ - yellow-fever
+ - japanese-encephalitis
+ - cholera
+ typicalSeriesDoses:
+ anyOf:
+ - type: integer
+ minimum: 1
+ maximum: 10
+ - type: "null"
+ boosterIntervalMonths:
+ anyOf:
+ - type: integer
+ minimum: 1
+ maximum: 600
+ - type: "null"
+ additionalProperties: false
+ description: Partial edit; an omitted key is left untouched and an empty body is a 422. The change applies to every dose
+ logged against the definition, because the series is derived on read; it never re-runs booster satisfaction.
CreateVaccinationRequest:
type: object
properties:
@@ -29300,6 +30914,12 @@ components:
minLength: 1
maxLength: 40
- type: "null"
+ customVaccineId:
+ anyOf:
+ - type: string
+ minLength: 1
+ maxLength: 40
+ - type: "null"
note:
anyOf:
- type: string
@@ -29316,12 +30936,15 @@ components:
- occurredAt
additionalProperties: false
description: "Log one administered dose. `occurredAt` is required and must not be in the future — a vaccination is
- recorded after the fact, and a planned booster is a reminder rather than a record. Exactly one identity arm is
- required: either `antigenSlug` (a slug the catalogue offers) or `vaccineName` (whatever the person's own record
- says, verbatim, which may be a trade name). Everything else is optional, so a decades-old paper entry with
- nothing but a date and a name is loggable. `note` is encrypted at rest. `documentIds` pre-links the scanned
- pages; an id naming nothing the caller owns is dropped rather than refused, so a link never blocks a save.
- Logging a dose satisfies any booster reminder keyed to one of its component antigens."
+ recorded after the fact, and a planned booster is a reminder rather than a record. At least one identity arm is
+ required: `antigenSlug` (a slug the catalogue offers), `customVaccineId` (one of the record's own vaccine
+ definitions, see `/api/vaccinations/custom`; an id the record does not hold live is a 404
+ `vaccination.custom-vaccine-not-found`) or `vaccineName` (whatever the person's own record says, verbatim, which
+ may be a trade name). When both a slug and a definition are given, the catalogue is the answer. Everything else
+ is optional, so a decades-old paper entry with nothing but a date and a name is loggable. `note` is encrypted at
+ rest. `documentIds` pre-links the scanned pages; an id naming nothing the caller owns is dropped rather than
+ refused, so a link never blocks a save. Logging a dose satisfies any booster reminder keyed to one of its
+ component antigens, the antigens of an own definition included."
UpdateVaccinationRequest:
type: object
properties:
@@ -29383,6 +31006,12 @@ components:
minLength: 1
maxLength: 40
- type: "null"
+ customVaccineId:
+ anyOf:
+ - type: string
+ minLength: 1
+ maxLength: 40
+ - type: "null"
note:
anyOf:
- type: string
@@ -29396,11 +31025,11 @@ components:
minLength: 1
maxLength: 40
additionalProperties: false
- description: Partial edit of a dose; an omitted key leaves the column untouched, and a body naming nothing is a 422.
+ description: "Partial edit of a dose; an omitted key leaves the column untouched, and a body naming nothing is a 422.
`occurredAt` is editable because a transcription typo is the common case — and editing it deliberately does NOT
re-run the booster satisfaction, so correcting a date can never move a reminder's due date. An edit that would
- leave the record with neither identity arm is refused. A present `documentIds` array replaces the links, empty
- array included.
+ leave the record with no identity arm (slug, own definition or name) is refused. `customVaccineId: null` unlinks
+ the dose from its own definition. A present `documentIds` array replaces the links, empty array included."
VaccinationBoosterRequest:
type: object
properties:
@@ -29874,6 +31503,26 @@ components:
additionalProperties: false
description: Partial edit of a family-history entry; an omitted key leaves the column untouched. A `null`
`ageAtOnset`/`note` clears that field. Rejects unknown keys.
+ DocumentsLayoutPutBody:
+ type: object
+ properties:
+ version:
+ type: number
+ const: 1
+ view:
+ type: string
+ enum:
+ - cards
+ - list
+ arrangement:
+ type: string
+ enum:
+ - stacked
+ - flow
+ required:
+ - version
+ additionalProperties: false
+ description: Either field may be omitted; the stored value is kept for the one left out.
DocumentBulkRequest:
type: object
properties:
@@ -31038,6 +32687,12 @@ components:
- painNrs
- waistCircumference
- waistToHeight
+ - scoreHealth
+ - scoreReadiness
+ - scoreRecovery
+ - scoreSleep
+ - scoreStress
+ - scoreStrain
description: Which chart's overlays are being written. Closed enum.
prefs:
type: object
@@ -31217,6 +32872,12 @@ components:
- painNrs
- waistCircumference
- waistToHeight
+ - scoreHealth
+ - scoreReadiness
+ - scoreRecovery
+ - scoreSleep
+ - scoreStress
+ - scoreStrain
additionalProperties:
type: object
properties:
@@ -31292,6 +32953,11 @@ components:
enum:
- score
- reminders
+ todayCardVisible:
+ description: v1.42 — whether the dashboard shows its top card (the Today card above the tiles). Missing resolves to
+ `true`; only an explicit `false` hides it. The stored default is omitted. A layout reset (DELETE) shows it
+ again. Additive.
+ type: boolean
baseUpdatedAt:
description: "Optimistic-concurrency base token: the `updatedAt` the client last read for this resource. Omit it for the
legacy unconditional write (older clients are unaffected). When present, the write is guarded on the stored
@@ -31306,6 +32972,71 @@ components:
- version
description: PUT body for the dashboard layout — the layout fields plus the optional optimistic-concurrency base token
(`baseUpdatedAt`). Omit the token for the legacy unconditional write.
+ CreateManagedProfileHandoverRequest:
+ type: object
+ properties:
+ expiresInDays:
+ default: 7
+ anyOf:
+ - type: number
+ const: 1
+ - type: number
+ const: 7
+ - type: number
+ const: 14
+ proposals:
+ default: []
+ maxItems: 50
+ type: array
+ items:
+ type: object
+ properties:
+ grantId:
+ type: string
+ minLength: 1
+ maxLength: 64
+ proposal:
+ type: string
+ enum:
+ - end
+ - read
+ - manage
+ required:
+ - grantId
+ - proposal
+ additionalProperties: false
+ additionalProperties: false
+ description: "`expiresInDays` is 1, 7 (default) or 14. `proposals` names at most one entry per active guardian grant; a
+ guardian left out is proposed `read`, and a grant that is not an active guardian of this profile is refused with
+ 422."
+ HandoverDecisionRequest:
+ type: object
+ properties:
+ decisions:
+ maxItems: 50
+ type: array
+ items:
+ type: object
+ properties:
+ grantId:
+ type: string
+ minLength: 1
+ maxLength: 64
+ access:
+ type: string
+ enum:
+ - end
+ - read
+ - manage
+ required:
+ - grantId
+ - access
+ additionalProperties: false
+ required:
+ - decisions
+ additionalProperties: false
+ description: The new owner's decision per former guardian, named by the guardian's grant id at the claim. A guardian
+ left out keeps the access the claim gave them.
AccountGrantInvite:
type: object
properties:
@@ -31737,6 +33468,8 @@ components:
type: boolean
vaccinations:
type: boolean
+ timeline:
+ type: boolean
additionalProperties: false
cycleTrackingEnabled:
type: boolean
@@ -32036,7 +33769,10 @@ components:
- last7days
- last30days
- last90days
+ - lastYear
- allTime
+ description: "How far back the Coach may read, in every answer: snapshot, retrieval tools and summaries alike. `allTime`
+ (the default) is no limit. Formerly only the starting window; `lastYear` came with the limit."
dataClusters:
maxItems: 10
type: array
@@ -32192,6 +33928,15 @@ components:
- layout
additionalProperties: false
description: The full insights layout. Unlike the other five families this one is a replacement rather than a merge.
+ EnvironmentPreferencesRequest:
+ type: object
+ properties:
+ airQualityEnabled:
+ type: boolean
+ description: Fetch and show air quality, pollen and UV for this account.
+ required:
+ - airQualityEnabled
+ additionalProperties: false
EnvironmentHomeRequest:
type: object
properties:
@@ -32390,6 +34135,81 @@ components:
- type: "null"
description: "Partial edit of one occurrence. `note: null` clears the note, `episodeId: null` drops the link. A changed
`episodeId` must name an open episode; an unchanged link to an episode that has since resolved is kept."
+ CreateLifeEventRequest:
+ type: object
+ properties:
+ category:
+ type: string
+ enum:
+ - FAMILY
+ - HOME
+ - WORK
+ - LOSS
+ - OTHER
+ startDate:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ endDate:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ precision:
+ type: string
+ enum:
+ - DAY
+ - MONTH
+ - YEAR
+ title:
+ type: string
+ minLength: 1
+ maxLength: 120
+ note:
+ anyOf:
+ - type: string
+ maxLength: 2000
+ - type: "null"
+ required:
+ - category
+ - startDate
+ - precision
+ - title
+ additionalProperties: false
+ UpdateLifeEventRequest:
+ type: object
+ properties:
+ category:
+ type: string
+ enum:
+ - FAMILY
+ - HOME
+ - WORK
+ - LOSS
+ - OTHER
+ startDate:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ endDate:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ precision:
+ type: string
+ enum:
+ - DAY
+ - MONTH
+ - YEAR
+ title:
+ type: string
+ minLength: 1
+ maxLength: 120
+ note:
+ anyOf:
+ - type: string
+ maxLength: 2000
+ - type: "null"
+ additionalProperties: false
MedicationTreatmentClass:
type: string
enum:
@@ -32621,48 +34441,51 @@ components:
cycle.bulk.too_large, cycle.calendar.invalid, cycle.calendar.range, cycle.cycles.invalid,
cycle.day-log.conflict, cycle.day-log.invalid, cycle.disabled, cycle.period.invalid,
cycle.symptom.custom.invalid, cycle.symptom.custom.limit; cycle-prefs — cycle-prefs.body.invalid_json,
- cycle-prefs.invalid; disable-coach — disable-coach.body.invalid_json; documents —
- documents.chat.conversationNotFound, documents.chat.invalid, documents.inbound.alreadyConfirmed,
- documents.inbound.alreadyPartlyConfirmed, documents.inbound.budgetExceeded,
- documents.inbound.decryptFailed, documents.inbound.encounterNotFound, documents.inbound.episodeNotFound,
- documents.inbound.extractFailed, documents.inbound.factNotFound, documents.inbound.factNotPending,
- documents.inbound.factTypeMismatch, documents.inbound.fileTooLarge, documents.inbound.fileType,
- documents.inbound.invalidMetadata, documents.inbound.invalidQuery, documents.inbound.invalidUpdate,
- documents.inbound.localOcrDisabled, documents.inbound.notFound, documents.inbound.notIndexed,
- documents.inbound.pdfNeedsAnthropic, documents.inbound.providerUnsupported,
- documents.inbound.quotaExceeded, documents.inbound.rateLimited, documents.inbound.restoreDuplicate,
- documents.inbound.restoreGone, documents.inbound.sourceAliasLimit, documents.inbound.uploadBusy,
- documents.inbound.uploadTimeout, documents.inbound.vaccinationNotFound, documents.sources.authRefused,
- documents.sources.badResponse, documents.sources.browserOnly, documents.sources.invalidAddress,
- documents.sources.linkTargetNotFound, documents.sources.notConnected, documents.sources.notFound,
- documents.sources.organizationRequired, documents.sources.originNotAllowed,
+ cycle-prefs.invalid; disable-coach — disable-coach.body.invalid_json; day — day.invalid; demo —
+ demo.readOnly; documents — documents.chat.conversationNotFound, documents.chat.invalid,
+ documents.inbound.alreadyConfirmed, documents.inbound.alreadyPartlyConfirmed,
+ documents.inbound.budgetExceeded, documents.inbound.decryptFailed, documents.inbound.encounterNotFound,
+ documents.inbound.episodeNotFound, documents.inbound.extractFailed, documents.inbound.factNotFound,
+ documents.inbound.factNotPending, documents.inbound.factTypeMismatch, documents.inbound.fileTooLarge,
+ documents.inbound.fileType, documents.inbound.invalidMetadata, documents.inbound.invalidQuery,
+ documents.inbound.invalidUpdate, documents.inbound.localOcrDisabled, documents.inbound.notFound,
+ documents.inbound.notIndexed, documents.inbound.pdfNeedsAnthropic,
+ documents.inbound.providerUnsupported, documents.inbound.quotaExceeded, documents.inbound.rateLimited,
+ documents.inbound.restoreDuplicate, documents.inbound.restoreGone, documents.inbound.sourceAliasLimit,
+ documents.inbound.uploadBusy, documents.inbound.uploadTimeout, documents.inbound.vaccinationNotFound,
+ documents.sources.authRefused, documents.sources.badResponse, documents.sources.browserOnly,
+ documents.sources.invalidAddress, documents.sources.linkTargetNotFound, documents.sources.notConnected,
+ documents.sources.notFound, documents.sources.organizationRequired, documents.sources.originNotAllowed,
documents.sources.permissionMissing, documents.sources.rateLimited, documents.sources.redirected,
documents.sources.tokenRequired, documents.sources.unavailable, documents.sources.unreachable,
documents.sources.versionTooOld; encounter — encounter.invalid, encounter.link.invalid,
encounter.practitioner-not-found, encounter.reminder-conflict, encounter.reminder-not-found; encryption
- — encryption.keyBackup.stale, encryption.key_mismatch; environment — environment.invalid,
- environment.no_home, environment.range_too_large, environment.travel.not_found; export —
- export.selection.empty, export.selection.unknown_leaf; familyHistory — familyHistory.invalid; feedback —
- feedback.body.invalid; health_score_config — health_score_config.invalid,
- health_score_config.too_narrow; illness — illness.day-log.invalid, illness.disabled,
- illness.episode.chronic-no-resolve, illness.episode.invalid, illness.episode.invalid-window,
- illness.episode.parent-not-found, illness.insights.invalid; import — import.apple_health.busy,
- import.write_failed; ingest — ingest.rate_limited; insights — insights.generate.budgetExceeded,
- insights.generate.outboundScreened, insights.generate.profileScopeChanged,
- insights.generate.scopeChanged; labs — labs.biomarker.create.invalid, labs.biomarker.duplicate,
- labs.biomarker.notFound, labs.biomarker.update.invalid, labs.biomarker.update.referenceRangeInvalid,
- labs.create.invalid, labs.ocr.budgetExceeded, labs.ocr.extractFailed, labs.ocr.fileTooLarge,
- labs.ocr.fileType, labs.ocr.localOcrDisabled, labs.ocr.pdfNeedsAnthropic, labs.ocr.providerUnsupported,
+ — encryption.keyBackup.stale, encryption.key_mismatch; environment — environment.backfill_pending,
+ environment.backfill_rate_limited, environment.invalid, environment.no_home,
+ environment.range_too_large, environment.travel.not_found; export — export.selection.empty,
+ export.selection.unknown_leaf; familyHistory — familyHistory.invalid; feedback — feedback.body.invalid;
+ health_score_config — health_score_config.invalid, health_score_config.too_narrow; illness —
+ illness.day-log.invalid, illness.disabled, illness.episode.chronic-no-resolve, illness.episode.invalid,
+ illness.episode.invalid-window, illness.episode.parent-not-found, illness.insights.invalid; import —
+ import.apple_health.busy, import.write_failed; ingest — ingest.rate_limited; insights —
+ insights.generate.budgetExceeded, insights.generate.outboundScreened,
+ insights.generate.profileScopeChanged, insights.generate.scopeChanged; labs —
+ labs.biomarker.create.invalid, labs.biomarker.duplicate, labs.biomarker.notFound,
+ labs.biomarker.update.invalid, labs.biomarker.update.referenceRangeInvalid, labs.create.invalid,
+ labs.ocr.budgetExceeded, labs.ocr.extractFailed, labs.ocr.fileTooLarge, labs.ocr.fileType,
+ labs.ocr.localOcrDisabled, labs.ocr.pdfNeedsAnthropic, labs.ocr.providerUnsupported,
labs.ocr.rateLimited, labs.restore.invalid, labs.result.notFound, labs.unit.mismatch,
labs.update.invalid, labs.update.linkedFieldsImmutable, labs.update.numericExpected,
labs.update.qualitativeExpected, labs.update.referenceRangeInvalid,
- labs.update.sourceReferenceRangeInvalid; managed_profile — managed_profile.guardian.duplicate,
- managed_profile.guardian.managed_invitee, managed_profile.guardian.required, managed_profile.not_found;
- measurement — measurement.batch.invalid, measurement.batch.source_not_permitted,
- measurement.batch.too_large, measurement.bulk-delete.invalid, measurement.create.invalid,
- measurement.create.source_not_permitted, measurement.delete.too_large, measurement.duplicate_timestamp,
- measurement.restore.invalid, measurement.update.server_owned_source; medication —
- medication.intake.import.invalid_format; medications — medications.category.invalid,
+ labs.update.sourceReferenceRangeInvalid; life_event — life_event.invalid, life_event.not_found;
+ managed_profile — managed_profile.guardian.duplicate, managed_profile.guardian.managed_invitee,
+ managed_profile.guardian.required, managed_profile.handover.no_pending,
+ managed_profile.handover.unknown_guardian, managed_profile.not_found; measurement —
+ measurement.batch.invalid, measurement.batch.source_not_permitted, measurement.batch.too_large,
+ measurement.bulk-delete.invalid, measurement.create.invalid, measurement.create.source_not_permitted,
+ measurement.delete.too_large, measurement.duplicate_timestamp, measurement.restore.invalid,
+ measurement.update.server_owned_source; medication — medication.intake.import.invalid_format,
+ medication.intake.notTracked; medications — medications.category.invalid,
medications.category.limitReached, medications.category.unknown, medications.course.currentOpen,
medications.course.invalid, medications.course.invalidRange, medications.course.notFound,
medications.course.oneShot, medications.course.overlap, medications.course.windowRequired,
@@ -32675,17 +34498,21 @@ components:
mood.create.external_id_not_delegable, mood.create.invalid, mood.duplicate_timestamp,
mood.linkedContext.invalid, mood.list.invalid, mood.not_found, mood.ratedFactor.out_of_range,
mood.restore.invalid, mood.update.invalid; notification-prefs — notification-prefs.body.invalid_json;
- nutrient — nutrient.batch.invalid, nutrient.batch.too_large, nutrient.daily.invalid,
- nutrient.read.invalid, nutrient.water.invalid, nutrient.water.invalid_day; onboarding —
- onboarding.answers.invalid, onboarding.answers.rateLimited, onboarding.complete.invalid,
- onboarding.complete.recordNotFound, onboarding.complete.recordTargetMismatch,
- onboarding.disclaimer.invalid, onboarding.restart.invalid, onboarding.restart.rateLimited,
- onboarding.tour.invalid; personal_records — personal_records.invalid_query; practitioner —
- practitioner.invalid; profile — profile.update.emailInUse, profile.update.emailRateLimited,
- profile.update.emailSecondFactorRequired, profile.update.invalidBody, profile.update.nothingSaved;
- record_write — record_write.rate_limited; report-selection — report-selection.body.invalid_json,
- report-selection.body.invalid_shape, report-selection.leaves.unknown; route — route.retired; share-link
- — share-link.document.decryptFailed, share-link.document.notFound, share-link.document.rateLimited,
+ notification — notification.action.already_used, notification.action.expired,
+ notification.action.invalid, notification.action.module_disabled; nutrient — nutrient.batch.invalid,
+ nutrient.batch.too_large, nutrient.daily.invalid, nutrient.read.invalid, nutrient.water.invalid,
+ nutrient.water.invalid_day; onboarding — onboarding.answers.invalid, onboarding.answers.rateLimited,
+ onboarding.complete.invalid, onboarding.complete.recordNotFound,
+ onboarding.complete.recordTargetMismatch, onboarding.disclaimer.invalid, onboarding.restart.invalid,
+ onboarding.restart.rateLimited, onboarding.tour.invalid; passkey — passkey.challenge.expired,
+ passkey.response.invalid, passkey.unknown, passkey.verification.failed; personal_records —
+ personal_records.invalid_query; practitioner — practitioner.invalid; profile —
+ profile.update.emailInUse, profile.update.emailRateLimited, profile.update.emailSecondFactorRequired,
+ profile.update.invalidBody, profile.update.nothingSaved; profile_claim — profile_claim.invalid,
+ profile_claim.oidc_only_unsupported, profile_claim.taken; record_write — record_write.rate_limited;
+ report-selection — report-selection.body.invalid_json, report-selection.body.invalid_shape,
+ report-selection.leaves.unknown; route — route.retired; search — search.invalid; share-link —
+ share-link.document.decryptFailed, share-link.document.notFound, share-link.document.rateLimited,
share-link.documents.invalid, share-link.report.notFound, share-link.report.rateLimited,
share-link.selection.forbidden_leaf, share-link.selection.unknown_leaf; sharing —
sharing.accept.expired, sharing.accept.not_found, sharing.accept.not_pending, sharing.accept.revoked,
@@ -32696,18 +34523,20 @@ components:
sharing.switch.wrong_transport; source-priority — source-priority.body.invalid_json,
source-priority.body.invalid_shape; symptoms — symptoms.definition.limitReached,
symptoms.definition.notFound, symptoms.episode.notOpen, symptoms.event.notFound, symptoms.invalid;
- tokens — tokens.documents.ceiling_reached, tokens.measurements.ceiling_reached,
- tokens.workouts.ceiling_reached; vaccination — vaccination.booster-invalid,
- vaccination.booster-no-antigen, vaccination.booster-out-of-scope, vaccination.encounter-not-found,
- vaccination.identity-required, vaccination.invalid, vaccination.link.invalid, vaccination.not-found,
+ timeline — timeline.invalid; tokens — tokens.documents.ceiling_reached,
+ tokens.measurements.ceiling_reached, tokens.workouts.ceiling_reached; vaccination —
+ vaccination.booster-invalid, vaccination.booster-no-antigen, vaccination.booster-out-of-scope,
+ vaccination.custom-vaccine-not-found, vaccination.custom.invalid, vaccination.custom.name-taken,
+ vaccination.custom.not-found, vaccination.encounter-not-found, vaccination.identity-required,
+ vaccination.invalid, vaccination.link.invalid, vaccination.not-found,
vaccination.practitioner-not-found; workout — workout.batch.invalid, workout.batch.payload_too_large,
workout.batch.source_not_permitted, workout.batch.too_large, workout.delete.synced_source; (unprefixed)
— about_me_conflict, ai_response_truncated, coach_prefs_conflict, dashboard_layout_conflict, empty_file,
health_score_config_conflict, insights_layout_conflict, invalid_base_updated_at,
json_carries_no_intake_time, json_not_an_array, medication_layout_conflict, missing_required_columns,
modules_conflict, mood_tag_layout_conflict, not_configured, notification_prefs_conflict, oidc_only,
- private_origin_not_approved, private_origin_not_grantable, rate_limited_self, too_many_rows,
- unreadable_json, upstream_invalid_json, vapid_not_configured"
+ private_origin_not_approved, private_origin_not_grantable, rate_limited_self, sync_in_progress,
+ too_many_rows, unreadable_json, upstream_invalid_json, vapid_not_configured"
type: string
capability:
description: "On an AI refusal: the capability that is unavailable, one of the keys of `AiCapabilities` (`coach`,
@@ -33824,6 +35653,9 @@ components:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
+ nEff:
+ description: v1.42 — effective sample size after serial correlation, ≤ `n`; the p-value uses it.
+ type: number
r:
type: number
pValue:
@@ -34922,6 +36754,14 @@ components:
type: boolean
assistantDocumentAiEnabled:
type: boolean
+ aiReasoningEnabled:
+ type: boolean
+ aiReasoningMaxEffort:
+ type: string
+ enum:
+ - low
+ - medium
+ - high
moduleAvailabilityJson:
anyOf:
- {}
@@ -35056,6 +36896,13 @@ components:
- type: string
minLength: 1
- type: "null"
+ homeLocationEncrypted:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
+ environmentAirQualityEnabled:
+ type: boolean
timezone:
type: string
locale:
@@ -35082,6 +36929,7 @@ components:
insightsLayoutJson: {}
medicationListLayoutJson: {}
moodTagLayoutJson: {}
+ documentsLayoutJson: {}
reportSelectionJson: {}
globalExcludedInjectionSites:
type: array
@@ -35277,6 +37125,7 @@ components:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
notes:
anyOf:
- type: string
@@ -35416,6 +37265,13 @@ components:
type: boolean
trackIntake:
type: boolean
+ autoMarkTaken:
+ type: boolean
+ autoMarkTakenSince:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
category:
type: string
maxLength: 64
@@ -36073,6 +37929,8 @@ components:
type: boolean
autoMissed:
type: boolean
+ autoRecorded:
+ type: boolean
attributionSource:
type: string
enum:
@@ -36605,6 +38463,54 @@ components:
anyOf:
- type: string
- type: "null"
+ note:
+ anyOf:
+ - type: string
+ - type: "null"
+ sensitive:
+ anyOf:
+ - anyOf:
+ - type: object
+ properties:
+ sexualActivity:
+ type: boolean
+ protectedSex:
+ anyOf:
+ - type: boolean
+ - type: "null"
+ pregnancyTest:
+ anyOf:
+ - type: string
+ enum:
+ - NEGATIVE
+ - POSITIVE
+ - INDETERMINATE
+ - type: "null"
+ progesteroneTest:
+ anyOf:
+ - type: string
+ enum:
+ - NEGATIVE
+ - POSITIVE
+ - INDETERMINATE
+ - type: "null"
+ contraceptive:
+ anyOf:
+ - type: string
+ enum:
+ - NONE
+ - UNSPECIFIED
+ - IMPLANT
+ - INJECTION
+ - IUD
+ - INTRAVAGINAL_RING
+ - ORAL
+ - PATCH
+ - EMERGENCY
+ - type: "null"
+ additionalProperties: false
+ - type: string
+ - type: "null"
source:
type: string
enum:
@@ -36623,6 +38529,7 @@ components:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
externalId:
anyOf:
- type: string
@@ -36708,6 +38615,10 @@ components:
anyOf:
- type: string
- type: "null"
+ label:
+ anyOf:
+ - type: string
+ - type: "null"
required:
- key
- labelKey
@@ -36744,6 +38655,10 @@ components:
anyOf:
- type: string
- type: "null"
+ label:
+ anyOf:
+ - type: string
+ - type: "null"
required:
- id
- key
@@ -36801,6 +38716,10 @@ components:
anyOf:
- type: string
- type: "null"
+ label:
+ anyOf:
+ - type: string
+ - type: "null"
scaleMin:
default: 1
type: integer
@@ -36872,6 +38791,14 @@ components:
anyOf:
- type: string
- type: "null"
+ sourceValue:
+ anyOf:
+ - type: number
+ - type: "null"
+ sourceUnit:
+ anyOf:
+ - type: string
+ - type: "null"
takenAt:
type: string
minLength: 1
@@ -36941,6 +38868,10 @@ components:
- type: "null"
hidden:
type: boolean
+ analyteKey:
+ anyOf:
+ - type: string
+ - type: "null"
context:
anyOf:
- type: string
@@ -37193,6 +39124,70 @@ components:
- updatedAt
- events
additionalProperties: {}
+ lifeEvents:
+ default: []
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ minLength: 1
+ category:
+ type: string
+ enum:
+ - FAMILY
+ - HOME
+ - WORK
+ - LOSS
+ - OTHER
+ startDate:
+ type: string
+ pattern: ^\d{4}-\d{2}-\d{2}$
+ endDate:
+ anyOf:
+ - type: string
+ pattern: ^\d{4}-\d{2}-\d{2}$
+ - type: "null"
+ precision:
+ type: string
+ enum:
+ - DAY
+ - MONTH
+ - YEAR
+ title:
+ type: string
+ minLength: 1
+ titleEncrypted:
+ type: string
+ minLength: 1
+ note:
+ anyOf:
+ - type: string
+ - type: "null"
+ noteEncrypted:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
+ createdAt:
+ type: string
+ minLength: 1
+ updatedAt:
+ type: string
+ minLength: 1
+ deletedAt:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
+ required:
+ - category
+ - startDate
+ - precision
+ - createdAt
+ - updatedAt
+ additionalProperties: {}
allergies:
default: []
type: array
@@ -37406,6 +39401,7 @@ components:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
externalId:
anyOf:
- type: string
@@ -38388,6 +40384,10 @@ components:
- type: string
minLength: 1
- type: "null"
+ note:
+ anyOf:
+ - type: string
+ - type: "null"
createdAt:
type: string
minLength: 1
@@ -38455,6 +40455,18 @@ components:
- type: string
minLength: 1
- type: "null"
+ reason:
+ anyOf:
+ - type: string
+ - type: "null"
+ outcome:
+ anyOf:
+ - type: string
+ - type: "null"
+ bodySite:
+ anyOf:
+ - type: string
+ - type: "null"
laterality:
anyOf:
- type: string
@@ -38546,6 +40558,51 @@ components:
- targetId
- createdAt
additionalProperties: {}
+ customVaccines:
+ default: []
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ minLength: 1
+ name:
+ type: string
+ minLength: 1
+ components:
+ type: array
+ items:
+ type: string
+ typicalSeriesDoses:
+ anyOf:
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ - type: "null"
+ boosterIntervalMonths:
+ anyOf:
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ - type: "null"
+ createdAt:
+ type: string
+ minLength: 1
+ updatedAt:
+ type: string
+ minLength: 1
+ deletedAt:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
+ required:
+ - id
+ - name
+ - createdAt
+ - updatedAt
+ additionalProperties: {}
vaccinations:
default: []
type: array
@@ -38606,11 +40663,19 @@ components:
anyOf:
- type: string
- type: "null"
+ customVaccineId:
+ anyOf:
+ - type: string
+ - type: "null"
noteEncrypted:
anyOf:
- type: string
minLength: 1
- type: "null"
+ note:
+ anyOf:
+ - type: string
+ - type: "null"
createdAt:
type: string
minLength: 1
@@ -38736,6 +40801,15 @@ components:
anyOf:
- type: string
- type: "null"
+ trailEncrypted:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
+ trailJson:
+ anyOf:
+ - type: string
+ - type: "null"
createdAt:
type: string
minLength: 1
@@ -38902,6 +40976,19 @@ components:
anyOf:
- type: string
- type: "null"
+ source:
+ type: string
+ minLength: 1
+ maxLength: 32
+ lastUsedAt:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
+ sourceMessageId:
+ anyOf:
+ - type: string
+ - type: "null"
createdAt:
type: string
minLength: 1
@@ -39399,6 +41486,7 @@ components:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
externalId:
anyOf:
- type: string
@@ -39442,11 +41530,22 @@ components:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
lat:
- type: number
+ anyOf:
+ - type: number
+ - type: "null"
lon:
- type: number
+ anyOf:
+ - type: number
+ - type: "null"
locationLabel:
- type: string
+ anyOf:
+ - type: string
+ - type: "null"
+ locationEncrypted:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
source:
type: string
enum:
@@ -39507,6 +41606,97 @@ components:
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
+ apparentMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ pm25Mean:
+ anyOf:
+ - type: number
+ - type: "null"
+ pm25Max:
+ anyOf:
+ - type: number
+ - type: "null"
+ pm10Mean:
+ anyOf:
+ - type: number
+ - type: "null"
+ no2Mean:
+ anyOf:
+ - type: number
+ - type: "null"
+ so2Mean:
+ anyOf:
+ - type: number
+ - type: "null"
+ coMean:
+ anyOf:
+ - type: number
+ - type: "null"
+ o3Max8h:
+ anyOf:
+ - type: number
+ - type: "null"
+ eaqiMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ usaqiMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ uvIndexMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ dustMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ aodMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ pollenAlderMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ pollenBirchMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ pollenGrassMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ pollenMugwortMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ pollenOliveMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ pollenRagweedMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ aqDomain:
+ anyOf:
+ - type: string
+ - type: "null"
+ aqHours:
+ anyOf:
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ - type: "null"
+ aqFetchedAt:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
fetchedAt:
type: string
minLength: 1
@@ -39536,11 +41726,22 @@ components:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
lat:
- type: number
+ anyOf:
+ - type: number
+ - type: "null"
lon:
- type: number
+ anyOf:
+ - type: number
+ - type: "null"
label:
- type: string
+ anyOf:
+ - type: string
+ - type: "null"
+ locationEncrypted:
+ anyOf:
+ - type: string
+ minLength: 1
+ - type: "null"
createdAt:
type: string
minLength: 1
@@ -39581,6 +41782,7 @@ components:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
externalRecordingId:
type: string
minLength: 1
@@ -39710,6 +41912,7 @@ components:
- biomarkers
- illnessEpisodes
- symptomDefinitions
+ - lifeEvents
- allergies
- familyHistory
- workouts
@@ -39729,6 +41932,7 @@ components:
- encounterDocumentLinks
- encounterLabLinks
- encounterConditionLinks
+ - customVaccines
- vaccinations
- vaccinationDocumentLinks
- coachConversations
@@ -39761,6 +41965,91 @@ components:
omissions there is a real, undeclared gap: roughly thirty of the models the backup plan marks as backed up have
neither a reader here nor a restore branch yet, and `src/lib/export/backup-plan.ts` is the enumeration of which.
`schemaVersion` is `2` on everything written today; the parser still accepts `1`."
+ ProfileClaimPreviewEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/ProfileClaimPreview"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ ProfileClaimPreview:
+ type: object
+ properties:
+ displayName:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: The profile's name, as its guardians set it.
+ expiresAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ guardians:
+ type: array
+ items:
+ type: object
+ properties:
+ displayName:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: "The guardian's display name; null when they set none. Never a login name or an id: the holder of the link
+ has proved nothing yet."
+ proposal:
+ $ref: "#/components/schemas/ProfileClaimGuardianAccess"
+ required:
+ - displayName
+ - proposal
+ additionalProperties: false
+ description: Each guardian and the access they keep after the claim, as proposed. The new owner confirms or changes it
+ after signing in.
+ required:
+ - displayName
+ - expiresAt
+ - guardians
+ additionalProperties: false
+ ProfileClaimGuardianAccess:
+ type: string
+ enum:
+ - end
+ - read
+ - manage
+ ProfileClaimEnvelope:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ userId:
+ type: string
+ username:
+ type: string
+ required:
+ - userId
+ - username
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
LoginResponse:
type: object
properties:
@@ -41288,19 +43577,7 @@ components:
accepted once the window rolls over."
type: array
items:
- type: object
- properties:
- path:
- type: string
- code:
- type: string
- message:
- type: string
- required:
- - path
- - code
- - message
- additionalProperties: false
+ $ref: "#/components/schemas/ValidationIssue"
required:
- id
- username
@@ -41883,6 +44160,7 @@ components:
- GOOGLE_HEALTH
- STRAVA
- EXTERNAL
+ - HEALTH_CONNECT
description: Origin of the measurement. v1.4.23 added APPLE_HEALTH for the iOS HealthKit batch ingest path. v1.38.x
added EXTERNAL for rows pushed in under a narrow `measurements:write` Bearer — a scale, a watch bridge, a
home-automation rule — and workouts pushed in under a narrow `workouts:write` Bearer carry it too. No client may
@@ -41917,6 +44195,8 @@ components:
- updatedAt
additionalProperties: false
description: A soft-deleted measurement. Apply tombstones BEFORE upserts within a page to avoid resurrecting a row.
+ Since v1.42 only deletions a person makes appear here; the nightly fold that condenses raw samples into `stats:`
+ rows deletes them outright and lists nothing.
SyncMoodUpsert:
type: object
properties:
@@ -42061,6 +44341,9 @@ components:
type: boolean
source:
type: string
+ autoRecorded:
+ type: boolean
+ description: v1.42.2 — the server recorded this dose because the medication has `autoMarkTaken` on. Counts as taken.
syncVersion:
type: integer
minimum: 0
@@ -42077,6 +44360,7 @@ components:
- takenAt
- skipped
- source
+ - autoRecorded
- syncVersion
- updatedAt
additionalProperties: false
@@ -42934,6 +45218,9 @@ components:
an existing row. `skipped` — validation no-op; see `reason`. `failed` — retryable database failure that must
not advance the entry cursor; the response is marked `Cache-Control: no-store`."
reason:
+ description: "Why a row was `skipped` or a `duplicate`. `folded_window` (since v1.42): a raw sample of a dense or mean
+ type whose local day lies before the fold boundary and whose hour or day a live `stats:` row of the same
+ source already covers. Nothing to retry; treat it like any duplicate."
type: string
convertedValue:
description: "Only on a `value_out_of_range` skip (since v1.39.1): the entry's value after conversion into the stored
@@ -43814,6 +46101,22 @@ components:
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
+ storedAvgHr:
+ description: The row's own average heart rate (v1.42). `avgHr` may be filled in from a same-session twin by the
+ canonical picker; this is the stored value, which an edit of a hand-entered workout sends back so the twin's
+ value is not written onto it.
+ anyOf:
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ - type: "null"
+ storedMaxHr:
+ description: The row's own maximum heart rate (v1.42), as `storedAvgHr` is to `avgHr`.
+ anyOf:
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ - type: "null"
stepCount:
anyOf:
- type: integer
@@ -44457,6 +46760,12 @@ components:
type: boolean
description: True when the nightly cron closed the slot as missed (no user action). Auto-missed rows count against
adherence but never consume inventory.
+ autoRecorded:
+ type: boolean
+ description: v1.42.2 — true when the server recorded this dose because the medication has `autoMarkTaken` on, not the
+ person. It counts as taken everywhere (adherence, history, Coach) and consumed inventory like a manual take.
+ Any take or skip the person records on the row clears it. Clients label such a dose "recorded
+ automatically".
attributionSource:
type: string
enum:
@@ -44538,6 +46847,7 @@ components:
- takenAt
- skipped
- autoMissed
+ - autoRecorded
- attributionSource
- source
- idempotencyKey
@@ -45298,6 +47608,11 @@ components:
description: "`inserted`/`updated`/`duplicate` — the row landed (advance the cursor). `skipped` — not stored; see
`reason`."
reason:
+ description: "Why a `skipped` entry was not stored. Among others: `medication_not_found`, `unstable_external_id`,
+ `force_slot_invalid`, `force_slot_occupied`, `injection_site_not_allowed`, and (v1.42.0)
+ `intake_not_tracked` for an entry against a medication kept as a record only (`trackIntake: false`), the
+ per-entry form of `medication.intake.notTracked`. A skip is final: resending the entry gives the same
+ answer."
type: string
id:
description: The landed row id (absent on skips).
@@ -45396,6 +47711,13 @@ components:
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
+ remindersActive:
+ type: boolean
+ description: "v1.42.2 — whether the server sends dose reminders for this medication: true only when
+ `notificationsEnabled` is on, the medication is `active`, its course has not ended (`endsOn` is today or
+ later on the person's clock, or unset), its intake is tracked (`trackIntake`), it is not `asNeeded`, and
+ `autoMarkTaken` is off. The same decision gates every server-side reminder send. A client that arms local
+ reminders arms them from this, not from `notificationsEnabled`. Read-only — computed, not stored."
nextDueAt:
anyOf:
- type: string
@@ -45436,6 +47758,23 @@ components:
any channel, and it is excluded from every adherence figure. Its stored schedule then travels in
`recordedSchedules` while `schedules` is served empty, so a client that arms reminders from `schedules`
stays silent without an update."
+ autoMarkTaken:
+ type: boolean
+ description: "v1.42.2 — doses are recorded automatically. False (the default) for every medication that existed before
+ the field. When true the server records each scheduled dose as taken (an intake with `autoRecorded: true`,
+ `takenAt` = the slot time) shortly after its time passes, from `autoMarkTakenSince` on. A slot the person
+ took, skipped or removed is never touched, and they can still change or skip any dose. No medication
+ reminder is sent for it on any channel while it is on; a client that arms local reminders should not arm
+ them for it either. `notificationsEnabled` keeps the person's own setting and applies again once this is
+ switched off."
+ autoMarkTakenSince:
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
+ description: v1.42.2 — when `autoMarkTaken` was last switched on. No slot anchored before it is ever recorded
+ automatically. NULL while the option is off. Read-only — set by the server on the switch.
createdAt:
type: string
format: date-time
@@ -45567,6 +47906,7 @@ components:
- rxNormCode
- pausedAt
- snoozedUntil
+ - remindersActive
- nextDueAt
- nextDueOverdue
- startsOn
@@ -45574,6 +47914,8 @@ components:
- oneShot
- asNeeded
- trackIntake
+ - autoMarkTaken
+ - autoMarkTakenSince
- createdAt
- updatedAt
- schedules
@@ -45738,6 +48080,9 @@ components:
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
+ remindersActive:
+ type: boolean
+ description: v1.42.2 — whether the server sends dose reminders for this medication. See the `Medication` component.
nextDueAt:
description: "Present on the READ paths only — the list and single GET compute it; the create / update responses return
the stored row and omit it. v1.7.0 server-computed next due instant across all the medication's schedules
@@ -45789,6 +48134,23 @@ components:
any channel, and it is excluded from every adherence figure. Its stored schedule then travels in
`recordedSchedules` while `schedules` is served empty, so a client that arms reminders from `schedules`
stays silent without an update."
+ autoMarkTaken:
+ type: boolean
+ description: "v1.42.2 — doses are recorded automatically. False (the default) for every medication that existed before
+ the field. When true the server records each scheduled dose as taken (an intake with `autoRecorded: true`,
+ `takenAt` = the slot time) shortly after its time passes, from `autoMarkTakenSince` on. A slot the person
+ took, skipped or removed is never touched, and they can still change or skip any dose. No medication
+ reminder is sent for it on any channel while it is on; a client that arms local reminders should not arm
+ them for it either. `notificationsEnabled` keeps the person's own setting and applies again once this is
+ switched off."
+ autoMarkTakenSince:
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
+ description: v1.42.2 — when `autoMarkTaken` was last switched on. No slot anchored before it is ever recorded
+ automatically. NULL while the option is off. Read-only — set by the server on the switch.
createdAt:
type: string
format: date-time
@@ -45890,11 +48252,14 @@ components:
- rxNormCode
- pausedAt
- snoozedUntil
+ - remindersActive
- startsOn
- endsOn
- oneShot
- asNeeded
- trackIntake
+ - autoMarkTaken
+ - autoMarkTakenSince
- createdAt
- updatedAt
- schedules
@@ -46892,12 +49257,33 @@ components:
meta:
type: object
properties:
+ aggregate:
+ anyOf:
+ - $ref: "#/components/schemas/MedicationComplianceAggregate"
+ - type: "null"
+ days:
+ description: The `days` the request asked for.
+ anyOf:
+ - type: number
+ const: 30
+ - type: number
+ const: 60
+ - type: number
+ const: 90
+ - type: number
+ const: 180
+ - type: number
+ const: 365
requestId:
type: string
+ required:
+ - aggregate
additionalProperties: false
+ description: Absent on servers older than v1.42.0; a client that reads it treats absence as no aggregate.
required:
- data
- error
+ - meta
additionalProperties: false
MedicationComplianceSummaryEntry:
type: object
@@ -46920,6 +49306,18 @@ components:
compliance30:
description: Thirty-day adherence summary, or an all-zero compatibility placeholder when applicable is false.
$ref: "#/components/schemas/ComplianceResult"
+ compliance60:
+ description: "Present only on `?days=60`: the trailing 60 days."
+ $ref: "#/components/schemas/ComplianceResult"
+ compliance90:
+ description: "Present only on `?days=90`: the trailing 90 days."
+ $ref: "#/components/schemas/ComplianceResult"
+ compliance180:
+ description: "Present only on `?days=180`: the trailing 180 days."
+ $ref: "#/components/schemas/ComplianceResult"
+ compliance365:
+ description: "Present only on `?days=365`: the trailing 365 days."
+ $ref: "#/components/schemas/ComplianceResult"
complianceDisplay:
anyOf:
- $ref: "#/components/schemas/ComplianceDisplay"
@@ -47145,6 +49543,45 @@ components:
additionalProperties: false
description: The two-row card block whose windows scale with dosing cadence (dense meds keep 7 / 30 days, sparse meds
step both windows up). NOT the 30-day denominator — read `compliance30.totalExpected` for that.
+ MedicationComplianceAggregate:
+ type: object
+ properties:
+ compliance7:
+ $ref: "#/components/schemas/ComplianceResult"
+ compliance30:
+ $ref: "#/components/schemas/ComplianceResult"
+ compliance60:
+ anyOf:
+ - $ref: "#/components/schemas/ComplianceResult"
+ - type: "null"
+ compliance90:
+ anyOf:
+ - $ref: "#/components/schemas/ComplianceResult"
+ - type: "null"
+ compliance180:
+ anyOf:
+ - $ref: "#/components/schemas/ComplianceResult"
+ - type: "null"
+ compliance365:
+ anyOf:
+ - $ref: "#/components/schemas/ComplianceResult"
+ - type: "null"
+ medicationCount:
+ type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ description: How many applicable medications the figure is built from.
+ required:
+ - compliance7
+ - compliance30
+ - medicationCount
+ additionalProperties: false
+ description: "One adherence figure for the whole account in the `ComplianceResult` shape, weighted by expected doses:
+ `taken`, `skipped` and `missed` are summed across every medication with `applicable: true`, and `rate` is
+ recomputed from the sums by the per-medication rule (round(100 · taken / (taken + missed)), capped at 100, 100
+ when nothing was expected). Averaging the per-medication rates would let a weekly injection weigh as much as a
+ three-times-daily tablet. `streak` is the shortest per-medication streak. The `complianceN` block for a
+ requested `days` is present when `days` was sent and is not 30."
GetMedicationComplianceResponse:
type: object
properties:
@@ -47748,6 +50185,9 @@ components:
type: boolean
autoMissed:
type: boolean
+ autoRecorded:
+ type: boolean
+ description: v1.42.2 — the dose was recorded automatically (`autoMarkTaken`), not by the person.
doseTaken:
anyOf:
- type: string
@@ -47773,6 +50213,7 @@ components:
- takenAt
- skipped
- autoMissed
+ - autoRecorded
- doseTaken
- source
additionalProperties: false
@@ -48543,6 +50984,27 @@ components:
- type: "null"
description: v1.22 (#89) — per-user response timeout for AI generation, in seconds (10–600). Null = the built-in
comprehensive-briefing default (~120 s). Mainly for slow local/self-hosted backends.
+ localReasoningEffort:
+ anyOf:
+ - type: string
+ enum:
+ - none
+ - low
+ - medium
+ - high
+ - type: "null"
+ description: "#1126 — reasoning setting of the Local provider entry, sent as `reasoning_effort` (`none` = Off). Null =
+ Default: the key is not sent."
+ compatReasoningEffort:
+ anyOf:
+ - type: string
+ enum:
+ - none
+ - low
+ - medium
+ - high
+ - type: "null"
+ description: "#1126 — the same for the OpenAI-compatible gateway entry."
serverProviderHealth:
type: string
enum:
@@ -48587,6 +51049,8 @@ components:
- compatModel
- hasCompatKey
- responseTimeoutSeconds
+ - localReasoningEffort
+ - compatReasoningEffort
- serverProviderHealth
- serverProviderOffer
- serverProviderConsent
@@ -48679,7 +51143,10 @@ components:
- last7days
- last30days
- last90days
+ - lastYear
- allTime
+ description: "How far back the Coach may read, in every answer: snapshot, retrieval tools and summaries alike. `allTime`
+ (the default) is no limit. Formerly only the starting window; `lastYear` came with the limit."
dataClusters:
maxItems: 10
type: array
@@ -48728,6 +51195,16 @@ components:
description: v1.39.4 — offer up to three follow-up chips under the Coach's latest answer. Absent means on; only `false`
switches them off.
type: boolean
+ reasoning:
+ description: "v1.41 — how hard the Coach thinks before it answers: `off`, `low`, `medium` or `high`. Absent means
+ `medium`. The operator may switch reasoning off or cap the level; the resolved value is published on `GET
+ /api/auth/me` as `coachReasoning`."
+ type: string
+ enum:
+ - off
+ - low
+ - medium
+ - high
updatedAt:
description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as
`baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or
@@ -49002,6 +51479,9 @@ components:
vaccinations:
type: boolean
description: Whether the "vaccinations" module is enabled for this account.
+ timeline:
+ type: boolean
+ description: Whether the "timeline" module is enabled for this account.
required:
- cycle
- mood
@@ -49022,6 +51502,7 @@ components:
- mentalHealth
- nutrients
- vaccinations
+ - timeline
additionalProperties: false
description: Fully-resolved module enable/disable map. Every toggleable module key is present. `false` means the surface
should disappear end-to-end (nav, dashboard, insights, …). `cycle` mirrors `cycleTrackingEnabled`; `coach`
@@ -49051,840 +51532,7 @@ components:
type: object
properties:
data:
- type: object
- properties:
- steps:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- activeEnergy:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- walkingRunningDistance:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- flightsClimbed:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- sleep:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- weight:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bloodPressure:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- pulse:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bodyFat:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bodyTemperature:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- spo2:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- hrv:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- restingHeartRate:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- vo2Max:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- skinTemperature:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- respiratoryRate:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- recovery:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- stress:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- sleepDebt:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- metricPriority:
- type: object
- properties:
- steps:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- activeEnergy:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- walkingRunningDistance:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- flightsClimbed:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- sleep:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- weight:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bloodPressure:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- pulse:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bodyFat:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bodyTemperature:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- spo2:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- hrv:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- restingHeartRate:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- vo2Max:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- skinTemperature:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- respiratoryRate:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- recovery:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- stress:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- sleepDebt:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- additionalProperties: false
- deviceTypePriority:
- type: object
- properties:
- default:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - watch
- - band
- - ring
- - phone
- - scale
- - other
- - unknown
- additionalProperties:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - watch
- - band
- - ring
- - phone
- - scale
- - other
- - unknown
- additionalProperties: false
+ $ref: "#/components/schemas/ResolvedSourcePriority"
error:
type: "null"
meta:
@@ -49897,844 +51545,953 @@ components:
- data
- error
additionalProperties: false
+ ResolvedSourcePriority:
+ type: object
+ properties:
+ steps:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ activeEnergy:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ walkingRunningDistance:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ flightsClimbed:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ sleep:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ weight:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ bloodPressure:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ pulse:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ bodyFat:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ bodyTemperature:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ spo2:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ hrv:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ restingHeartRate:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ vo2Max:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ skinTemperature:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ respiratoryRate:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ recovery:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ stress:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ sleepDebt:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ hrvRmssd:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ metricPriority:
+ type: object
+ properties:
+ steps:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ activeEnergy:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ walkingRunningDistance:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ flightsClimbed:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ sleep:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ weight:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ bloodPressure:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ pulse:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ bodyFat:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ bodyTemperature:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ spo2:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ hrv:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ restingHeartRate:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ vo2Max:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ skinTemperature:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ respiratoryRate:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ recovery:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ stress:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ sleepDebt:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ hrvRmssd:
+ maxItems: 16
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ additionalProperties: false
+ deviceTypePriority:
+ type: object
+ properties:
+ default:
+ maxItems: 8
+ type: array
+ items:
+ type: string
+ enum:
+ - watch
+ - band
+ - ring
+ - phone
+ - scale
+ - other
+ - unknown
+ additionalProperties:
+ maxItems: 8
+ type: array
+ items:
+ type: string
+ enum:
+ - watch
+ - band
+ - ring
+ - phone
+ - scale
+ - other
+ - unknown
+ inUse:
+ type: array
+ items:
+ type: string
+ enum:
+ - MANUAL
+ - WITHINGS
+ - IMPORT
+ - APPLE_HEALTH
+ - COMPUTED
+ - WHOOP
+ - FITBIT
+ - NIGHTSCOUT
+ - POLAR
+ - OURA
+ - TELEGRAM
+ - MCP
+ - GOOGLE_HEALTH
+ - STRAVA
+ - EXTERNAL
+ - HEALTH_CONNECT
+ description: Ranked sources this account has connected or holds at least one measurement from, in ladder-catalogue
+ order. Read-only; an editor may list only these while the ladders keep every source.
+ required:
+ - inUse
+ additionalProperties: false
PutSourcePriorityResponse:
type: object
properties:
data:
- type: object
- properties:
- steps:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- activeEnergy:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- walkingRunningDistance:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- flightsClimbed:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- sleep:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- weight:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bloodPressure:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- pulse:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bodyFat:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bodyTemperature:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- spo2:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- hrv:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- restingHeartRate:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- vo2Max:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- skinTemperature:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- respiratoryRate:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- recovery:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- stress:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- sleepDebt:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- metricPriority:
- type: object
- properties:
- steps:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- activeEnergy:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- walkingRunningDistance:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- flightsClimbed:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- sleep:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- weight:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bloodPressure:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- pulse:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bodyFat:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- bodyTemperature:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- spo2:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- hrv:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- restingHeartRate:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- vo2Max:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- skinTemperature:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- respiratoryRate:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- recovery:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- stress:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- sleepDebt:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - MANUAL
- - WITHINGS
- - IMPORT
- - APPLE_HEALTH
- - COMPUTED
- - WHOOP
- - FITBIT
- - NIGHTSCOUT
- - POLAR
- - OURA
- - TELEGRAM
- - MCP
- - GOOGLE_HEALTH
- - STRAVA
- - EXTERNAL
- additionalProperties: false
- deviceTypePriority:
- type: object
- properties:
- default:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - watch
- - band
- - ring
- - phone
- - scale
- - other
- - unknown
- additionalProperties:
- maxItems: 8
- type: array
- items:
- type: string
- enum:
- - watch
- - band
- - ring
- - phone
- - scale
- - other
- - unknown
- additionalProperties: false
+ $ref: "#/components/schemas/ResolvedSourcePriority"
error:
type: "null"
meta:
@@ -50997,19 +52754,7 @@ components:
it was published only on PUT /api/auth/profile."
type: array
items:
- type: object
- properties:
- path:
- type: string
- code:
- type: string
- message:
- type: string
- required:
- - path
- - code
- - message
- additionalProperties: false
+ $ref: "#/components/schemas/ValidationIssue"
required:
- username
- email
@@ -51055,8 +52800,10 @@ components:
properties:
clientManaged:
type: boolean
- description: True when the iOS app owns local medication reminders; the server-side MEDICATION_REMINDER APNs cron is
- suppressed.
+ description: True when the iOS app delivers medication reminders locally. The server then keeps the reminder that
+ announces a dose off APNs (the phone shows its own at dose time) and still pushes the follow-ups for a
+ dose that stays open, which the phone does not send (v1.42.0; before, every MEDICATION_REMINDER push was
+ suppressed). Other channels are never affected.
deliveryDefault:
type: string
enum:
@@ -51131,6 +52878,19 @@ components:
- nudgeRoutine
- nudgeFrequency
additionalProperties: false
+ content:
+ type: object
+ properties:
+ detail:
+ type: string
+ enum:
+ - generic
+ - detailed
+ description: What a notification may show. "generic" (default) names what it is about with no health figure; "detailed"
+ adds figures such as the morning score. Tap targets and quick answers are the same in both.
+ required:
+ - detail
+ additionalProperties: false
updatedAt:
description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as
`baseUpdatedAt` on the next write. Opaque — only ever echo a server-returned value, never parse or
@@ -51143,6 +52903,7 @@ components:
- mood
- cycle
- coach
+ - content
additionalProperties: false
PatchNotificationPrefsResponse:
type: object
@@ -52009,6 +53770,12 @@ components:
type: string
usage:
$ref: "#/components/schemas/CoachUsage"
+ stop:
+ $ref: "#/components/schemas/CoachStop"
+ withheldResults:
+ description: v1.41 — the turn was blocked after interim tables went out; remove them.
+ type: boolean
+ const: true
required:
- type
- conversationId
@@ -52056,6 +53823,10 @@ components:
const: result
result:
$ref: "#/components/schemas/CoachResultTable"
+ interim:
+ description: v1.41 — sent while the turn still runs.
+ type: boolean
+ const: true
required:
- type
- result
@@ -52084,9 +53855,43 @@ components:
- type
- clarification
additionalProperties: false
+ - type: object
+ properties:
+ type:
+ type: string
+ const: activity
+ activity:
+ $ref: "#/components/schemas/CoachActivity"
+ required:
+ - type
+ - activity
+ additionalProperties: false
+ - type: object
+ properties:
+ type:
+ type: string
+ const: memoryNote
+ note:
+ $ref: "#/components/schemas/CoachMemoryNote"
+ required:
+ - type
+ - note
+ additionalProperties: false
+ - type: object
+ properties:
+ type:
+ type: string
+ const: planProposal
+ proposal:
+ $ref: "#/components/schemas/CoachPlanProposal"
+ required:
+ - type
+ - proposal
+ additionalProperties: false
description: "One Server-Sent Events frame of a Coach turn, sent as `data: \\n\\n` and dispatched on `type`.
- Order: `step*` → `token*` → `provenance` → `result*` → `suggestion?` → `suggestedAction?` → `clarification?` →
- `followUps?` → `done`, or a single `error`. Clients ignore a `type` they do not know."
+ Order: `(activity | step)*` → `token*` → `provenance` → `result*` → `suggestion?` → `suggestedAction?` →
+ `memoryNote?` → `planProposal?` → `clarification?` → `followUps?` → `done`, or a single `error`. `result` frames
+ with `interim: true` may also arrive among the `activity` frames. Clients ignore a `type` they do not know."
type: object
CoachProvenance:
type: object
@@ -52295,6 +54100,25 @@ components:
that earlier assistant message. A continuation offers no further `continue` chip. Absent otherwise."
type: string
maxLength: 64
+ activity:
+ description: v1.41 — the turn's trail, metadata only.
+ type: array
+ items:
+ $ref: "#/components/schemas/CoachActivityMeta"
+ stop:
+ description: v1.41 — why the answer was forced, when it was.
+ $ref: "#/components/schemas/CoachStop"
+ assumptions:
+ description: v1.41 — what the answer assumed instead of asking, at most two.
+ type: array
+ items:
+ $ref: "#/components/schemas/CoachAssumption"
+ memoryNote:
+ description: v1.41 — the fact this reply saved or proposes, without its text.
+ $ref: "#/components/schemas/CoachMemoryNoteMeta"
+ planProposal:
+ description: v1.41 — the plan this reply proposes, without its text.
+ $ref: "#/components/schemas/CoachPlanProposalMeta"
required:
- windows
- metrics
@@ -52374,7 +54198,7 @@ components:
id:
type: string
pattern: ^s[1-9]\d{0,2}$
- description: "`s1`..`s12`, unique within a turn."
+ description: "`s1`..`s64`, unique within a turn."
tool:
type: string
enum:
@@ -52388,6 +54212,8 @@ components:
- get_cycle
- get_correlations
- get_metric_table
+ - get_environment
+ - get_day
- show_result
- snapshot
labelKey:
@@ -52442,7 +54268,10 @@ components:
resultRef:
description: The table this step produced (`r1`..), when it produced one.
type: string
- pattern: ^r[1-6]$
+ pattern: ^r[1-8]$
+ day:
+ description: The calendar date a `get_day` step read (`YYYY-MM-DD`), so a view can offer the days an answer looked at.
+ type: string
required:
- id
- tool
@@ -52500,9 +54329,11 @@ components:
- illness
- cycle
- correlations
+ - environment
+ - day
- snapshot
description: "The data domain a step, table or chip is about: a measurement-backed scope source, or a domain read as a
- whole (labs, illness, cycle, correlations, the full snapshot)."
+ whole (labs, illness, cycle, correlations, the environment, one day of the record, the full snapshot)."
CoachMethod:
type: object
properties:
@@ -52568,8 +54399,8 @@ components:
properties:
ref:
type: string
- pattern: ^r[1-6]$
- description: "`r1`..`r6`, unique within a message."
+ pattern: ^r[1-8]$
+ description: "`r1`..`r8`, unique within a message."
source:
type: object
properties:
@@ -52586,6 +54417,8 @@ components:
- get_cycle
- get_correlations
- get_metric_table
+ - get_environment
+ - get_day
domain:
$ref: "#/components/schemas/CoachStepDomain"
window:
@@ -52635,6 +54468,7 @@ components:
- type: string
enum:
- line
+ - compare
- bar
- histogram
- type: "null"
@@ -52651,7 +54485,7 @@ components:
pattern: ^[A-Za-z0-9_-]{1,64}$
ref:
type: string
- pattern: ^r[1-6]$
+ pattern: ^r[1-8]$
required:
- messageId
- ref
@@ -52690,6 +54524,7 @@ components:
- as_table
- related_metric
- continue
+ - change_assumption
labelKey:
type: string
label:
@@ -52700,7 +54535,7 @@ components:
properties:
ref:
type: string
- pattern: ^(?:r[1-6])?$
+ pattern: ^(?:r[1-8])?$
domain:
$ref: "#/components/schemas/CoachStepDomain"
window:
@@ -52735,6 +54570,91 @@ components:
enum:
- server
- model
+ assumption:
+ description: "v1.41 — on a `change_assumption` chip: the assumption it replaces and the alternative it picks."
+ type: object
+ properties:
+ kind:
+ type: string
+ enum:
+ - metric
+ - window
+ - comparison
+ value:
+ type: object
+ properties:
+ metric:
+ type: string
+ enum:
+ - bp
+ - weight
+ - pulse
+ - mood
+ - compliance
+ - hrv
+ - sleep
+ - resting_hr
+ - steps
+ - active_energy
+ - flights
+ - distance
+ - vo2_max
+ - body_temp
+ - walking_hr
+ - respiratory_rate
+ - spo2
+ - pulse_wave_velocity
+ - vascular_age
+ - body_fat
+ - fat_mass
+ - fat_free_mass
+ - muscle_mass
+ - lean_body_mass
+ - bone_mass
+ - total_body_water
+ - bmi
+ - visceral_fat
+ - glucose
+ - walking_steadiness
+ - walking_asymmetry
+ - walking_double_support
+ - walking_step_length
+ - walking_speed
+ - audio_env
+ - audio_headphone
+ - audio_event
+ - daylight
+ - skin_temp
+ - workouts
+ window:
+ type: string
+ enum:
+ - last7days
+ - last30days
+ - last90days
+ - lastYear
+ - allTime
+ comparison:
+ description: v1.41 — what a comparison is drawn against.
+ type: string
+ enum:
+ - previous_period
+ - year_ago
+ - baseline_90d
+ goal:
+ description: v1.41 — an active plan or a goal fact, by id.
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ anchor:
+ description: v1.41 — a medication start, an illness episode or a cycle phase, by id.
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ additionalProperties: false
+ description: "What a choice stands for: catalog tokens and ids, never text."
+ required:
+ - kind
+ - value
+ additionalProperties: false
required:
- id
- kind
@@ -52753,7 +54673,11 @@ components:
enum:
- metric
- window
+ - comparison
+ - goal
+ - anchor
- context
+ description: "`comparison`, `goal` and `anchor` since v1.41."
choices:
type: array
items:
@@ -52820,7 +54744,23 @@ components:
- last90days
- lastYear
- allTime
+ comparison:
+ description: v1.41 — what a comparison is drawn against.
+ type: string
+ enum:
+ - previous_period
+ - year_ago
+ - baseline_90d
+ goal:
+ description: v1.41 — an active plan or a goal fact, by id.
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ anchor:
+ description: v1.41 — a medication start, an illness episode or a cycle phase, by id.
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
additionalProperties: false
+ description: "What a choice stands for: catalog tokens and ids, never text."
required:
- id
- labelKey
@@ -52831,6 +54771,11 @@ components:
freeText:
type: boolean
description: Whether a typed answer is welcome.
+ assumption:
+ description: v1.41 — the choice that applies when the person does not answer. The question names it, and it is the first
+ choice.
+ type: string
+ pattern: ^c[1-4]$
required:
- kind
- choices
@@ -52838,6 +54783,328 @@ components:
additionalProperties: false
description: "The choices for a clarifying question. The question itself is the assistant message's text. Answer with
`clarification: { messageId, choiceId? }`."
+ CoachActivityMeta:
+ type: object
+ properties:
+ id:
+ type: string
+ pattern: ^a[1-9]\d?$
+ description: "`a1`..`a99`, unique within a turn."
+ phase:
+ type: string
+ enum:
+ - thinking
+ - memory
+ - fetch
+ - digest
+ - checkpoint
+ - remember
+ - plan
+ - asking
+ - stop
+ - answer
+ status:
+ type: string
+ enum:
+ - running
+ - done
+ - empty
+ - failed
+ round:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: The tool round, from 1.
+ labelKey:
+ type: string
+ description: Closed catalog key (`insights.coach.activity.*`).
+ label:
+ type: string
+ description: Rendered on the server in the request locale from the catalog; never model text.
+ stepRef:
+ description: The `step` a `fetch` entry stands for.
+ type: string
+ pattern: ^s[1-9]\d{0,2}$
+ count:
+ description: Readings, lookups or entries the server counted.
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ durationMs:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ stop:
+ description: Set on the `stop` entry.
+ type: string
+ enum:
+ - budget
+ - time
+ - cap
+ - no_progress
+ required:
+ - id
+ - phase
+ - status
+ - round
+ - labelKey
+ - label
+ additionalProperties: false
+ description: "v1.41 — one entry of a turn's trail as persisted on `metricSource.activity`: phase, status, round, catalog
+ label, counts. The model-written title and text are behind `GET
+ /api/insights/chat/{id}/messages/{messageId}/trail`."
+ CoachStop:
+ type: object
+ properties:
+ reason:
+ type: string
+ enum:
+ - budget
+ - time
+ - cap
+ - no_progress
+ rounds:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: The rounds the turn ran, the final one included.
+ required:
+ - reason
+ - rounds
+ additionalProperties: false
+ description: "v1.41 — why the Coach answered before it was done reading: the token budget, the time budget, the round
+ cap, or no new data."
+ CoachAssumption:
+ type: object
+ properties:
+ kind:
+ type: string
+ enum:
+ - metric
+ - window
+ - comparison
+ value:
+ type: object
+ properties:
+ labelKey:
+ type: string
+ label:
+ type: string
+ value:
+ type: object
+ properties:
+ metric:
+ type: string
+ enum:
+ - bp
+ - weight
+ - pulse
+ - mood
+ - compliance
+ - hrv
+ - sleep
+ - resting_hr
+ - steps
+ - active_energy
+ - flights
+ - distance
+ - vo2_max
+ - body_temp
+ - walking_hr
+ - respiratory_rate
+ - spo2
+ - pulse_wave_velocity
+ - vascular_age
+ - body_fat
+ - fat_mass
+ - fat_free_mass
+ - muscle_mass
+ - lean_body_mass
+ - bone_mass
+ - total_body_water
+ - bmi
+ - visceral_fat
+ - glucose
+ - walking_steadiness
+ - walking_asymmetry
+ - walking_double_support
+ - walking_step_length
+ - walking_speed
+ - audio_env
+ - audio_headphone
+ - audio_event
+ - daylight
+ - skin_temp
+ - workouts
+ window:
+ type: string
+ enum:
+ - last7days
+ - last30days
+ - last90days
+ - lastYear
+ - allTime
+ comparison:
+ description: v1.41 — what a comparison is drawn against.
+ type: string
+ enum:
+ - previous_period
+ - year_ago
+ - baseline_90d
+ goal:
+ description: v1.41 — an active plan or a goal fact, by id.
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ anchor:
+ description: v1.41 — a medication start, an illness episode or a cycle phase, by id.
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ additionalProperties: false
+ description: "What a choice stands for: catalog tokens and ids, never text."
+ required:
+ - labelKey
+ - label
+ - value
+ additionalProperties: false
+ alternatives:
+ type: array
+ items:
+ type: object
+ properties:
+ labelKey:
+ type: string
+ label:
+ type: string
+ value:
+ type: object
+ properties:
+ metric:
+ type: string
+ enum:
+ - bp
+ - weight
+ - pulse
+ - mood
+ - compliance
+ - hrv
+ - sleep
+ - resting_hr
+ - steps
+ - active_energy
+ - flights
+ - distance
+ - vo2_max
+ - body_temp
+ - walking_hr
+ - respiratory_rate
+ - spo2
+ - pulse_wave_velocity
+ - vascular_age
+ - body_fat
+ - fat_mass
+ - fat_free_mass
+ - muscle_mass
+ - lean_body_mass
+ - bone_mass
+ - total_body_water
+ - bmi
+ - visceral_fat
+ - glucose
+ - walking_steadiness
+ - walking_asymmetry
+ - walking_double_support
+ - walking_step_length
+ - walking_speed
+ - audio_env
+ - audio_headphone
+ - audio_event
+ - daylight
+ - skin_temp
+ - workouts
+ window:
+ type: string
+ enum:
+ - last7days
+ - last30days
+ - last90days
+ - lastYear
+ - allTime
+ comparison:
+ description: v1.41 — what a comparison is drawn against.
+ type: string
+ enum:
+ - previous_period
+ - year_ago
+ - baseline_90d
+ goal:
+ description: v1.41 — an active plan or a goal fact, by id.
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ anchor:
+ description: v1.41 — a medication start, an illness episode or a cycle phase, by id.
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ additionalProperties: false
+ description: "What a choice stands for: catalog tokens and ids, never text."
+ required:
+ - labelKey
+ - label
+ - value
+ additionalProperties: false
+ description: At most 3.
+ required:
+ - kind
+ - value
+ - alternatives
+ additionalProperties: false
+ description: v1.41 — what an answer assumed instead of asking, from a catalog. Each alternative is offered as a
+ `change_assumption` follow-up.
+ CoachMemoryNoteMeta:
+ type: object
+ properties:
+ proposal:
+ type: boolean
+ description: "`true`: waiting for the person, answered with `memoryDecision`. `false`: already saved; undo deletes
+ `factId`."
+ proposalId:
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ factId:
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ category:
+ type: string
+ enum:
+ - preference
+ - goal
+ - context
+ - condition
+ - constraint
+ - medication
+ required:
+ - proposal
+ - category
+ additionalProperties: false
+ description: v1.41 — the fact a reply saved or proposes, as persisted on `metricSource.memoryNote`. No fact text.
+ CoachPlanProposalMeta:
+ type: object
+ properties:
+ planId:
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ metric:
+ type: string
+ maxLength: 64
+ reviewInDays:
+ type: integer
+ minimum: 7
+ maximum: 56
+ required:
+ - planId
+ - metric
+ - reviewInDays
+ additionalProperties: false
+ description: v1.41 — the plan a reply proposes, as persisted on `metricSource.planProposal`. No plan text.
CoachUsage:
type: object
properties:
@@ -52872,8 +55139,8 @@ components:
properties:
ref:
type: string
- pattern: ^r[1-6]$
- description: "`r1`..`r6`, unique within a message."
+ pattern: ^r[1-8]$
+ description: "`r1`..`r8`, unique within a message."
source:
type: object
properties:
@@ -52890,6 +55157,8 @@ components:
- get_cycle
- get_correlations
- get_metric_table
+ - get_environment
+ - get_day
domain:
$ref: "#/components/schemas/CoachStepDomain"
window:
@@ -52939,6 +55208,7 @@ components:
- type: string
enum:
- line
+ - compare
- bar
- histogram
- type: "null"
@@ -52955,7 +55225,7 @@ components:
pattern: ^[A-Za-z0-9_-]{1,64}$
ref:
type: string
- pattern: ^r[1-6]$
+ pattern: ^r[1-8]$
required:
- messageId
- ref
@@ -53028,6 +55298,38 @@ components:
- x
- series
additionalProperties: false
+ - type: object
+ properties:
+ kind:
+ type: string
+ const: compare
+ mode:
+ type: string
+ enum:
+ - periods
+ - metrics
+ x:
+ type: string
+ a:
+ type: string
+ b:
+ type: string
+ axes:
+ anyOf:
+ - type: number
+ const: 1
+ - type: number
+ const: 2
+ required:
+ - kind
+ - mode
+ - x
+ - a
+ - b
+ - axes
+ additionalProperties: false
+ description: "v1.41 — two series on one chart: two periods overlaid (`periods`, one axis) or two metrics (`metrics`, one
+ or two axes). `a` and `b` name the value columns, `x` the shared column."
- type: object
properties:
kind:
@@ -53101,6 +55403,143 @@ components:
additionalProperties: false
description: "A table of the values a turn read. Health data: encrypted at rest and sent only to the account that owns
the conversation."
+ CoachActivity:
+ type: object
+ properties:
+ id:
+ type: string
+ pattern: ^a[1-9]\d?$
+ description: "`a1`..`a99`, unique within a turn."
+ phase:
+ type: string
+ enum:
+ - thinking
+ - memory
+ - fetch
+ - digest
+ - checkpoint
+ - remember
+ - plan
+ - asking
+ - stop
+ - answer
+ status:
+ type: string
+ enum:
+ - running
+ - done
+ - empty
+ - failed
+ round:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: The tool round, from 1.
+ labelKey:
+ type: string
+ description: Closed catalog key (`insights.coach.activity.*`).
+ label:
+ type: string
+ description: Rendered on the server in the request locale from the catalog; never model text.
+ stepRef:
+ description: The `step` a `fetch` entry stands for.
+ type: string
+ pattern: ^s[1-9]\d{0,2}$
+ count:
+ description: Readings, lookups or entries the server counted.
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ durationMs:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ stop:
+ description: Set on the `stop` entry.
+ type: string
+ enum:
+ - budget
+ - time
+ - cap
+ - no_progress
+ title:
+ description: A screened reasoning title or checkpoint sentence.
+ type: string
+ maxLength: 80
+ text:
+ description: A screened reasoning summary for the round.
+ type: string
+ maxLength: 400
+ required:
+ - id
+ - phase
+ - status
+ - round
+ - labelKey
+ - label
+ additionalProperties: false
+ description: v1.41 — one entry of the live trail, sent as `activity` frames that upsert by `id`. `title` and `text` are
+ model text, screened, and sent only to the account that owns the conversation.
+ CoachMemoryNote:
+ type: object
+ properties:
+ proposal:
+ type: boolean
+ description: "`true`: waiting for the person, answered with `memoryDecision`. `false`: already saved; undo deletes
+ `factId`."
+ proposalId:
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ factId:
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ category:
+ type: string
+ enum:
+ - preference
+ - goal
+ - context
+ - condition
+ - constraint
+ - medication
+ fact:
+ type: string
+ maxLength: 160
+ required:
+ - proposal
+ - category
+ - fact
+ additionalProperties: false
+ description: v1.41 — a fact the Coach saved during the turn, or proposes to save (health facts are never saved without a
+ tap). Owner only.
+ CoachPlanProposal:
+ type: object
+ properties:
+ planId:
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ metric:
+ type: string
+ maxLength: 64
+ reviewInDays:
+ type: integer
+ minimum: 7
+ maximum: 56
+ ifCue:
+ type: string
+ thenAction:
+ type: string
+ target:
+ type: string
+ required:
+ - planId
+ - metric
+ - reviewInDays
+ - ifCue
+ - thenAction
+ additionalProperties: false
+ description: "v1.41 — a plan the Coach proposes, written as `proposed`. Answer with `planDecision: { messageId, planId,
+ accept }`. Owner only."
CoachMessageResultsResponse:
type: object
properties:
@@ -53133,7 +55572,7 @@ components:
properties:
ref:
type: string
- pattern: ^r[1-6]$
+ pattern: ^r[1-8]$
withheld:
type: string
enum:
@@ -53146,6 +55585,88 @@ components:
- withheld
additionalProperties: false
description: "One stored table, or the reason it is not served. Absence is explicit: a withheld table names why."
+ CoachMessageTrailResponse:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ trail:
+ anyOf:
+ - $ref: "#/components/schemas/CoachTrail"
+ - type: "null"
+ ai:
+ description: The `coach` capability. While unavailable `trail.entries` is empty.
+ $ref: "#/components/schemas/AiCapabilityState"
+ required:
+ - trail
+ - ai
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ CoachTrail:
+ type: object
+ properties:
+ entries:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ pattern: ^a[1-9]\d?$
+ title:
+ type: string
+ maxLength: 80
+ text:
+ type: string
+ maxLength: 400
+ required:
+ - id
+ additionalProperties: false
+ recalled:
+ type: array
+ items:
+ type: string
+ maxLength: 160
+ proposal:
+ type: object
+ properties:
+ proposalId:
+ type: string
+ pattern: ^[A-Za-z0-9_-]{1,64}$
+ category:
+ type: string
+ enum:
+ - preference
+ - goal
+ - context
+ - condition
+ - constraint
+ - medication
+ fact:
+ type: string
+ maxLength: 160
+ required:
+ - proposalId
+ - category
+ - fact
+ additionalProperties: false
+ required:
+ - entries
+ additionalProperties: false
+ description: v1.41 — the model-written text of a turn's trail and the fact texts it touched. Encrypted at rest and
+ served only to the account that owns the conversation.
CoachConversationDetailResponse:
type: object
properties:
@@ -53588,11 +56109,12 @@ components:
type: string
enum:
- preference
- - condition
- goal
- - constraint
- context
- description: "App-side closed category: preference | condition | goal | constraint | context."
+ - condition
+ - constraint
+ - medication
+ description: "App-side closed category: preference | goal | context | condition | constraint | medication (v1.41)."
text:
type: string
description: Decrypted fact text.
@@ -53601,16 +56123,54 @@ components:
minimum: -9007199254740991
maximum: 9007199254740991
description: 0..100 server-assigned extraction confidence.
+ source:
+ type: string
+ enum:
+ - user
+ - coach
+ - extracted
+ - pattern
+ - proposed
+ description: "v1.41 — where the fact came from: `user` (the remember button, or a proposal the caller confirmed),
+ `coach` (saved during a turn), `extracted` (the background extraction; every fact older than
+ v1.41), `pattern` (the deterministic matcher). `proposed` is never listed."
+ sourceConversationId:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: The conversation the fact came out of, when known.
+ sourceMessageId:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: v1.41 — the message the fact came out of, when known.
+ lastUsedAt:
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
+ description: v1.41 — when the fact last went into a Coach turn.
createdAt:
type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ updatedAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ description: v1.41 — when the fact was last written or edited.
required:
- id
- category
- text
- confidence
+ - source
+ - sourceConversationId
+ - sourceMessageId
+ - lastUsedAt
- createdAt
+ - updatedAt
additionalProperties: false
description: The caller's active facts, highest-confidence then newest first. Undecryptable rows are omitted.
required:
@@ -53628,6 +56188,128 @@ components:
- data
- error
additionalProperties: false
+ CoachFactAlreadyKnown:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ fact:
+ type: object
+ properties:
+ id:
+ type: string
+ category:
+ type: string
+ enum:
+ - preference
+ - goal
+ - context
+ - condition
+ - constraint
+ - medication
+ description: "App-side closed category: preference | goal | context | condition | constraint | medication (v1.41)."
+ text:
+ type: string
+ description: The fact as stored.
+ source:
+ type: string
+ enum:
+ - user
+ - coach
+ - extracted
+ - pattern
+ - proposed
+ description: "v1.41 — where the fact came from: `user` (the remember button, or a proposal the caller confirmed),
+ `coach` (saved during a turn), `extracted` (the background extraction; every fact older than v1.41),
+ `pattern` (the deterministic matcher). `proposed` is never listed."
+ required:
+ - id
+ - category
+ - text
+ - source
+ additionalProperties: false
+ created:
+ type: boolean
+ description: False when the caller's memory already held the same fact (or a proposal for it, now confirmed); the
+ existing fact is returned.
+ required:
+ - fact
+ - created
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ CoachFactCreated:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ fact:
+ type: object
+ properties:
+ id:
+ type: string
+ category:
+ type: string
+ enum:
+ - preference
+ - goal
+ - context
+ - condition
+ - constraint
+ - medication
+ description: "App-side closed category: preference | goal | context | condition | constraint | medication (v1.41)."
+ text:
+ type: string
+ description: The fact as stored.
+ source:
+ type: string
+ enum:
+ - user
+ - coach
+ - extracted
+ - pattern
+ - proposed
+ description: "v1.41 — where the fact came from: `user` (the remember button, or a proposal the caller confirmed),
+ `coach` (saved during a turn), `extracted` (the background extraction; every fact older than v1.41),
+ `pattern` (the deterministic matcher). `proposed` is never listed."
+ required:
+ - id
+ - category
+ - text
+ - source
+ additionalProperties: false
+ created:
+ type: boolean
+ description: False when the caller's memory already held the same fact (or a proposal for it, now confirmed); the
+ existing fact is returned.
+ required:
+ - fact
+ - created
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
CoachFactsCleared:
type: object
properties:
@@ -53654,6 +56336,67 @@ components:
- data
- error
additionalProperties: false
+ CoachFactUpdated:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ fact:
+ type: object
+ properties:
+ id:
+ type: string
+ category:
+ type: string
+ enum:
+ - preference
+ - goal
+ - context
+ - condition
+ - constraint
+ - medication
+ description: "App-side closed category: preference | goal | context | condition | constraint | medication (v1.41)."
+ text:
+ type: string
+ description: The fact as stored.
+ source:
+ type: string
+ enum:
+ - user
+ - coach
+ - extracted
+ - pattern
+ - proposed
+ description: "v1.41 — where the fact came from: `user` (the remember button, or a proposal the caller confirmed),
+ `coach` (saved during a turn), `extracted` (the background extraction; every fact older than v1.41),
+ `pattern` (the deterministic matcher). `proposed` is never listed."
+ updatedAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ required:
+ - id
+ - category
+ - text
+ - source
+ - updatedAt
+ additionalProperties: false
+ required:
+ - fact
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
CoachFactDeleted:
type: object
properties:
@@ -55133,6 +57876,130 @@ components:
- data
- error
additionalProperties: false
+ AdminMeasurementMaintenanceStatusResponse:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/AdminMeasurementMaintenanceStatus"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ AdminMeasurementMaintenanceStatus:
+ type: object
+ properties:
+ available:
+ type: boolean
+ description: False when there is no background queue to ask (a web-only deployment); nothing can be started then.
+ purgePending:
+ type: boolean
+ description: The compaction-tombstone purge is queued or running. A run started now finishes at once with
+ `refused_purge_running`.
+ run:
+ anyOf:
+ - type: object
+ properties:
+ state:
+ type: string
+ enum:
+ - queued
+ - running
+ - completed
+ - failed
+ requestedAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
+ startedAt:
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
+ - type: "null"
+ finishedAt:
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
+ - type: "null"
+ outcome:
+ anyOf:
+ - type: string
+ enum:
+ - completed
+ - refused_purge_running
+ - already_running
+ - type: "null"
+ description: "How a `completed` run ended: `completed` did the work, the two refusals finished without touching the
+ table. Null for any other state."
+ required:
+ - state
+ - requestedAt
+ - startedAt
+ - finishedAt
+ - outcome
+ additionalProperties: false
+ - type: "null"
+ description: The newest run pg-boss still keeps (terminal rows live seven days), or null when there is none.
+ sizes:
+ anyOf:
+ - type: object
+ properties:
+ tableBytes:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: "`measurements` with its indexes and TOAST."
+ indexBytes:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: Its indexes alone.
+ required:
+ - tableBytes
+ - indexBytes
+ additionalProperties: false
+ - type: "null"
+ required:
+ - available
+ - purgePending
+ - run
+ - sizes
+ additionalProperties: false
+ AdminMeasurementMaintenanceResponse:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/AdminMeasurementMaintenanceResult"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ AdminMeasurementMaintenanceResult:
+ type: object
+ properties:
+ enqueued:
+ type: boolean
+ description: False when a maintenance run is already queued or running; nothing new was queued.
+ required:
+ - enqueued
+ additionalProperties: false
AdminReminderCheckResponse:
type: object
properties:
@@ -57801,9 +60668,12 @@ components:
type: string
additionalProperties: {}
- type: "null"
- description: The cached insight payload, verbatim from the account's cache row. Null when nothing is cached or the
- stored row failed to parse — the two are indistinguishable here, and the second also silently enqueues a
- repair.
+ description: "The cached insight payload from the account's cache row, with its `dailyBriefing` resolved for today: null
+ when the text was generated on an earlier calendar day in the account's timezone (or carries no
+ `briefingGeneratedAt`, the moment it was generated), otherwise without the signals of the day whose
+ reading-backed metric has no reading today, and with each signal delta at its metric's display precision in
+ the reader's number format. Null when nothing is cached or the stored row failed to parse — the two are
+ indistinguishable here, and the second also silently enqueues a repair."
cached:
type: boolean
description: True when a payload was served from the cache.
@@ -57881,7 +60751,10 @@ components:
propertyNames:
type: string
additionalProperties: {}
- description: The insight payload, fresh or cached depending on `cached`.
+ description: "The insight payload, fresh or cached depending on `cached`, with its `dailyBriefing` resolved for today
+ exactly as on the read GET. A cached briefing generated on an earlier calendar day never short-circuits an
+ account with a provider: like a briefingless cache, it regenerates, and degrades to the cached payload on an
+ exhausted quota."
cached:
type: boolean
cachedAt:
@@ -58051,7 +60924,16 @@ components:
description: Robust upper edge.
latest:
type: number
- description: Today's latest reading, same units.
+ description: The latest reading, same units. Today's only when `latestIsToday` is true; otherwise it is from
+ `latestDate`.
+ latestDate:
+ type: string
+ pattern: ^\d{4}-\d{2}-\d{2}$
+ description: Calendar day of the latest reading in the account's time zone, `YYYY-MM-DD`.
+ latestIsToday:
+ type: boolean
+ description: True when `latestDate` is today in the account's time zone. When false, a client names the reading's date
+ instead of calling it today's.
placement:
type: string
enum:
@@ -58063,10 +60945,19 @@ components:
minimum: -9007199254740991
maximum: 9007199254740991
description: Distinct days behind the band, for transparency.
+ basis:
+ description: Present when today is still in progress for a metric whose day mean moves with the hour (blood glucose).
+ `latest` is then today's mean so far and `low` / `high` the usual range for the same hours of the
+ earlier days, not the whole-day band; a client naming the range says it is the one for this time of
+ day. Absent for the whole-day comparison.
+ type: string
+ const: sameHours
required:
- low
- high
- latest
+ - latestDate
+ - latestIsToday
- placement
- sampleDays
additionalProperties: false
@@ -58108,57 +60999,6 @@ components:
description: The two server-authoritative lines a metric sub-page renders above its chart. Pure compute over the
baseline and correlation engines — no provider call, no cache table — so web and native decode the same resolved
DTO.
- CoachSeededQuestionEnvelope:
- type: object
- properties:
- data:
- $ref: "#/components/schemas/CoachSeededQuestion"
- error:
- type: "null"
- meta:
- type: object
- properties:
- requestId:
- type: string
- additionalProperties: false
- required:
- - data
- - error
- additionalProperties: false
- CoachSeededQuestion:
- type: object
- properties:
- signal:
- anyOf:
- - type: object
- properties:
- sourceMetric:
- type: string
- description: Sentinel id the client keys its localised copy on (`readiness` / `recovery`).
- score:
- type: number
- description: The latest 0..100 score.
- band:
- type: string
- description: "`yellow` or `red`. Green never surfaces — a good day is not notable."
- required:
- - sourceMetric
- - score
- - band
- additionalProperties: false
- - type: "null"
- description: Null whenever nothing crossed the detector's confidence and notability gate, AND whenever the account has
- turned proactive suggestions off, AND whenever the `coach` capability is unavailable (then `ai` says why) —
- all mean the hero keeps its neutral greeting rather than showing a fabricated opener.
- ai:
- description: The `coach` capability the opener exists for.
- $ref: "#/components/schemas/AiCapabilityState"
- required:
- - signal
- - ai
- additionalProperties: false
- description: Today's single most notable derived wellness signal, resolved server-side into a tappable opener for the
- Coach's blank-chat hero. The client renders the resolved DTO and never recomputes the selection.
NarrativeEnvelope:
type: object
properties:
@@ -59438,6 +62278,93 @@ components:
description: Batched derived-metric values. One request resolves the whole dashboard grid (the wellness scores + the
derived re-frames + one baseline per vital) instead of N concurrent single-metric requests sharing the Prisma
pool. Pure compute over the rollup tier — no LLM, no narrative, no cache table.
+ ScoreHistoryResponseEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/ScoreHistoryResponse"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ ScoreHistoryResponse:
+ type: object
+ properties:
+ score:
+ type: string
+ enum:
+ - HEALTH_SCORE
+ - READINESS
+ - SLEEP_SCORE
+ days:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: The window the request asked for.
+ points:
+ type: array
+ items:
+ type: object
+ properties:
+ day:
+ type: string
+ description: The local calendar day the value describes, `YYYY-MM-DD`; the day the day view opens for it.
+ value:
+ type: integer
+ minimum: 0
+ maximum: 100
+ seamBreak:
+ type: boolean
+ description: "True on the first day scored under a different recipe (what counts, or the algorithm version) than the day
+ before. Draw no line from the previous point to this one: the values either side are averages of
+ different things. Only the health score has seams; never true on the first point."
+ required:
+ - day
+ - value
+ - seamBreak
+ additionalProperties: false
+ description: One point per day that has a value, oldest first. A day without one is absent, never zero.
+ band:
+ anyOf:
+ - type: object
+ properties:
+ lo:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ hi:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ n:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: Days the range was formed from.
+ required:
+ - lo
+ - hi
+ - n
+ additionalProperties: false
+ - type: "null"
+ description: "The person's usual range behind the newest point: median and scaled MAD of the values in the 30 days
+ before it, from its own side of the last seam. Null with fewer than seven such days. Describes, does not
+ grade."
+ required:
+ - score
+ - days
+ - points
+ - band
+ additionalProperties: false
+ description: One score's daily course over a trailing window, for a history chart with a day per point.
InsightsGlp1PlateauResponseEnvelope:
type: object
properties:
@@ -59924,11 +62851,17 @@ components:
description: v1.22 — labs ↔ outcome associations (each draw vs the contemporaneous outcome window-mean), FDR-controlled;
absent-degrading on sparse draws.
$ref: "#/components/schemas/LabCorrelationResult"
+ findingsBeforeSeasonalAdjustment:
+ type: boolean
+ description: v1.42 — true when the record held a discovery finding before the engine began removing trend and season
+ from both series. Drives a one-time explanation of why fewer pairs surface; false for every account created
+ since.
required:
- discovered
- pairsTested
- fdrQ
- minPairs
+ - findingsBeforeSeasonalAdjustment
additionalProperties: false
description: v1.10.0 — FDR-controlled correlation discovery over a curated behaviour × outcome matrix, lagged behaviour
→ next-day outcome. Only statistically-defensible pairs surface; descriptive, never causal.
@@ -59951,13 +62884,18 @@ components:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- description: Paired-day count after the day+1 lag join (≥ 20).
+ description: Paired-day count after the lag join (≥ 20).
+ nEff:
+ description: v1.42 — effective sample size after serial correlation (Pyper–Peterman), ≤ `n`. The p-value, the shrinkage
+ and the tier use it.
+ type: number
r:
type: number
- description: Pearson r over the lag-joined daily series.
+ description: "Pearson r over the lag-joined daily series, computed on residuals: a linear trend (and, from 120 paired
+ days, one annual harmonic) is removed from both series first (v1.42)."
pValue:
type: number
- description: Two-sided exact Student-t p-value (< 0.05).
+ description: Two-sided exact Student-t p-value (< 0.05) at the effective sample size.
qValue:
type: number
description: Benjamini-Hochberg FDR-adjusted q-value (≤ the surface threshold).
@@ -59980,7 +62918,8 @@ components:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- description: Lag in days applied (1).
+ description: "Lag in days applied: 1 = behaviour day, outcome the next day; 0 = the environmental channels, whose
+ exposure is the mean of the day before and the day itself, paired with the same day's outcome (v1.42)."
window:
description: v1.22 — which window surfaced the pair. Absent on the 180-day scan (retrospective default); `recent` for an
emerging early-detection pair.
@@ -60277,6 +63216,14 @@ components:
enum:
- above
- below
+ basis:
+ type: string
+ enum:
+ - day
+ - sameHours
+ description: "What `low`/`high` describe: the usual whole day (`day`), or the usual range for this time of day
+ (`sameHours`), used for a day still in progress whose mean moves with the hour (glucose). Additive
+ since v1.40.3."
required:
- type
- value
@@ -60284,6 +63231,7 @@ components:
- low
- high
- direction
+ - basis
additionalProperties: false
shifts:
type: array
@@ -60870,7 +63818,20 @@ components:
- days
additionalProperties: false
- type: "null"
- description: Null for a sparse logger below the day floor.
+ description: Day-to-day steadiness over the trailing `stabilityWindowDays` days. Null below the day floor (seven days
+ with an entry) inside that window, so a short window can be null where the year is not.
+ stabilityWindowDays:
+ anyOf:
+ - type: number
+ const: 30
+ - type: number
+ const: 90
+ - type: number
+ const: 180
+ - type: number
+ const: 365
+ description: "The window `stability` covers: the `days` query parameter, or 365 (the whole daily series) when it was not
+ sent. Added in v1.42.0; absent from older servers, whose score always covers the year."
tags:
type: array
items:
@@ -61326,6 +64287,7 @@ components:
- weekday
- timeOfDay
- stability
+ - stabilityWindowDays
- tags
- contextComparison
- structuredTags
@@ -62369,6 +65331,7 @@ components:
- WEB
- TELEGRAM
- DAYLIO
+ - NOTIFICATION
externalId:
anyOf:
- type: string
@@ -63602,6 +66565,124 @@ components:
required:
- revoked
additionalProperties: false
+ HealthConnectImportKickoffEnvelope:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ jobId:
+ type: string
+ status:
+ type: string
+ enum:
+ - queued
+ - unpacking
+ - parsing
+ - upserting
+ - done
+ idempotent:
+ type: boolean
+ required:
+ - jobId
+ - status
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ HealthConnectImportStatusEnvelope:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ job:
+ anyOf:
+ - $ref: "#/components/schemas/HealthConnectImportJob"
+ - type: "null"
+ required:
+ - job
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ HealthConnectImportJob:
+ type: object
+ properties:
+ jobId:
+ type: string
+ status:
+ type: string
+ description: "`queued`, `unpacking`, `parsing`, `upserting`, `done` or `failed`. A plain string, like the Apple Health
+ status."
+ startedAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ completedAt:
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
+ uploadBytes:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ progress:
+ type: object
+ propertyNames:
+ type: string
+ additionalProperties: {}
+ description: Free-form phase progress. Shape is not part of the contract.
+ result:
+ anyOf:
+ - type: object
+ propertyNames:
+ type: string
+ additionalProperties: {}
+ - type: "null"
+ description: "The worker's outcome summary, null until it finishes. Counts only: `perType` (read / inserted / updated /
+ unchanged / skipped per measurement type), `perApp` (records per Android package, `leftOut` for an app whose
+ integration the account has connected), `workouts`, `sleep`, `cycle`, `nutrients`, `skipped` (reason →
+ count), `warnings` (codes for tables or columns the importer could not read), `totals`. Never a value, a
+ timestamp or text from the file."
+ failureReason:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: Why the job failed. Starts with a code the client can key on when the file was refused —
+ `unsupported_version` (database older than version 9), `not_health_connect` (no `health_connect_export.db`,
+ or not a Health Connect database), `unsafe_schema` (a record table is a view), `too_large`,
+ `staging_missing` — else free text.
+ required:
+ - jobId
+ - status
+ - startedAt
+ - completedAt
+ - uploadBytes
+ - progress
+ - result
+ - failureReason
+ additionalProperties: false
CsvImportResponseEnvelope:
type: object
properties:
@@ -66189,6 +69270,127 @@ components:
- data
- error
additionalProperties: false
+ ListCustomVaccinesEnvelope:
+ type: object
+ properties:
+ data:
+ type: array
+ items:
+ $ref: "#/components/schemas/CustomVaccine"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ CustomVaccine:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ components:
+ type: array
+ items:
+ type: string
+ description: Catalogue antigen slugs the product protects against, only ones this release knows.
+ typicalSeriesDoses:
+ anyOf:
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ - type: "null"
+ boosterIntervalMonths:
+ anyOf:
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ - type: "null"
+ createdAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ updatedAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ required:
+ - id
+ - name
+ - components
+ - typicalSeriesDoses
+ - boosterIntervalMonths
+ - createdAt
+ - updatedAt
+ additionalProperties: false
+ description: A vaccine the record's owner defined for a product the shipped catalogue does not list, in the shape a
+ catalogue entry resolves to. A dose logged against it counts into the series of each listed antigen, settles a
+ booster reminder keyed to one of them and offers one from `boosterIntervalMonths`, exactly as a catalogue pick
+ does. `name` is the person's own wording.
+ CreateCustomVaccineEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/CustomVaccine"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ UpdateCustomVaccineEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/CustomVaccine"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ DeleteCustomVaccineEnvelope:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ deleted:
+ type: boolean
+ required:
+ - deleted
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
ListVaccinationsEnvelope:
type: object
properties:
@@ -66275,6 +69477,17 @@ components:
anyOf:
- $ref: "#/components/schemas/VaccinationCatalogEntry"
- type: "null"
+ customVaccineId:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: The record's own vaccine definition the dose names (v1.42), verbatim.
+ customVaccine:
+ anyOf:
+ - $ref: "#/components/schemas/CustomVaccine"
+ - type: "null"
+ description: That definition, resolved, or null when the dose names none or it was removed. When `catalogEntry` is also
+ set, the catalogue is the answer.
series:
type: array
items:
@@ -66313,6 +69526,8 @@ components:
- lotNumber
- site
- catalogEntry
+ - customVaccineId
+ - customVaccine
- series
- practitioner
- encounter
@@ -68371,6 +71586,45 @@ components:
- episodeId
- name
additionalProperties: false
+ linkedProcedures:
+ type: array
+ items:
+ type: object
+ properties:
+ encounterId:
+ type: string
+ occurredAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ reason:
+ anyOf:
+ - type: string
+ - type: "null"
+ bodySite:
+ anyOf:
+ - type: string
+ - type: "null"
+ laterality:
+ anyOf:
+ - type: string
+ enum:
+ - LEFT
+ - RIGHT
+ - BOTH
+ - type: "null"
+ practitionerName:
+ anyOf:
+ - type: string
+ - type: "null"
+ required:
+ - encounterId
+ - occurredAt
+ - reason
+ - bodySite
+ - laterality
+ - practitionerName
+ additionalProperties: false
assistAvailable:
type: boolean
description: "Whether a document read can run for this record now: a provider can serve it and the `documentAi`
@@ -68396,9 +71650,69 @@ components:
- maxFileBytes
- acceptedExtensions
- linkedEpisodes
+ - linkedProcedures
- assistAvailable
- contentIndex
additionalProperties: false
+ DocumentsLayoutEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/DocumentsLayout"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ DocumentsLayout:
+ type: object
+ properties:
+ version:
+ type: number
+ const: 1
+ view:
+ type: string
+ enum:
+ - cards
+ - list
+ description: '`cards` renders the preview-tile grid, `list` one compact row per document. Default "cards".'
+ arrangement:
+ type: string
+ enum:
+ - stacked
+ - flow
+ description: '`stacked` gives every month its own block; `flow` runs the documents across the full width and wraps, with
+ month names inline. Default "flow"; an account that never chose keeps following the default.'
+ required:
+ - version
+ - view
+ - arrangement
+ additionalProperties: false
+ description: Per-user document vault presentation. Display-only; a client that does not render a vault ignores it.
+ DocumentsLayoutSavedEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/DocumentsLayout"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
DocumentBulkEnvelope:
type: object
properties:
@@ -70034,6 +73348,7 @@ components:
- foreground
- background
- push
+ - manual
description: What triggered a HealthKit batch.
- type: "null"
lastBackgroundSyncAt:
@@ -70050,11 +73365,11 @@ components:
description: Present on the GET read; omitted from the PATCH echo.
$ref: "#/components/schemas/SyncHealth"
metricFreshness:
- description: Per-metric-type freshness for the `APPLE_HEALTH` source. Present on the GET read; omitted from the PATCH
- echo.
+ description: Per-metric-type freshness for the `APPLE_HEALTH` source, with the per-type arrival facts. Present on the
+ GET read; omitted from the PATCH echo.
type: array
items:
- $ref: "#/components/schemas/MetricFreshnessEntry"
+ $ref: "#/components/schemas/AppleHealthMetricFreshnessEntry"
syncProgress:
description: Backfill/sync progress summary. Present on the GET read (null when the progress read failed); omitted from
the PATCH echo.
@@ -70122,7 +73437,7 @@ components:
additionalProperties: false
description: Server-resolved sync-health verdict. The single source of liveness truth — `lastSyncedAt` alone cannot
express 'this pipe stopped delivering'.
- MetricFreshnessEntry:
+ AppleHealthMetricFreshnessEntry:
type: object
properties:
type:
@@ -70137,13 +73452,44 @@ components:
type: boolean
description: This type has gone quiet while the source around it reads healthy — the dead-pipe signature (e.g. a single
revoked HealthKit permission). Only ever true when the verdict is `fresh`.
+ lastReceivedAt:
+ description: "When a live sync batch last carried this type (inserted, updated or duplicate entries all count). Null
+ when no live sync has carried it yet: a type that so far came only from the export import, or from before
+ v1.42. `lastSeenAt` is when the newest sample was TAKEN; this is when the server last RECEIVED the type.
+ Omitted on the `WORKOUTS` entry, which the ledger does not cover, and on every entry when the ledger read
+ failed."
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
+ lastTrigger:
+ description: The `syncTrigger` of that batch. Null when the client reported none. A type whose trigger is only ever
+ `manual` reaches the server only through a user-started full sync. Same omission rules as `lastReceivedAt`.
+ anyOf:
+ - type: string
+ enum:
+ - foreground
+ - background
+ - push
+ - manual
+ description: What triggered a HealthKit batch.
+ - type: "null"
+ lastNewSampleAt:
+ description: When a live sync batch last brought a new or updated sample of this type, as opposed to duplicates only.
+ Null when none has. Same omission rules as `lastReceivedAt`.
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
required:
- type
- lastSeenAt
- stale
additionalProperties: false
- description: Per-metric-type last-seen timestamp with the server-computed staleness flag. Only types that have actually
- delivered appear — absence is absence, never an invented row.
+ description: "Per-type Apple Health freshness: the newest sample's timestamp and staleness flag, plus the per-type
+ arrival facts recorded by `POST /api/measurements/batch` (v1.42)."
AppleHealthSyncProgress:
type: object
properties:
@@ -72011,6 +75357,28 @@ components:
entry carries, plus the extras that provider has. The optional fields are not optional per request — each is
present exactly for the providers named in its description and absent for the rest, so branch on `integration`,
not on presence."
+ MetricFreshnessEntry:
+ type: object
+ properties:
+ type:
+ type: string
+ description: The measurement type (e.g. `RESPIRATORY_RATE`), or `WORKOUTS` for the workout leg.
+ lastSeenAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ description: Newest recorded reading for this type from this source.
+ stale:
+ type: boolean
+ description: This type has gone quiet while the source around it reads healthy — the dead-pipe signature (e.g. a single
+ revoked HealthKit permission). Only ever true when the verdict is `fresh`.
+ required:
+ - type
+ - lastSeenAt
+ - stale
+ additionalProperties: false
+ description: Per-metric-type last-seen timestamp with the server-computed staleness flag. Only types that have actually
+ delivered appear — absence is absence, never an invented row.
FitbitResumeEnvelope:
type: object
properties:
@@ -72284,6 +75652,20 @@ components:
so this is what distinguishes a narrow score from a broad one. Optional so older cached digests
without the field stay valid.
$ref: "#/components/schemas/HealthScoreBasis"
+ steadyWeeks:
+ description: Whole weeks the score has held where it is (same band, within two points, computed the same way), read off
+ the stored daily scores. Null when it has not held for two weeks, when the stored record does not
+ reach today, or when the score on screen differs from the newest stored day in value, band,
+ algorithm version or composition. Shown in place of a delta, never beside one.
+ anyOf:
+ - type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ - type: "null"
+ steadyAtLeast:
+ description: "True when the run reaches back past everything the read covered (120 days): `steadyWeeks` is then a lower
+ bound, said as \"at least\", and the run's start is unknown. Absent on older cached digests."
+ type: boolean
required:
- value
- band
@@ -72326,11 +75708,82 @@ components:
anyOf:
- type: string
- type: "null"
- description: First sentence of the cached briefing paragraph. Null while the `briefing` capability is unavailable;
- `line` then falls to its deterministic floor.
+ description: "The first sentence of the cached briefing paragraph that says something: a greeting or a sentence that
+ only repeats the score is skipped. Null while the `briefing` capability is unavailable or when no sentence
+ qualifies; `line` then falls to its deterministic floor."
+ lead:
+ anyOf:
+ - type: object
+ properties:
+ text:
+ type: string
+ source:
+ type: string
+ enum:
+ - reaction
+ - briefing
+ - signal
+ description: "`reaction` and `briefing` are cached model text; `signal` is the deterministic sentence about the day's
+ strongest signal."
+ required:
+ - text
+ - source
+ additionalProperties: false
+ - type: "null"
+ description: "The Today hero's lead line, resolved: the reaction line or a briefing sentence while their capabilities
+ are available, otherwise a deterministic sentence about the day's strongest signal (a vital outside its
+ personal range, an unusual night, vitals in range, last night). Null when there is nothing to say. When the
+ deterministic sentence is built from a fact, that fact is left out of `today`. Additive since v1.40."
+ signalLine:
+ anyOf:
+ - type: object
+ properties:
+ headline:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: The top signal's headline, or null when the lead already talks about its metric.
+ delta:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: The top signal's delta, when it has one.
+ required:
+ - headline
+ - delta
+ additionalProperties: false
+ - type: "null"
+ description: "The muted line under an AI lead, decided on the server so every client shows the same one: the top signal
+ minus what the lead already says. Null under a deterministic lead, without a top signal, or when the lead
+ already says everything the signal would. Additive since v1.40.3."
+ today:
+ maxItems: 5
+ type: array
+ items:
+ $ref: "#/components/schemas/DailyTodayFact"
+ description: "The Today overview: up to 5 statements about the day in priority order (Rest Mode, medications taken
+ today, an appointment today or tomorrow that is not already on the rail, last night's sleep, vitals against
+ their personal range, cycle day), never padded. A fact about a switched-off module is absent. A narrow
+ surface shows the first 4. Additive since v1.40."
+ restMode:
+ anyOf:
+ - type: object
+ properties:
+ day:
+ type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ description: 1-based day of the episode on the reader's calendar.
+ required:
+ - day
+ additionalProperties: false
+ - type: "null"
+ description: "Rest Mode: present while an illness episode is active and the illness module is on, else null. Additive
+ since v1.40."
line:
type: string
- description: "Push / lock-screen line: cached-AI lead with a deterministic floor."
+ description: "Push / lock-screen line: cached-AI lead with a deterministic floor. Carries no number (no reading, dose or
+ score), since it is shown on a locked phone; the in-app `lead` keeps its figures."
worthALook:
type: array
items:
@@ -72392,6 +75845,10 @@ components:
- score
- topSignal
- briefingLead
+ - lead
+ - signalLine
+ - today
+ - restMode
- line
- worthALook
- justIn
@@ -72400,6 +75857,39 @@ components:
additionalProperties: false
description: The one data spine of the daily-value system — composed from already-cached data, never a fresh AI call.
Consumed by the Today surface, the daily push, and a future iOS widget.
+ DailyTodayFact:
+ type: object
+ properties:
+ kind:
+ type: string
+ enum:
+ - rest_mode
+ - medications
+ - appointment
+ - sleep
+ - vitals
+ - cycle
+ description: Closed fact kind.
+ label:
+ type: string
+ description: Localised label (resolved server-side).
+ value:
+ type: string
+ description: Localised value (resolved server-side).
+ href:
+ type: string
+ description: The page the fact belongs to.
+ moduleKey:
+ description: The module that admitted the fact; absent for core data.
+ type: string
+ required:
+ - kind
+ - label
+ - value
+ - href
+ additionalProperties: false
+ description: One statement about the day on the Today overview. A fact never asks the reader to do anything; the rail is
+ the only place that does.
DailyPriorityItem:
type: object
properties:
@@ -72932,6 +76422,12 @@ components:
- painNrs
- waistCircumference
- waistToHeight
+ - scoreHealth
+ - scoreReadiness
+ - scoreRecovery
+ - scoreSleep
+ - scoreStress
+ - scoreStrain
additionalProperties:
type: object
properties:
@@ -73008,6 +76504,11 @@ components:
enum:
- score
- reminders
+ todayCardVisible:
+ description: v1.42 — whether the dashboard shows its top card (the Today card above the tiles). Missing resolves to
+ `true`; only an explicit `false` hides it. The stored default is omitted. A layout reset (DELETE) shows it
+ again. Additive.
+ type: boolean
updatedAt:
description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as
`baseUpdatedAt` on the next write. Opaque."
@@ -73183,6 +76684,12 @@ components:
- painNrs
- waistCircumference
- waistToHeight
+ - scoreHealth
+ - scoreReadiness
+ - scoreRecovery
+ - scoreSleep
+ - scoreStress
+ - scoreStrain
additionalProperties:
type: object
properties:
@@ -73259,6 +76766,11 @@ components:
enum:
- score
- reminders
+ todayCardVisible:
+ description: v1.42 — whether the dashboard shows its top card (the Today card above the tiles). Missing resolves to
+ `true`; only an explicit `false` hides it. The stored default is omitted. A layout reset (DELETE) shows it
+ again. Additive.
+ type: boolean
updatedAt:
description: "Optimistic-concurrency token: the stored row's `updatedAt` at read/write time. Echo it back as
`baseUpdatedAt` on the next write. Opaque."
@@ -73286,6 +76798,217 @@ components:
- data
- error
additionalProperties: false
+ ManagedProfileHandoverStatusEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/ManagedProfileHandoverStatus"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ ManagedProfileHandoverStatus:
+ type: object
+ properties:
+ available:
+ type: boolean
+ description: False on an instance that allows single sign-on only, where a handover cannot be created.
+ open:
+ anyOf:
+ - type: object
+ properties:
+ id:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ expiresAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ createdByYou:
+ type: boolean
+ required:
+ - id
+ - createdAt
+ - expiresAt
+ - createdByYou
+ additionalProperties: false
+ - type: "null"
+ description: "The profile's open link, or null. Never the link itself: only its hash is stored."
+ required:
+ - available
+ - open
+ additionalProperties: false
+ ManagedProfileHandoverEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/ManagedProfileHandover"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ ManagedProfileHandover:
+ type: object
+ properties:
+ id:
+ type: string
+ token:
+ type: string
+ description: The one-time `hlp_` token. Returned only in this response and never again; only its hash is stored.
+ url:
+ type: string
+ description: "The link to hand over: `/claim/`."
+ expiresAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ required:
+ - id
+ - token
+ - url
+ - expiresAt
+ additionalProperties: false
+ RevokeManagedProfileHandoverEnvelope:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ revoked:
+ type: boolean
+ required:
+ - revoked
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ PendingHandoverDecisionEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/PendingHandoverDecision"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ PendingHandoverDecision:
+ type: object
+ properties:
+ pending:
+ anyOf:
+ - type: object
+ properties:
+ claimedAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ guardians:
+ type: array
+ items:
+ type: object
+ properties:
+ grantId:
+ type: string
+ description: The guardian's grant at the claim; what a decision names.
+ displayName:
+ type: string
+ proposal:
+ $ref: "#/components/schemas/ManagedProfileHandoverAccess"
+ current:
+ $ref: "#/components/schemas/ManagedProfileHandoverAccess"
+ decidable:
+ type: boolean
+ description: False when the access moved since the claim (the guardian stepped away, or it was changed under shared
+ access); such a row is shown and left alone.
+ required:
+ - grantId
+ - displayName
+ - proposal
+ - current
+ - decidable
+ additionalProperties: false
+ required:
+ - claimedAt
+ - guardians
+ additionalProperties: false
+ - type: "null"
+ required:
+ - pending
+ additionalProperties: false
+ ManagedProfileHandoverAccess:
+ type: string
+ enum:
+ - end
+ - read
+ - manage
+ description: "What a guardian's access becomes after the handover: ended (`end`), view only (`read`), or still managing
+ (`manage`)."
+ HandoverDecisionEnvelope:
+ type: object
+ properties:
+ data:
+ type: object
+ properties:
+ decided:
+ type: boolean
+ const: true
+ changed:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: How many guardians' access this changed.
+ required:
+ - decided
+ - changed
+ additionalProperties: false
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
AccountPayloadEnvelope:
type: object
properties:
@@ -73310,12 +77033,41 @@ components:
type: string
username:
type: string
+ timezone:
+ type: string
+ description: "The IANA zone the server cuts this account's days in: the stored zone, or the instance default when the
+ stored one is unusable. Resolved, never the raw column, so bucket day keys by this value."
+ glucoseUnit:
+ anyOf:
+ - type: string
+ enum:
+ - mg/dL
+ - mmol/L
+ - type: "null"
+ description: The account's chosen glucose display unit, as stored. `null` means the person never chose one (or the
+ stored value is neither unit), and the server then shows glucose in mg/dL.
+ features:
+ type: object
+ properties:
+ trackIntake:
+ type: boolean
+ const: true
+ description: "Medications take `trackIntake`: a medication can be kept as a record whose doses are never due, and a dose
+ written against one is refused with `medication.intake.notTracked`."
+ required:
+ - trackIntake
+ additionalProperties: false
+ description: What this server understands, for a client released apart from it. Constant across accounts; a flag appears
+ once the server supports the feature and stays `true`. Absent on servers older than v1.42.0. A client that
+ meets a flag it does not know ignores it.
accountAccess:
$ref: "#/components/schemas/AccountAccess"
moduleAccess:
$ref: "#/components/schemas/ModuleAccessMap"
ai:
$ref: "#/components/schemas/AiAccountBlock"
+ coachReasoning:
+ $ref: "#/components/schemas/CoachReasoningState"
recordSession:
anyOf:
- $ref: "#/components/schemas/RecordSessionState"
@@ -73339,9 +77091,13 @@ components:
required:
- id
- username
+ - timezone
+ - glucoseUnit
+ - features
- accountAccess
- moduleAccess
- ai
+ - coachReasoning
- recordSession
- notificationPrefs
- onboarding
@@ -73356,7 +77112,9 @@ components:
carries none — the two are the same account and nothing is masked. `moduleAccess` says WHY each module is or is
not there — the record's own switch, the edge of the grant, or the operator's instance-wide one — beside the
`modules` booleans it never contradicts. `ai` (v1.39) is the resolved AI capability map for the same record,
- with the same masking; it replaces reading `GET /api/feature-flags`."
+ with the same masking; it replaces reading `GET /api/feature-flags`. `coachReasoning` (v1.41) is the Coach's
+ resolved thinking depth: the caller's own preference against the operator's reasoning controls and the record's
+ provider."
AccountAccess:
type: object
properties:
@@ -73716,6 +77474,14 @@ components:
- not_granted
- unavailable
description: Why the "vaccinations" module is or is not available here.
+ timeline:
+ type: string
+ enum:
+ - enabled
+ - disabled
+ - not_granted
+ - unavailable
+ description: Why the "timeline" module is or is not available here.
required:
- cycle
- mood
@@ -73736,6 +77502,7 @@ components:
- mentalHealth
- nutrients
- vaccinations
+ - timeline
additionalProperties: false
description: "Why each toggleable module is or is not available for the record this session is inside. `enabled` — the
module is on and the client may paint it. `disabled` — the record has it switched off. `not_granted` — the
@@ -73849,6 +77616,67 @@ components:
- responseTimeoutMs
additionalProperties: false
description: The account-level companion to the capability map.
+ CoachReasoningState:
+ type: object
+ properties:
+ level:
+ type: string
+ enum:
+ - off
+ - low
+ - medium
+ - high
+ description: "The level a Coach turn runs at right now, every cap applied: the person's choice, lowered to the
+ operator's highest level, to `medium` when the operator pays for the provider, and `off` when the operator
+ switched reasoning off or the provider cannot reason."
+ preference:
+ type: string
+ enum:
+ - off
+ - low
+ - medium
+ - high
+ description: What the person picked (`reasoning` on `PUT /api/auth/me/coach-prefs`), `medium` when they never chose.
+ Bind the settings field to this, not to `level`.
+ maxLevel:
+ type: string
+ enum:
+ - off
+ - low
+ - medium
+ - high
+ description: The highest level the settings may offer. Options above it are shown disabled as limited by the operator.
+ `off` when the operator switched reasoning off.
+ available:
+ type: boolean
+ description: False when the operator switched reasoning off or the configured provider cannot reason; the settings field
+ is then locked.
+ offIsReal:
+ type: boolean
+ description: False when the provider cannot switch reasoning off (it thinks at its lowest level instead); label the
+ lowest option "Minimal" rather than "Off".
+ source:
+ type: string
+ enum:
+ - user
+ - job
+ - admin_off
+ - admin_cap
+ - cost_cap
+ - unsupported
+ description: "Which layer decided `level`: the person's own choice (`user`), the operator's switch (`admin_off`) or cap
+ (`admin_cap`), the operator-funded ceiling (`cost_cap`), or a provider that cannot reason (`unsupported`).
+ `job` never appears here."
+ required:
+ - level
+ - preference
+ - maxLevel
+ - available
+ - offIsReal
+ - source
+ additionalProperties: false
+ description: The Coach's thinking depth for the signed-in person, resolved on the server from their preference, the
+ operator's reasoning switch and cap, who pays for the provider, and what the provider can do.
RecordSessionState:
type: object
properties:
@@ -74659,6 +78487,42 @@ components:
required:
- ok
additionalProperties: false
+ NotificationActionResponse:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/NotificationActionResult"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ NotificationActionResult:
+ type: object
+ properties:
+ logged:
+ type: boolean
+ const: true
+ action:
+ type: string
+ enum:
+ - mood-good
+ - mood-okay
+ - mood-bad
+ description: "The answer that was logged: mood-good = 4 (GUT), mood-okay = 3 (OKAY), mood-bad = 2 (SCHLECHT) on the
+ five-point mood scale."
+ required:
+ - logged
+ - action
+ additionalProperties: false
+ description: A quick answer from a notification, logged as a mood entry.
GetNotificationPreferences:
type: object
properties:
@@ -75984,6 +79848,35 @@ components:
description: "The flattened, pre-translated badge the native client renders. Note the differences from the web shape:
translated strings instead of keys, `iconName` instead of `icon`, `unlockedAt` instead of `completedAt`, and
`progress` as a 0–1 fraction."
+ EnvironmentPreferencesEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/EnvironmentPreferences"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ EnvironmentPreferences:
+ type: object
+ properties:
+ airQualityEnabled:
+ type: boolean
+ operatorDisabled:
+ type: boolean
+ description: True when the operator turned air quality off for the whole instance; the account switch then has no effect.
+ required:
+ - airQualityEnabled
+ - operatorDisabled
+ additionalProperties: false
EnvironmentOverviewEnvelope:
type: object
properties:
@@ -76063,12 +79956,202 @@ components:
attribution:
type: string
description: Upstream attribution string. Display it wherever the observations are shown.
+ airQuality:
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ description: The account's switch (`PATCH /api/environment/preferences`).
+ operatorDisabled:
+ type: boolean
+ description: True when the operator turned air quality off for the instance; the account switch then has no effect.
+ days:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: Stored days the air-quality feed returned hourly values for.
+ latestDate:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: Newest such day (YYYY-MM-DD).
+ domain:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: "The model domain of that day: `cams_europe` (about 11 km, with pollen) or `cams_global` (about 45 km, no
+ pollen)."
+ history:
+ anyOf:
+ - type: object
+ properties:
+ total:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: Past days with entries the source can serve at their place (from 2013 in Europe, from August 2022
+ elsewhere). Older days stay without values.
+ done:
+ type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: Of those, the days whose air quality is stored.
+ complete:
+ type: boolean
+ description: True when nothing the source serves is left.
+ checkedAt:
+ type: string
+ description: ISO instant of the run that counted them.
+ required:
+ - total
+ - done
+ - complete
+ - checkedAt
+ additionalProperties: false
+ - type: "null"
+ description: Progress of the background backfill that fills air quality for every past day with entries. Null before its
+ first run and while air quality is off.
+ required:
+ - enabled
+ - operatorDisabled
+ - days
+ - latestDate
+ - domain
+ - history
+ additionalProperties: false
+ description: v1.42 — the air-quality part of the module.
+ latestDay:
+ anyOf:
+ - type: object
+ properties:
+ date:
+ type: string
+ description: YYYY-MM-DD of the newest stored day.
+ tempMin:
+ anyOf:
+ - type: number
+ - type: "null"
+ tempMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ apparentMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ description: Daily feels-like maximum, °C.
+ airQuality:
+ anyOf:
+ - type: object
+ properties:
+ pm25Mean:
+ anyOf:
+ - type: number
+ - type: "null"
+ description: PM2.5 daily mean, µg/m³.
+ pm10Mean:
+ anyOf:
+ - type: number
+ - type: "null"
+ description: PM10 daily mean, µg/m³.
+ no2Mean:
+ anyOf:
+ - type: number
+ - type: "null"
+ description: NO2 daily mean, µg/m³.
+ o3Max8h:
+ anyOf:
+ - type: number
+ - type: "null"
+ description: Ozone, highest 8-hour running mean, µg/m³.
+ eaqiMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ description: European air-quality index, daily maximum.
+ uvIndexMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ dustMax:
+ anyOf:
+ - type: number
+ - type: "null"
+ description: Dust, µg/m³.
+ pollen:
+ type: object
+ properties:
+ alder:
+ anyOf:
+ - type: number
+ - type: "null"
+ birch:
+ anyOf:
+ - type: number
+ - type: "null"
+ grass:
+ anyOf:
+ - type: number
+ - type: "null"
+ mugwort:
+ anyOf:
+ - type: number
+ - type: "null"
+ olive:
+ anyOf:
+ - type: number
+ - type: "null"
+ ragweed:
+ anyOf:
+ - type: number
+ - type: "null"
+ required:
+ - alder
+ - birch
+ - grass
+ - mugwort
+ - olive
+ - ragweed
+ additionalProperties: false
+ description: Daily maxima, grains/m³; null where not covered (outside Europe, before mid 2022).
+ required:
+ - pm25Mean
+ - pm10Mean
+ - no2Mean
+ - o3Max8h
+ - eaqiMax
+ - uvIndexMax
+ - dustMax
+ - pollen
+ additionalProperties: false
+ - type: "null"
+ description: Null while air quality is off for the account or the instance, and for a day whose air-quality part was not
+ fetched yet. Null values inside mean not covered, never zero.
+ required:
+ - date
+ - tempMin
+ - tempMax
+ - apparentMax
+ - airQuality
+ additionalProperties: false
+ - type: "null"
+ description: v1.42 — the newest stored day, usually yesterday or the day before (the archive settles late). No forecast.
+ Null when nothing is stored.
+ attributions:
+ type: array
+ items:
+ type: string
+ description: v1.42 — every attribution line, weather first; the air-quality lines (Open-Meteo, Copernicus) while air
+ quality is on. Display them wherever the values are shown.
required:
- home
- travel
- context
- lastFetchFailure
- attribution
+ - airQuality
+ - latestDay
+ - attributions
additionalProperties: false
description: "The module overview: home, travel overrides, a count of stored observations, and whether the background
fetch is healthy."
@@ -77192,6 +81275,1318 @@ components:
- data
- error
additionalProperties: false
+ DayIndexEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/DayIndex"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ DayIndex:
+ type: object
+ properties:
+ from:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ to:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ days:
+ type: object
+ propertyNames:
+ type: string
+ additionalProperties:
+ type: array
+ items:
+ type: string
+ enum:
+ - values
+ - sleep
+ - scores
+ - mood
+ - assessments
+ - medications
+ - illness
+ - symptoms
+ - allergies
+ - labs
+ - visits
+ - vaccinations
+ - checkups
+ - documents
+ - workouts
+ - cycle
+ - environment
+ - lifeEvents
+ description: Each day in the window that holds anything, keyed `YYYY-MM-DD`, with the sections it holds. Days without
+ entries are absent.
+ notable:
+ type: array
+ items:
+ type: string
+ description: Days in the window that carry a notable observation.
+ required:
+ - from
+ - to
+ - days
+ - notable
+ additionalProperties: false
+ DayNotableWindowEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/DayNotableWindow"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ DayNotableWindow:
+ type: object
+ properties:
+ from:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ to:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ anchor:
+ type: string
+ enum:
+ - lastVisit
+ - requested
+ - fallback
+ description: "Where `from` came from: the last completed visit, the request, or the fallback of 90 days when the record
+ holds no visit the caller may see."
+ observations:
+ type: array
+ items:
+ $ref: "#/components/schemas/DatedDayNotable"
+ changes:
+ type: array
+ items:
+ $ref: "#/components/schemas/DayChange"
+ description: "Context changes in the window, oldest first: dose changes, medication and course starts and ends, pauses,
+ illness onsets and recoveries, vaccinations, procedures and lab days."
+ required:
+ - from
+ - to
+ - anchor
+ - observations
+ - changes
+ additionalProperties: false
+ DatedDayNotable:
+ type: object
+ properties:
+ date:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ kind:
+ type: string
+ enum:
+ - extremeHigh
+ - extremeLow
+ - firstValue
+ - gap
+ type:
+ anyOf:
+ - type: string
+ - type: "null"
+ params:
+ type: object
+ propertyNames:
+ type: string
+ additionalProperties:
+ anyOf:
+ - type: string
+ - type: number
+ description: Parameters for the message the client words from its own bundle. Dates are `YYYY-MM-DD`, values are numbers
+ in the unit the entry names.
+ required:
+ - date
+ - kind
+ - type
+ - params
+ additionalProperties: false
+ DayChange:
+ type: object
+ properties:
+ date:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ kind:
+ type: string
+ enum:
+ - intake
+ - doseChange
+ - medicationStart
+ - medicationEnd
+ - pauseStart
+ - pauseEnd
+ - courseStart
+ - courseEnd
+ - illnessOnset
+ - illnessResolved
+ - illnessDayLog
+ - symptom
+ - allergyOnset
+ - labResult
+ - visit
+ - procedure
+ - vaccination
+ - checkup
+ - document
+ - mood
+ - assessment
+ - workout
+ - cycleDayLog
+ - lifeEvent
+ section:
+ type: string
+ enum:
+ - values
+ - sleep
+ - scores
+ - mood
+ - assessments
+ - medications
+ - illness
+ - symptoms
+ - allergies
+ - labs
+ - visits
+ - vaccinations
+ - checkups
+ - documents
+ - workouts
+ - cycle
+ - environment
+ - lifeEvents
+ id:
+ type: string
+ title:
+ type: string
+ description: The record's own name for it, decrypted for the caller.
+ count:
+ type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ description: Entries folded into this one (lab results of one day).
+ href:
+ anyOf:
+ - type: string
+ - type: "null"
+ required:
+ - date
+ - kind
+ - section
+ - id
+ - title
+ - count
+ - href
+ additionalProperties: false
+ DayEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/Day"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ Day:
+ type: object
+ properties:
+ date:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ tz:
+ type: string
+ description: The IANA zone the day was cut in.
+ counts:
+ type: object
+ properties:
+ values:
+ type: integer
+ minimum: 0
+ maximum: 9007199254740991
+ entries:
+ type: integer
+ minimum: 0
+ maximum: 9007199254740991
+ required:
+ - values
+ - entries
+ additionalProperties: false
+ running:
+ type: array
+ items:
+ $ref: "#/components/schemas/DayRunningItem"
+ values:
+ type: array
+ items:
+ $ref: "#/components/schemas/DayValue"
+ events:
+ type: array
+ items:
+ $ref: "#/components/schemas/DayEvent"
+ notable:
+ type: array
+ items:
+ $ref: "#/components/schemas/DayNotable"
+ scores:
+ type: array
+ items:
+ $ref: "#/components/schemas/DayScore"
+ description: The scores the record holds for this day, in `DAY_SCORE_KEYS` order. A score with no value that day is
+ absent; an empty list means none.
+ sections:
+ type: object
+ propertyNames:
+ type: string
+ enum:
+ - values
+ - sleep
+ - scores
+ - mood
+ - assessments
+ - medications
+ - illness
+ - symptoms
+ - allergies
+ - labs
+ - visits
+ - vaccinations
+ - checkups
+ - documents
+ - workouts
+ - cycle
+ - environment
+ - lifeEvents
+ additionalProperties:
+ type: object
+ properties:
+ available:
+ type: boolean
+ const: false
+ reason:
+ type: string
+ enum:
+ - not_shared
+ - module_disabled
+ required:
+ - available
+ - reason
+ additionalProperties: false
+ description: Sections the caller cannot see. A section of a switched-off module is left out entirely rather than listed.
+ required:
+ - date
+ - tz
+ - counts
+ - running
+ - values
+ - events
+ - notable
+ - scores
+ - sections
+ additionalProperties: false
+ DayRunningItem:
+ type: object
+ properties:
+ kind:
+ type: string
+ enum:
+ - medication
+ - medicationCourse
+ - medicationPause
+ - illness
+ - restMode
+ - cyclePhase
+ - travel
+ - lifeEvent
+ section:
+ type: string
+ enum:
+ - values
+ - sleep
+ - scores
+ - mood
+ - assessments
+ - medications
+ - illness
+ - symptoms
+ - allergies
+ - labs
+ - visits
+ - vaccinations
+ - checkups
+ - documents
+ - workouts
+ - cycle
+ - environment
+ - lifeEvents
+ id:
+ type: string
+ title:
+ type: string
+ description: Decrypted server-side for the caller's own session.
+ sub:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: A second line, already formatted (a dose, a destination).
+ since:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ description: The record's own start date. Null when the record holds none (a medication filed without a start date); the
+ entry's creation date is never put in its place.
+ until:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ description: Null while it is still open.
+ dayIndex:
+ anyOf:
+ - type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ - type: "null"
+ description: Day n of the period, counting `since` as day 1; null when `since` is null.
+ dayCount:
+ anyOf:
+ - type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ - type: "null"
+ description: Length of a closed period in days; null while open.
+ href:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: In-app path to the record this came from.
+ required:
+ - kind
+ - section
+ - id
+ - title
+ - sub
+ - since
+ - until
+ - dayIndex
+ - dayCount
+ - href
+ additionalProperties: false
+ DayValue:
+ type: object
+ properties:
+ type:
+ type: string
+ description: A `MeasurementType`.
+ value:
+ type: number
+ unit:
+ type: string
+ at:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ source:
+ type: string
+ description: The `MeasurementSource` of the canonical row.
+ band:
+ anyOf:
+ - type: object
+ properties:
+ lo:
+ type: number
+ hi:
+ type: number
+ n:
+ type: integer
+ minimum: 0
+ maximum: 9007199254740991
+ required:
+ - lo
+ - hi
+ - n
+ additionalProperties: false
+ - type: "null"
+ description: The person's own usual range over the 30 days before this day, from their daily values. Null with too
+ little history.
+ required:
+ - type
+ - value
+ - unit
+ - at
+ - source
+ - band
+ additionalProperties: false
+ DayEvent:
+ type: object
+ properties:
+ at:
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
+ description: The instant, or null for something that has a date only.
+ kind:
+ type: string
+ enum:
+ - intake
+ - doseChange
+ - medicationStart
+ - medicationEnd
+ - pauseStart
+ - pauseEnd
+ - courseStart
+ - courseEnd
+ - illnessOnset
+ - illnessResolved
+ - illnessDayLog
+ - symptom
+ - allergyOnset
+ - labResult
+ - visit
+ - procedure
+ - vaccination
+ - checkup
+ - document
+ - mood
+ - assessment
+ - workout
+ - cycleDayLog
+ - lifeEvent
+ section:
+ type: string
+ enum:
+ - values
+ - sleep
+ - scores
+ - mood
+ - assessments
+ - medications
+ - illness
+ - symptoms
+ - allergies
+ - labs
+ - visits
+ - vaccinations
+ - checkups
+ - documents
+ - workouts
+ - cycle
+ - environment
+ - lifeEvents
+ id:
+ type: string
+ title:
+ type: string
+ description: "The record's own name, or a code the client words: a mood, a screener's instrument, a workout's sport,
+ `cycle` for a cycle day."
+ meta:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: "The second line: text (a dose, a lab value, a duration) or a code or number the client words (a category,
+ a kind, a severity, a screener's score and band, an intensity, a dose number)."
+ note:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: The person's own note, decrypted for the caller's session.
+ docs:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ additionalProperties: false
+ description: Documents filed against the entry.
+ href:
+ anyOf:
+ - type: string
+ - type: "null"
+ required:
+ - at
+ - kind
+ - section
+ - id
+ - title
+ - meta
+ - note
+ - docs
+ - href
+ additionalProperties: false
+ DayNotable:
+ type: object
+ properties:
+ kind:
+ type: string
+ enum:
+ - extremeHigh
+ - extremeLow
+ - firstValue
+ - gap
+ type:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: The `MeasurementType` it is about.
+ params:
+ type: object
+ propertyNames:
+ type: string
+ additionalProperties:
+ anyOf:
+ - type: string
+ - type: number
+ description: Parameters for the message the client words from its own bundle. Dates are `YYYY-MM-DD`, values are numbers
+ in the unit the entry names.
+ required:
+ - kind
+ - type
+ - params
+ additionalProperties: false
+ DayScore:
+ type: object
+ properties:
+ key:
+ type: string
+ enum:
+ - healthScore
+ - readiness
+ - recovery
+ - sleepScore
+ - strain
+ value:
+ type: number
+ description: "The score on its own scale: whole points for a 0 to 100 score, one decimal for a device's day strain."
+ max:
+ type: integer
+ exclusiveMinimum: 0
+ maximum: 9007199254740991
+ description: "The top of the scale: 100, or 21 for a device's day strain."
+ source:
+ type: string
+ description: "`COMPUTED` for a score the server derived, else the `MeasurementSource` of the device row."
+ band:
+ anyOf:
+ - type: object
+ properties:
+ lo:
+ type: number
+ hi:
+ type: number
+ n:
+ type: integer
+ minimum: 0
+ maximum: 9007199254740991
+ required:
+ - lo
+ - hi
+ - n
+ additionalProperties: false
+ - type: "null"
+ description: The person's own usual range of the score over the 30 days before this day, on the same scale. Null with
+ too little history.
+ required:
+ - key
+ - value
+ - max
+ - source
+ - band
+ additionalProperties: false
+ TimelineEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/Timeline"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ Timeline:
+ type: object
+ properties:
+ zoom:
+ type: string
+ enum:
+ - all
+ - year
+ - quarter
+ - range
+ range:
+ type: object
+ properties:
+ from:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ to:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ dataFrom:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ description: The earliest date anything in the record carries.
+ required:
+ - from
+ - to
+ - dataFrom
+ additionalProperties: false
+ lanes:
+ type: array
+ items:
+ type: object
+ properties:
+ key:
+ type: string
+ enum:
+ - life
+ - illness
+ - allergies
+ - medications
+ - vaccinations
+ - visits
+ - labs
+ - documents
+ - cycle
+ items:
+ type: array
+ items:
+ $ref: "#/components/schemas/TimelineItem"
+ required:
+ - key
+ - items
+ additionalProperties: false
+ standing:
+ type: array
+ items:
+ $ref: "#/components/schemas/TimelineStandingItem"
+ description: "Things without a start: shown once as standing."
+ bucket:
+ type: string
+ enum:
+ - quarter
+ - month
+ - week
+ - day
+ description: "The span every series point averages: `quarter` for a multi-year `all`, `month` for `year` and an `all`
+ under two years, `week` for `quarter`, and for a `range` by its length: `quarter` from two years, `month`
+ from 120 days, `week` from 42 days, `day` below."
+ series:
+ type: array
+ items:
+ $ref: "#/components/schemas/TimelineSeries"
+ availableSeries:
+ type: array
+ items:
+ type: string
+ description: "Every value series this record can show, whatever the window: the `MeasurementType` names with live
+ readings that the caller may see (events excluded), and `MOOD` when the mood score is visible and has
+ entries. Each may be named in `values`."
+ notable:
+ type: array
+ items:
+ type: object
+ properties:
+ date:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ kind:
+ type: string
+ enum:
+ - extremeHigh
+ - extremeLow
+ - firstValue
+ - gap
+ required:
+ - date
+ - kind
+ additionalProperties: false
+ required:
+ - zoom
+ - range
+ - lanes
+ - standing
+ - bucket
+ - series
+ - availableSeries
+ - notable
+ additionalProperties: false
+ TimelineItem:
+ type: object
+ properties:
+ id:
+ type: string
+ kind:
+ type: string
+ enum:
+ - lifeEvent
+ - travel
+ - episode
+ - chronic
+ - allergy
+ - medication
+ - course
+ - doseChange
+ - pause
+ - vaccination
+ - visit
+ - procedure
+ - labDay
+ - document
+ - cycle
+ start:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ end:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ description: Null for a point or an open span.
+ open:
+ type: boolean
+ description: A span that has not ended.
+ precision:
+ type: string
+ enum:
+ - DAY
+ - MONTH
+ - YEAR
+ startKnown:
+ type: boolean
+ description: False when the start is a stand-in (a medication without a start date starts at its first intake).
+ label:
+ type: string
+ description: The item's name. A trip is labelled `travel` and a cycle `cycle`; the client words both.
+ sub:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: "The second line, by kind: a code for a life event (its category), a visit or procedure (its encounter
+ kind) and a document (its kind); the analyte count for a lab day; the dose as text for a medication or a
+ dose change. The client words every code."
+ href:
+ anyOf:
+ - type: string
+ - type: "null"
+ group:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: "Items that belong to one thing share a group and are drawn on one row: a medication's own span, its
+ courses, its dose changes and its pauses all carry the medication's id. Null for an item that stands alone."
+ required:
+ - id
+ - kind
+ - start
+ - end
+ - open
+ - precision
+ - startKnown
+ - label
+ - sub
+ - href
+ - group
+ additionalProperties: false
+ TimelineStandingItem:
+ type: object
+ properties:
+ lane:
+ type: string
+ enum:
+ - life
+ - illness
+ - allergies
+ - medications
+ - vaccinations
+ - visits
+ - labs
+ - documents
+ - cycle
+ id:
+ type: string
+ label:
+ type: string
+ since:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ href:
+ anyOf:
+ - type: string
+ - type: "null"
+ required:
+ - lane
+ - id
+ - label
+ - since
+ - href
+ additionalProperties: false
+ TimelineSeries:
+ type: object
+ properties:
+ key:
+ type: string
+ description: A `MeasurementType` or `MOOD`.
+ unit:
+ anyOf:
+ - type: string
+ - type: "null"
+ points:
+ type: array
+ items:
+ type: object
+ properties:
+ t:
+ type: string
+ description: The bucket's first day.
+ mean:
+ type: number
+ description: The mean of the bucket's day values, each day weighing one (a cumulative type's days are its day totals, a
+ pulse-like type's its hourly-mean day value).
+ count:
+ type: integer
+ minimum: 1
+ maximum: 9007199254740991
+ description: Readings behind the mean. Clients draw a point from fewer than three as thin.
+ required:
+ - t
+ - mean
+ - count
+ additionalProperties: false
+ description: Buckets with at least one reading, oldest first. A bucket without one is absent, never interpolated.
+ required:
+ - key
+ - unit
+ - points
+ additionalProperties: false
+ TimelineReadinessEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/TimelineReadiness"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ TimelineReadiness:
+ type: object
+ properties:
+ verdict:
+ type: string
+ enum:
+ - carries
+ - thin
+ since:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ description: The earliest date the carrying lanes reach back to.
+ lanes:
+ type: array
+ items:
+ type: object
+ properties:
+ key:
+ type: string
+ enum:
+ - values
+ - life
+ - illness
+ - allergies
+ - medications
+ - vaccinations
+ - visits
+ - labs
+ - documents
+ - cycle
+ - mood
+ - environment
+ status:
+ type: string
+ enum:
+ - carries
+ - thin
+ - empty
+ count:
+ type: integer
+ minimum: 0
+ maximum: 9007199254740991
+ detail:
+ anyOf:
+ - type: object
+ properties:
+ key:
+ type: string
+ params:
+ type: object
+ propertyNames:
+ type: string
+ additionalProperties:
+ anyOf:
+ - type: string
+ - type: number
+ description: Parameters for the message the client words from its own bundle. Dates are `YYYY-MM-DD`, values are numbers
+ in the unit the entry names.
+ required:
+ - key
+ - params
+ additionalProperties: false
+ - type: "null"
+ description: Worded by the client from `timeline.readiness.detail`.
+ gaps:
+ type: array
+ items:
+ type: object
+ properties:
+ key:
+ type: string
+ count:
+ type: integer
+ minimum: 0
+ maximum: 9007199254740991
+ href:
+ type: string
+ required:
+ - key
+ - count
+ - href
+ additionalProperties: false
+ required:
+ - key
+ - status
+ - count
+ - detail
+ - gaps
+ additionalProperties: false
+ required:
+ - verdict
+ - since
+ - lanes
+ additionalProperties: false
+ LifeEventListEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/LifeEventList"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ LifeEventList:
+ type: object
+ properties:
+ events:
+ type: array
+ items:
+ $ref: "#/components/schemas/LifeEvent"
+ required:
+ - events
+ additionalProperties: false
+ LifeEvent:
+ type: object
+ properties:
+ id:
+ type: string
+ category:
+ type: string
+ enum:
+ - FAMILY
+ - HOME
+ - WORK
+ - LOSS
+ - OTHER
+ startDate:
+ type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ endDate:
+ anyOf:
+ - type: string
+ description: A local calendar date, `YYYY-MM-DD`.
+ - type: "null"
+ precision:
+ type: string
+ enum:
+ - DAY
+ - MONTH
+ - YEAR
+ title:
+ anyOf:
+ - type: string
+ - type: "null"
+ description: Decrypted; null only when the stored title cannot be read.
+ note:
+ anyOf:
+ - type: string
+ - type: "null"
+ createdAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ updatedAt:
+ type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ required:
+ - id
+ - category
+ - startDate
+ - endDate
+ - precision
+ - title
+ - note
+ - createdAt
+ - updatedAt
+ additionalProperties: false
+ description: A dated anchor in the person's life. Shared under the `profile` domain; never sent to a model.
+ CreateLifeEventEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/LifeEvent"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ UpdateLifeEventEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/LifeEvent"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ DeleteLifeEventEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/LifeEvent"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ SearchEnvelope:
+ type: object
+ properties:
+ data:
+ $ref: "#/components/schemas/SearchResponse"
+ error:
+ type: "null"
+ meta:
+ type: object
+ properties:
+ requestId:
+ type: string
+ additionalProperties: false
+ required:
+ - data
+ - error
+ additionalProperties: false
+ SearchResponse:
+ type: object
+ properties:
+ groups:
+ type: array
+ items:
+ type: object
+ properties:
+ kind:
+ type: string
+ enum:
+ - visit
+ - condition
+ - document
+ - vaccination
+ - lab
+ - medication
+ - symptom
+ - workout
+ - note
+ hits:
+ type: array
+ items:
+ $ref: "#/components/schemas/SearchHit"
+ more:
+ type: boolean
+ required:
+ - kind
+ - hits
+ - more
+ additionalProperties: false
+ truncated:
+ type: boolean
+ description: The search stopped before it had read every entry (a per-kind row cap or the time budget), so older entries
+ may match and are not listed.
+ required:
+ - groups
+ - truncated
+ additionalProperties: false
+ SearchHit:
+ type: object
+ properties:
+ id:
+ type: string
+ kind:
+ type: string
+ enum:
+ - visit
+ - condition
+ - document
+ - vaccination
+ - lab
+ - medication
+ - symptom
+ - workout
+ - note
+ title:
+ anyOf:
+ - $ref: "#/components/schemas/SearchMarkedText"
+ - type: "null"
+ context:
+ type: array
+ items:
+ $ref: "#/components/schemas/SearchMarkedText"
+ snippet:
+ anyOf:
+ - $ref: "#/components/schemas/SearchMarkedText"
+ - type: "null"
+ day:
+ anyOf:
+ - type: string
+ - type: "null"
+ path:
+ type: string
+ description: In-app path that opens the record, never an API path.
+ source:
+ anyOf:
+ - type: string
+ enum:
+ - mood
+ - measurement
+ - illness
+ - type: "null"
+ required:
+ - id
+ - kind
+ - title
+ - context
+ - snippet
+ - day
+ - path
+ - source
+ additionalProperties: false
+ SearchMarkedText:
+ type: object
+ properties:
+ text:
+ type: string
+ marks:
+ type: array
+ items:
+ type: array
+ prefixItems:
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ - type: integer
+ minimum: -9007199254740991
+ maximum: 9007199254740991
+ description: Half-open [start, end) character ranges of `text` a query word matched.
+ required:
+ - text
+ - marks
+ additionalProperties: false
__schema0:
anyOf:
- type: string
@@ -77634,6 +83029,14 @@ components:
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
+ remindersActive:
+ description: "Present on the READ paths only (list and single GET). v1.42.2 — whether the server sends dose reminders
+ for this medication: true only when `notificationsEnabled` is on, the medication is `active`, its course has
+ not ended (`endsOn` is today or later on the person's clock, or unset), its intake is tracked
+ (`trackIntake`), it is not `asNeeded`, and `autoMarkTaken` is off. The same decision gates every server-side
+ reminder send. A client that arms local reminders arms them from this, not from `notificationsEnabled`.
+ Read-only — computed, not stored."
+ type: boolean
nextDueAt:
description: "Present on the READ paths only — the list and single GET compute it; the create / update responses return
the stored row and omit it. v1.7.0 server-computed next due instant across all the medication's schedules
@@ -77685,6 +83088,23 @@ components:
any channel, and it is excluded from every adherence figure. Its stored schedule then travels in
`recordedSchedules` while `schedules` is served empty, so a client that arms reminders from `schedules`
stays silent without an update."
+ autoMarkTaken:
+ type: boolean
+ description: "v1.42.2 — doses are recorded automatically. False (the default) for every medication that existed before
+ the field. When true the server records each scheduled dose as taken (an intake with `autoRecorded: true`,
+ `takenAt` = the slot time) shortly after its time passes, from `autoMarkTakenSince` on. A slot the person
+ took, skipped or removed is never touched, and they can still change or skip any dose. No medication
+ reminder is sent for it on any channel while it is on; a client that arms local reminders should not arm
+ them for it either. `notificationsEnabled` keeps the person's own setting and applies again once this is
+ switched off."
+ autoMarkTakenSince:
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
+ description: v1.42.2 — when `autoMarkTaken` was last switched on. No slot anchored before it is ever recorded
+ automatically. NULL while the option is off. Read-only — set by the server on the switch.
createdAt:
type: string
format: date-time
@@ -77726,6 +83146,8 @@ components:
- oneShot
- asNeeded
- trackIntake
+ - autoMarkTaken
+ - autoMarkTakenSince
- createdAt
- updatedAt
- schedules
@@ -77790,7 +83212,10 @@ components:
- last7days
- last30days
- last90days
+ - lastYear
- allTime
+ description: "How far back the Coach may read, in every answer: snapshot, retrieval tools and summaries alike. `allTime`
+ (the default) is no limit. Formerly only the starting window; `lastYear` came with the limit."
dataClusters:
maxItems: 10
type: array
@@ -77839,6 +83264,16 @@ components:
description: v1.39.4 — offer up to three follow-up chips under the Coach's latest answer. Absent means on; only `false`
switches them off.
type: boolean
+ reasoning:
+ description: "v1.41 — how hard the Coach thinks before it answers: `off`, `low`, `medium` or `high`. Absent means
+ `medium`. The operator may switch reasoning off or cap the level; the resolved value is published on `GET
+ /api/auth/me` as `coachReasoning`."
+ type: string
+ enum:
+ - off
+ - low
+ - medium
+ - high
required:
- tone
- verbosity
@@ -77951,6 +83386,24 @@ components:
- weight
additionalProperties: false
- type: "null"
+ device:
+ description: STRAIN only — present when no computed strain score exists in the window and the score is served from the
+ device's own DAY_STRAIN instead. `score` is then `value` as a 0–100 share of `scaleMax`. Absent when the
+ computed proxy is the source.
+ anyOf:
+ - type: object
+ properties:
+ value:
+ type: number
+ description: The device's latest day strain on its own scale.
+ scaleMax:
+ type: number
+ description: Top of the device scale (21 for WHOOP day strain).
+ required:
+ - value
+ - scaleMax
+ additionalProperties: false
+ - type: "null"
required:
- score
- band
diff --git a/docs/assets/Coach.png b/docs/assets/Coach.png
index ed9cd06a6..073645bc1 100644
Binary files a/docs/assets/Coach.png and b/docs/assets/Coach.png differ
diff --git a/docs/assets/Insights.png b/docs/assets/Insights.png
index 1eacdab74..273e6b837 100644
Binary files a/docs/assets/Insights.png and b/docs/assets/Insights.png differ
diff --git a/docs/assets/Medications.png b/docs/assets/Medications.png
index 1c2cdc15b..bd131837a 100644
Binary files a/docs/assets/Medications.png and b/docs/assets/Medications.png differ
diff --git a/docs/assets/dashboard-hero.jpeg b/docs/assets/dashboard-hero.jpeg
index 26fd5be3e..92fefb629 100755
Binary files a/docs/assets/dashboard-hero.jpeg and b/docs/assets/dashboard-hero.jpeg differ
diff --git a/docs/assets/resting-pulse.png b/docs/assets/resting-pulse.png
index bf9b5a2fa..2e5f09620 100644
Binary files a/docs/assets/resting-pulse.png and b/docs/assets/resting-pulse.png differ
diff --git a/docs/integrations/apple-health.md b/docs/integrations/apple-health.md
index 456dd96c3..dc7a3d615 100644
--- a/docs/integrations/apple-health.md
+++ b/docs/integrations/apple-health.md
@@ -120,6 +120,14 @@ The HK quantity types that map to HealthLog metric types live in
mapped set (for example workout routes) is counted as unsupported and
not stored.
+Heart-rate variability comes in two measures, and they stay two types.
+`HeartRateVariabilitySDNN` becomes `HEART_RATE_VARIABILITY`;
+`HeartRateVariabilityRMSSD`, which iOS and watchOS 27 added, becomes
+`HRV_RMSSD`, the same type WHOOP, Oura and Polar write. RMSSD readings
+up to 300 ms are accepted. When both arrive, the HRV page charts each on
+its own and the Coach reads them apart, because one is not a stand-in for
+the other.
+
### ECG archive safety, privacy, and idempotency
ECG support is source-agnostic: if HealthKit includes a valid
@@ -284,6 +292,9 @@ Concrete consequences:
source tag — the lowest rank — is reserved for legacy CSV/JSON
imports that pre-date the Apple Health passthrough.
+RMSSD HRV has its own ladder, **WHOOP ≻ OURA ≻ POLAR ≻ APPLE_HEALTH**,
+separate from the SDNN one.
+
Override per-user via Settings → Sources (`/settings/sources`).
## Failure modes
diff --git a/docs/integrations/google-health.md b/docs/integrations/google-health.md
index ca623a9d0..7fc61e208 100644
--- a/docs/integrations/google-health.md
+++ b/docs/integrations/google-health.md
@@ -120,6 +120,24 @@ GOOGLE_HEALTH_REDIRECT_URI="https://your-instance.example.com/api/google-health/
The Google Cloud client id/secret are per-user (Settings), not env vars.
+## Backfill, the hourly sync and Google's rate limit
+
+Only one sync runs per account at a time. While the history backfill that
+follows a connect is running, the hourly sync leaves that account alone, and
+**Sync now** reports that a sync is already in progress and shows its progress
+instead of starting a second one.
+
+Google limits how many requests one account may make per minute. When a sync
+reaches that limit, Google answers `429`, and HealthLog waits and repeats the
+request; the connection is never marked as needing a reconnect because of it.
+Access tokens last an hour, and a long backfill renews its token as it goes.
+A backfill that stops part-way (a restart, the job's time budget) picks up
+with the data types it has not finished yet on its next attempt.
+
+If an older version left your Google Health card asking for a reconnect after
+a backfill, the first sync after updating checks the stored grant and resumes
+on its own when the grant is still valid.
+
## The 7-day re-consent caveat
While the OAuth consent screen stays in **Testing** publishing mode, Google
diff --git a/docs/integrations/health-connect.md b/docs/integrations/health-connect.md
new file mode 100644
index 000000000..df370ea98
--- /dev/null
+++ b/docs/integrations/health-connect.md
@@ -0,0 +1,166 @@
+# Health Connect import
+
+HealthLog reads the export file of Android's Health Connect and folds
+it into the same timeline as every other source. There is no Android
+app and no live sync: you export from the phone, upload the file, and
+a background job imports it. Rows from this path carry the source
+`HEALTH_CONNECT`.
+
+## What you need
+
+- An Android phone with Health Connect and its export feature. The
+ export arrived with Android 15 and reaches Android 14 through a
+ system update; if you do not see it, update Health Connect first.
+- A cloud storage app on the phone (Google Drive, Dropbox, Nextcloud
+ or any other app that offers itself as a storage location). Health
+ Connect refuses to save its export to the phone's own storage or the
+ Downloads folder, so the file always goes through a cloud first.
+- A HealthLog account in a browser. The upload uses your normal
+ session; there is no token to create.
+
+## 1. Export on the phone
+
+1. Open Health Connect (in the system settings, or the Health Connect
+ app).
+2. Go to **Manage data**, then **Export and import**, then
+ **Scheduled export** or **Export now**, depending on the version.
+3. Pick the cloud storage app as the destination. You can choose how
+ often Health Connect exports again; HealthLog only ever reads the
+ file you upload.
+
+The result is a ZIP file named `Health Connect.zip` with a single
+entry, `health_connect_export.db`. That entry is a copy of the phone's
+Health Connect database (a SQLite file). Health Connect does not
+encrypt it, so treat the file like any other export of your health
+data.
+
+## 2. Upload to HealthLog
+
+Download the ZIP from the cloud to the computer or phone you use
+HealthLog on, open **Settings → Export & Import** and drop the file
+onto the **Health Connect export** card.
+
+The endpoint is `POST /api/import/health-connect-export`
+(`multipart/form-data`, field `file`). It:
+
+1. Allows three uploads per minute per account.
+2. Refuses anything above **1 GiB**, once against the declared
+ `Content-Length` and again while the body streams to disk.
+3. Streams the body to a temp file and hashes it with SHA-256 on the
+ way. Uploading the same bytes again returns the earlier job
+ (`idempotent: true`) instead of importing twice; a failed job never
+ blocks a retry.
+4. Runs one import per account at a time, Apple Health and Health
+ Connect together. A second upload while one runs answers `409` and
+ the file is discarded.
+5. Records the upload's size and hash in the audit log, and nothing
+ from inside the file.
+
+`GET /api/import/health-connect-export/status` answers the account's
+latest Health Connect job, or `null`. The card polls it while a job
+runs.
+
+## 3. What the import does
+
+The worker extracts `health_connect_export.db` (at most 4 GiB, with
+the same zip-bomb checks as the Apple Health importer) and opens it
+read-only. The file is treated as untrusted input:
+
+- It is opened with SQLite's `immutable` flag, so a database last
+ written in WAL mode opens without its `-wal` and `-shm` files, and
+ nothing is ever written next to it.
+- Views, triggers and generated columns in the file cannot call
+ functions with side effects (`trusted_schema=OFF`), and a view in
+ place of one of the record tables refuses the whole file.
+- Columns are looked up by name. Health Connect adds columns as its
+ schema version moves, so their order differs between phones.
+ Database versions below 9 are refused; versions above 27 (the newest
+ known when this was written) are read, with a warning on the job.
+ A table that lacks a column the importer needs is skipped and named
+ in the job's `warnings`.
+
+### What becomes what
+
+| Health Connect | HealthLog |
+| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| Weight, lean body mass (grams) | `WEIGHT`, `LEAN_BODY_MASS` in kg |
+| Body fat | `BODY_FAT` in % |
+| Blood pressure | `BLOOD_PRESSURE_SYS` and `BLOOD_PRESSURE_DIA`, one pair per reading |
+| Resting heart rate, heart-rate variability (RMSSD) | `RESTING_HEART_RATE`, `HRV_RMSSD` |
+| Oxygen saturation, respiratory rate, body temperature, VO2 max | the matching types |
+| Blood glucose (mmol/L) | `BLOOD_GLUCOSE` in mg/dL, fasting and after-meal context kept |
+| Heart rate | per minute for the last 90 days; older history as hourly means with the hour's minimum and maximum |
+| Steps, active calories, distance | one total per day |
+| Sleep sessions and stages | one row per stage; light sleep becomes core sleep, both awake stages awake time; a session without stages counts as asleep |
+| Exercise sessions | workouts, with the Health Connect exercise type kept on the workout |
+| Menstruation flow and periods | cycle day logs (needs cycle tracking switched on) |
+| Hydration and vitamins and minerals | daily nutrient totals (needs the Nutrients module) |
+
+Left out on purpose: food energy and macronutrients, exercise routes,
+planned workouts, Health Connect's access logs, the "out of bed" sleep
+stage, and an "unknown" sleep stage inside a session that also has real
+stages.
+
+### When several apps write the same thing
+
+The export holds every app's data side by side. Summing them would
+count a step the phone and the watch both saw twice, so:
+
+- A day total (steps, calories, distance, water, nutrients) comes from
+ one app per day: the first one your Health Connect app priority
+ list names for that category, otherwise the app with the largest
+ total.
+- Heart rate comes from one app per hour, by the same rule.
+- Of two overlapping sleep sessions or workouts from different apps,
+ the one from the higher-ranked app is kept.
+- Two apps writing the same type at the same instant keep the
+ higher-ranked app's value.
+
+### Apps HealthLog already connects to
+
+If your account has the Withings, Fitbit, Google Health, Oura, Polar
+or WHOOP integration connected, that app's records in the export are
+left out: HealthLog already receives them directly, and importing them
+again would put every reading in twice. As a further safety net, a
+reading that matches one already stored under another source (same
+type and value within two seconds) is left out too. The job result
+lists how many records each app contributed and which apps were left
+out.
+
+### Importing again
+
+Every row is keyed by the record's Health Connect ID (`hc:`),
+day totals and hourly means by their `stats:` ID. Importing the same
+export again writes nothing; a newer export that overlaps an older one
+adds only what is new. A value that changed on the phone is updated;
+a value you deleted in HealthLog stays deleted.
+
+Heart rate imported per minute ages the way Apple Health heart rate
+does: once a minute is older than 90 days, the nightly pass folds its
+hour into one hourly mean and removes the minutes. A later import of
+an export that still holds those minutes leaves them out, so an hour
+is never counted twice.
+
+## Privacy
+
+- The uploaded ZIP and the extracted database are deleted as soon as
+ the job ends, whether it succeeded or failed. A periodic sweep
+ removes anything a crash left behind.
+- The job result holds counts only: per measurement type and per app.
+ No value, no timestamp and no text from the file (notes, titles)
+ is stored on the job.
+
+## Operator notes
+
+- The worker uses Node's built-in `node:sqlite` module. It needs no
+ flag on Node 22 and prints one `ExperimentalWarning` per process.
+- Memory stays bounded: the database is read in batches of 500 rows
+ and aggregated inside SQLite, so what the import keeps alive does not
+ grow with the length of the history. Measured under a 792 MB heap,
+ one and three years of minute-by-minute heart rate peaked at about
+ 160 MB of heap in total.
+- Split web and worker containers need a shared staging directory,
+ exactly as for the Apple Health import.
+- The status of a running job shows up under **Settings → Export &
+ Import**; a job interrupted by a restart is marked failed by the
+ same reconcile pass that watches Apple Health imports.
diff --git a/docs/integrations/home-assistant.md b/docs/integrations/home-assistant.md
index c521a8c74..71d679ccc 100644
--- a/docs/integrations/home-assistant.md
+++ b/docs/integrations/home-assistant.md
@@ -171,8 +171,9 @@ tells you which rows to retry.
## Retries and duplicates
Send an `Idempotency-Key` header with a value your automation can reproduce for
-the same reading. A retry with the same key returns the original response
-instead of writing a second row:
+the same reading. A retry with the same key and the same body returns the
+original response instead of writing a second row. A different body under the
+same key is a different reading and is written:
```yaml
headers:
diff --git a/docs/ops/backup-restore.md b/docs/ops/backup-restore.md
index 4d0a70f0e..83fd03ec3 100644
--- a/docs/ops/backup-restore.md
+++ b/docs/ops/backup-restore.md
@@ -210,8 +210,8 @@ let a new model land without one.
This is the part that bites, and it bit the nightly job a release after it bit
the weekly one. The job runs inside the app process, so V8's heap limit is the
-app's heap limit, and a container capped at 1 GB gives Node a 524 MB old-space
-limit by default. A long-lived Next.js server is already holding a large share
+app's heap limit. Node sizes it from the container's memory cap: 1 GB gives a
+524 MB old-space limit, the compose default of 1536m (since 1.40.1) gives 792 MB. A long-lived Next.js server is already holding a large share
of that before the job starts.
The uploader used to build the whole backup JSON as one string, gzipped
@@ -252,6 +252,22 @@ A run where SOME account got a copy still succeeds, with the rest counted in
`offhost_backup_failed`. Failing the whole queue over one account's object
would re-upload everybody's on every retry.
+### A run the process died under
+
+Both backup jobs carry a pg-boss heartbeat of five minutes. While a run is
+alive the worker refreshes it; a run whose process died (a restart, or a
+container killed for memory) stops refreshing, and pg-boss fails the job with
+`job heartbeat timeout` within minutes instead of leaving it `active` until its
+two- or four-hour expiry. The retries the queue already allows then pick it up.
+
+Each pass also notes when it started and when it got past every account
+(`backup_pass_attempts`). An account whose last attempt started and never
+finished is the one the process died under, and the next run takes it last, so
+one record too large for the container cannot keep every account after it from
+a copy. **Admin console → Backups** shows when the run in progress started and
+names such an account under both the weekly and the off-host card. Seeing the
+same account there twice means it needs more memory than the container has.
+
### Which account has no copy
The counts above say how many accounts were uploaded, never which. An account
@@ -305,7 +321,7 @@ writer produces the JSON a page at a time, gzips it as it goes and seals each
megabyte of gzip output on its own with AES-256-GCM. Reading it back (restore,
preview, download) takes the pieces one at a time. Nothing on either path holds
the whole copy, so the size of a copy has nothing to do with the app's memory.
-Measured on an account of 2.6 million readings in the default 1 GB container,
+Measured on an account of 2.6 million readings in a 1 GB container,
with the web server and the worker in one process: the backup took 87 seconds
and stored 97 MB in 96 pieces, the preview 20 seconds, the download of the
1.4 GB file 25 seconds, and the restore 264 seconds, which gave back every
@@ -441,10 +457,12 @@ the restart.
Since v1.16.4 a pg-boss job (`data-restore-drill`, cron `11 4 1 * *` —
04:11 on the 1st of each month) exercises the read path end-to-end:
-fetch the most recent backup object from the bucket, decrypt it under
-the current `BACKUP_ENCRYPTION_KEY`, JSON-parse it, and sanity-check
-the payload shape. It performs **no database restore** — it validates
-the artefact, not the import path.
+fetch every backup object of the most recent date from the bucket (one per
+account), decrypt each under the current `BACKUP_ENCRYPTION_KEY`, JSON-parse
+it, and sanity-check the payload shape. It performs **no database restore** —
+it validates the artefacts, not the import path. Each account is checked and
+reported on its own: one object that fails is named, and the others are still
+read.
Outcomes:
@@ -454,14 +472,16 @@ Outcomes:
uploader has stalled (or the lifecycle rule is too aggressive). The
drill pages via the worker error reporter (stderr + GlitchTip).
- **Failure** — empty bucket, fetch error, decryption failure (wrong or
- rotated key), malformed JSON: pages the same way. A decryption
+ rotated key), malformed JSON: pages the same way, once per failing object,
+ with the object key and how many accounts were checked. A decryption
failure right after a `BACKUP_ENCRYPTION_KEY` change means the new
key cannot read the existing objects: put the old key into
`BACKUP_ENCRYPTION_PREVIOUS_KEYS` (see "Rotating the off-host key").
- **Content under a missing application key** — the object opens, but its
content was written under an `ENCRYPTION_KEYS` entry this server no longer
- has, or under an id whose key material changed. The drill decrypts one value
- per key id it finds, and pages naming the key. A restore of that object
+ has, or under an id whose key material changed. The drill decrypts the
+ shortest value of each section under every key id it finds, and pages naming
+ the key when none of them opens. A restore of that object
would write back rows nobody can open; put the key back before restoring.
- **Not configured** — deployments without the `BACKUP_S3_*` vars skip
silently (wide-event warning only).
diff --git a/docs/ops/blood-pressure-missing-diastolic.md b/docs/ops/blood-pressure-missing-diastolic.md
new file mode 100644
index 000000000..1509a6ade
--- /dev/null
+++ b/docs/ops/blood-pressure-missing-diastolic.md
@@ -0,0 +1,60 @@
+# Blood pressure readings missing their diastolic half
+
+## What happened
+
+The iPhone app (every App Store build up to 1.0.3 and TestFlight 1.1.0)
+saves a manually entered blood pressure as two requests to
+`POST /api/measurements`: one `BLOOD_PRESSURE_SYS`, one
+`BLOOD_PRESSURE_DIA`, both carrying the same `Idempotency-Key`. Until the
+replay cache learned to compare request bodies, the server treated the second
+request as a retry of the first. It answered with the first request's cached
+`201` (header `X-Idempotent-Replay: true`) and never wrote the diastolic
+value. The app showed success, so nobody noticed.
+
+The fix makes the cache replay only a request with the same body. A different
+body under the same key now runs normally. Nothing recovers the values that
+were never sent to the database: they exist only on the person's phone, if at
+all.
+
+## Finding the affected readings
+
+Read-only. Run it against the HealthLog database (with the bundled compose
+file: `docker compose exec db psql -U healthlog healthlog`). It lists every live systolic reading that has no
+live diastolic reading for the same account at the same instant.
+
+
+
+```sql
+SELECT u.username,
+ m.id,
+ m.measured_at,
+ m.value AS systolic,
+ m.source
+FROM measurements m
+JOIN users u ON u.id = m.user_id
+WHERE m.type = 'BLOOD_PRESSURE_SYS'
+ AND m.deleted_at IS NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM measurements d
+ WHERE d.user_id = m.user_id
+ AND d.measured_at = m.measured_at
+ AND d.type = 'BLOOD_PRESSURE_DIA'
+ AND d.deleted_at IS NULL
+ )
+ORDER BY u.username, m.measured_at;
+```
+
+
+
+Readings from the phone app carry `source = 'MANUAL'`. Rows from a synced
+source (Withings, Apple Health) that show up here are a different gap, not
+this one: those sources write both halves themselves.
+
+## What to tell users
+
+There is nothing the server can repair: the diastolic value never reached
+it. A person whose reading appears in the list can re-enter it: delete the
+incomplete reading, then add the blood pressure again with both values and
+the original date and time. From this release on, both halves are saved on
+the first try, from any app version.
diff --git a/docs/ops/deploy.md b/docs/ops/deploy.md
index 4cfe63778..94f0c047d 100644
--- a/docs/ops/deploy.md
+++ b/docs/ops/deploy.md
@@ -78,6 +78,12 @@ roll back:
dev --create-only --name vX_Y_Z_rollback_
`), edit the SQL,
commit, and re-deploy. Never `migrate resolve --rolled-back` against
production unless you've taken a fresh `pg_dump` first.
+4. Rolling back from v1.40.2 or later to v1.40.1 or earlier: an account
+ that saved the Coach lookback "Last year" reads its whole Coach
+ preference blob as defaults on the older version, metric exclusions
+ included, until the setting is saved again. Ask affected accounts to
+ pick another lookback before the rollback, or re-save their exclusions
+ after it.
## Catalog templates
diff --git a/docs/ops/measurement-maintenance.md b/docs/ops/measurement-maintenance.md
new file mode 100644
index 000000000..2576b3fcd
--- /dev/null
+++ b/docs/ops/measurement-maintenance.md
@@ -0,0 +1,165 @@
+# Measurement table maintenance
+
+Since v1.42 the nightly folds of Apple Health data delete the raw samples they
+fold into hourly or daily rows, instead of keeping them as deleted rows for 75
+days. On an instance that has synced Apple Health for a while, those leftovers
+("compaction tombstones") were most of the `measurements` table and of its
+indexes. Three pieces deal with them, in this order:
+
+1. **The fold repair** runs by itself, once per account, before anything is
+ deleted. Up to v1.42 the folds could fold a day in two runs, and the
+ second run stored the mean of the later part of the day only. The repair
+ recomputes those hourly and daily means from all of the day's samples,
+ including the compaction tombstones, and corrects a stored mean only when
+ it differs. It does so only where every sample of the day is provably
+ still in the table: the last 75 days for daily means, and for hourly
+ means (folded 90 days late) the 75 days before that. Older days stay as
+ they are, because the deleted rows of their samples have passed the
+ 75-day retention and what is left can be a fragment of the day. When it
+ has finished an account it records that in `measurement_fold_repairs`.
+2. **The backlog purge** runs by itself, after the repair. Every worker boot queues one
+ `compaction-tombstone-purge` job. It deletes the leftovers 5,000 rows at a
+ time, one account at a time, pausing briefly between batches, and stops
+ after 40 batches; a run that stopped with work left queues the next one a
+ minute later. Rows a person deleted are never touched by it: they keep the
+ 75-day retention of the nightly tombstone cleanup. The purge leaves an
+ account alone until the repair has finished it, and a boot queues no purge
+ run while the repair has finished no account.
+3. **The table maintenance** is yours to start, after the purge. It runs
+ `VACUUM (ANALYZE)` on `measurements` and then rebuilds each of its indexes
+ with `REINDEX INDEX CONCURRENTLY`, largest first. The purge leaves the
+ indexes full of empty pages, and only a rebuild gives that space back.
+
+## Repair before purge
+
+The order matters because the purge deletes what the repair reads. Do not
+delete compaction tombstones by hand, and do not insert rows into
+`measurement_fold_repairs` to hurry the purge along: a mean the repair has not
+seen keeps its wrong value for good once its tombstones are gone.
+
+The repair reports every run on the wide event `job.measurement_fold_repair`
+(`fold_repair_means_checked`, `fold_repair_means_corrected`,
+`fold_repair_resting_corrected`, `fold_repair_skipped_beyond_horizon` for the
+older windows it left as they are, `fold_repair_samples_absorbed` for live
+samples it took into a checked mean, and `fold_repair_by_type` with the same
+counts per type; counts only, never a value). A second run over an account
+corrects nothing. It is finished when every account has a row:
+
+```sql
+SELECT count(*) AS accounts_left
+FROM users u
+LEFT JOIN measurement_fold_repairs r ON r.user_id = u.id
+WHERE r.user_id IS NULL;
+```
+
+Each run works on one account, a day at a time, and stops on its time budget
+with a follow-up that resumes at the next day; an account under restore is
+tried again five minutes later. While accounts are left, purge runs report
+them as `compaction_purge_awaiting_repair_accounts`.
+
+A restore of a backup exported before the account's repair finished writes
+the compaction tombstones back, removes the account's row and queues the
+repair again; the purge follows once the repair is through.
+
+## Is the purge finished?
+
+The purge reports every run on the wide event `job.compaction_tombstone_purge`
+(`compaction_purge_deleted`, `compaction_purge_drained`,
+`compaction_purge_deferred_accounts`). It is finished when a run reports
+`drained: true` and deletes nothing. From `psql`:
+
+```sql
+SELECT state, count(*)
+FROM pgboss.job
+WHERE name = 'compaction-tombstone-purge'
+GROUP BY state;
+```
+
+No `created`, `retry` or `active` rows means nothing is queued or running. The
+maintenance refuses to start while one is.
+
+## Before you start
+
+- **Pick a quiet window.** Reads and writes keep working during the rebuild,
+ but each rebuild reads the whole table twice and writes a full new copy of
+ the index.
+- **Free disk.** A concurrent rebuild needs room for one more copy of the
+ index it is rebuilding, plus the WAL it writes. Check the largest index:
+
+ ```sql
+ SELECT c.relname, pg_size_pretty(pg_relation_size(c.oid))
+ FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
+ WHERE i.indrelid = 'measurements'::regclass
+ ORDER BY pg_relation_size(c.oid) DESC
+ LIMIT 3;
+ ```
+
+ Have at least twice the largest size free on the database volume.
+
+- **Take a backup** (Settings, Data, Backups), as before any operation on the
+ whole table.
+
+## Starting it
+
+In the admin area, or with a signed-in admin session:
+
+```sh
+curl -X POST https:///api/admin/maintenance/measurements \
+ -H 'content-type: application/json' \
+ --cookie 'healthlog_session=' \
+ -d '{"vacuum": true, "reindex": true}'
+```
+
+The answer is `202` with `{ "enqueued": true }`, or `enqueued: false` when a
+run is already queued or running. A Bearer token is refused: the route is
+cookie-only, like every admin route. Without a background worker the route
+answers `503`.
+
+Either step can be left out (`"reindex": false` runs only the vacuum).
+
+## What it does, and what stops it
+
+The run uses a connection of its own: no statement timeout (a rebuild of a
+large index takes longer than the app's 60 seconds), `maintenance_work_mem`
+of 128 MB for this session only, and a lock timeout of five minutes, so a
+rebuild that cannot get its lock gives up instead of holding writers behind
+it.
+
+- It refuses to start while the purge is queued or running.
+- Before the first rebuild it drops any invalid `_ccnew` copy an
+ interrupted earlier run left behind.
+- If a rebuild fails, most likely for lack of disk, it drops that rebuild's
+ half-built copy at once and stops. The remaining indexes keep working as
+ they are. Free some space and start it again; finished indexes are simply
+ rebuilt once more.
+- One run at a time. It is never retried automatically.
+
+## Reading the result
+
+The run's wide event `job.measurement_maintenance` carries
+`maintenance_outcome` (`completed`, `refused_purge_running`, `failed`), the
+table and index sizes before and after, and `maintenance_steps`, one entry
+per step with its target, size before and after, and duration. The same
+sizes from `psql`:
+
+```sql
+SELECT pg_size_pretty(pg_total_relation_size('measurements')) AS total,
+ pg_size_pretty(pg_indexes_size('measurements')) AS indexes;
+```
+
+## "could not resize shared memory segment"
+
+Up to v1.42.0 the run could stop at once with `could not resize shared memory
+segment ... No space left on device`. Postgres ran the vacuum in parallel and
+kept its working memory in `/dev/shm`, which Docker limits to 64 MB per
+container unless `shm_size` is set. From v1.42.1 the maintenance connection
+runs single-process and needs no shared segment, and the bundled
+`docker-compose.yml` gives the database `shm_size: 256mb`. On v1.42.0, add
+`shm_size: 256mb` to the `db` service and restart the database container.
+
+## Not part of this
+
+`VACUUM FULL` would shrink the table file as well, but it locks the table for
+reads and writes while it runs. After the purge the remaining table is small
+and the space inside it is reused by new rows, so it is not needed. If you
+want it anyway, run it yourself in a maintenance window with the app stopped.
diff --git a/docs/ops/query-statistics.md b/docs/ops/query-statistics.md
new file mode 100644
index 000000000..6689898af
--- /dev/null
+++ b/docs/ops/query-statistics.md
@@ -0,0 +1,29 @@
+# Query statistics (pg_stat_statements)
+
+From v1.42 the bundled database loads the `pg_stat_statements` library, so the time spent in each query shape can be read from the database itself. Loading the library only collects statistics in shared memory; nothing is sent anywhere.
+
+## Enable the view
+
+The library is loaded through the `command:` of the `db` service in `docker-compose.yml`. The first `docker compose up` after updating restarts the database once. Then create the extension once:
+
+```sh
+docker compose exec db psql -U healthlog -d healthlog -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
+```
+
+## Read it
+
+The slowest query shapes by total time:
+
+```sh
+docker compose exec db psql -U healthlog -d healthlog -c "SELECT calls, round(total_exec_time) AS total_ms, round(mean_exec_time, 1) AS mean_ms, left(query, 120) AS query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 15;"
+```
+
+The statements are stored with their parameters replaced by placeholders, so values from your records do not appear in the view.
+
+## Reset
+
+```sh
+docker compose exec db psql -U healthlog -d healthlog -c "SELECT pg_stat_statements_reset();"
+```
+
+If you run your own PostgreSQL instead of the bundled one, add `pg_stat_statements` to `shared_preload_libraries` in its configuration and restart it before creating the extension.
diff --git a/docs/releases/v1.42.0/air-quality-dark.png b/docs/releases/v1.42.0/air-quality-dark.png
new file mode 100644
index 000000000..99b0d62aa
Binary files /dev/null and b/docs/releases/v1.42.0/air-quality-dark.png differ
diff --git a/docs/releases/v1.42.0/air-quality-light.png b/docs/releases/v1.42.0/air-quality-light.png
new file mode 100644
index 000000000..68a045126
Binary files /dev/null and b/docs/releases/v1.42.0/air-quality-light.png differ
diff --git a/docs/releases/v1.42.0/coach-day-dark.png b/docs/releases/v1.42.0/coach-day-dark.png
new file mode 100644
index 000000000..5ee7e0484
Binary files /dev/null and b/docs/releases/v1.42.0/coach-day-dark.png differ
diff --git a/docs/releases/v1.42.0/coach-day-light.png b/docs/releases/v1.42.0/coach-day-light.png
new file mode 100644
index 000000000..fd55f1417
Binary files /dev/null and b/docs/releases/v1.42.0/coach-day-light.png differ
diff --git a/docs/releases/v1.42.0/day-view-dark.png b/docs/releases/v1.42.0/day-view-dark.png
new file mode 100644
index 000000000..f5ea90fcc
Binary files /dev/null and b/docs/releases/v1.42.0/day-view-dark.png differ
diff --git a/docs/releases/v1.42.0/day-view-light.png b/docs/releases/v1.42.0/day-view-light.png
new file mode 100644
index 000000000..8d031c2a7
Binary files /dev/null and b/docs/releases/v1.42.0/day-view-light.png differ
diff --git a/docs/releases/v1.42.0/day-view-phone-dark.png b/docs/releases/v1.42.0/day-view-phone-dark.png
new file mode 100644
index 000000000..569302459
Binary files /dev/null and b/docs/releases/v1.42.0/day-view-phone-dark.png differ
diff --git a/docs/releases/v1.42.0/day-view-phone-light.png b/docs/releases/v1.42.0/day-view-phone-light.png
new file mode 100644
index 000000000..b38da4d23
Binary files /dev/null and b/docs/releases/v1.42.0/day-view-phone-light.png differ
diff --git a/docs/releases/v1.42.0/health-score-dark.png b/docs/releases/v1.42.0/health-score-dark.png
new file mode 100644
index 000000000..182c9abd9
Binary files /dev/null and b/docs/releases/v1.42.0/health-score-dark.png differ
diff --git a/docs/releases/v1.42.0/health-score-light.png b/docs/releases/v1.42.0/health-score-light.png
new file mode 100644
index 000000000..50a22a730
Binary files /dev/null and b/docs/releases/v1.42.0/health-score-light.png differ
diff --git a/docs/releases/v1.42.0/palette-dark.png b/docs/releases/v1.42.0/palette-dark.png
new file mode 100644
index 000000000..813dab5d4
Binary files /dev/null and b/docs/releases/v1.42.0/palette-dark.png differ
diff --git a/docs/releases/v1.42.0/palette-light.png b/docs/releases/v1.42.0/palette-light.png
new file mode 100644
index 000000000..8f0d90d25
Binary files /dev/null and b/docs/releases/v1.42.0/palette-light.png differ
diff --git a/docs/releases/v1.42.0/timeline-dark.png b/docs/releases/v1.42.0/timeline-dark.png
new file mode 100644
index 000000000..d7118bb20
Binary files /dev/null and b/docs/releases/v1.42.0/timeline-dark.png differ
diff --git a/docs/releases/v1.42.0/timeline-light.png b/docs/releases/v1.42.0/timeline-light.png
new file mode 100644
index 000000000..606d2e51f
Binary files /dev/null and b/docs/releases/v1.42.0/timeline-light.png differ
diff --git a/docs/releases/v1.42.0/timeline-phone-dark.png b/docs/releases/v1.42.0/timeline-phone-dark.png
new file mode 100644
index 000000000..7285d4c87
Binary files /dev/null and b/docs/releases/v1.42.0/timeline-phone-dark.png differ
diff --git a/docs/releases/v1.42.0/timeline-phone-light.png b/docs/releases/v1.42.0/timeline-phone-light.png
new file mode 100644
index 000000000..3ca842604
Binary files /dev/null and b/docs/releases/v1.42.0/timeline-phone-light.png differ
diff --git a/docs/releases/v1.42.0/timeline-readiness-dark.png b/docs/releases/v1.42.0/timeline-readiness-dark.png
new file mode 100644
index 000000000..9ea78b0fc
Binary files /dev/null and b/docs/releases/v1.42.0/timeline-readiness-dark.png differ
diff --git a/docs/releases/v1.42.0/timeline-readiness-light.png b/docs/releases/v1.42.0/timeline-readiness-light.png
new file mode 100644
index 000000000..b37f0c254
Binary files /dev/null and b/docs/releases/v1.42.0/timeline-readiness-light.png differ
diff --git a/docs/self-hosting/environment.md b/docs/self-hosting/environment.md
new file mode 100644
index 000000000..51dbb6d3d
--- /dev/null
+++ b/docs/self-hosting/environment.md
@@ -0,0 +1,110 @@
+# Weather, air quality and pollen
+
+The environment module stores, for each day, the weather and the outdoor air
+at a coarse location: temperature, sunshine, daylight, rain, pressure,
+humidity, and since v1.42 fine particles, ozone, nitrogen dioxide, the
+European and US air-quality indices, UV, Saharan dust and six kinds of
+pollen. Those days sit beside the person's own data: the correlations look
+for patterns between them and sleep, heart-rate variability, blood pressure,
+mood and symptoms, the Coach and connected MCP clients can read them, and the
+dashboard shows a quiet note on a day with high pollen, a warm night or very
+poor air.
+
+The module is on by default. Nothing leaves the host until a person sets a
+home location in Settings → Location & weather.
+
+## What leaves the host
+
+Three hosted Open-Meteo APIs, all keyless:
+
+| Host | What for | Variable to self-host it |
+| -------------------------------- | ------------------------------------ | --------------------------- |
+| `archive-api.open-meteo.com` | daily weather (ERA5 reanalysis) | `OPENMETEO_BASE_URL` |
+| `air-quality-api.open-meteo.com` | hourly air quality and pollen (CAMS) | `OPENMETEO_AIR_QUALITY_URL` |
+| `geocoding-api.open-meteo.com` | the city search in settings | `OPENMETEO_GEOCODING_URL` |
+
+A request carries the coordinates rounded to one decimal (about 11 km, the
+size of a town), a date range and a timezone. No account id, no name, no
+health value. Open-Meteo is open source; an operator who wants no egress at
+all can run it and point the three variables at their own instance (a host
+on the LAN is allowed).
+
+## The data, and what it is not
+
+- Weather comes from the ERA5 reanalysis on a grid of 9 to 25 kilometres. It
+ settles over a few days, so the newest stored day is usually yesterday or
+ the day before. Nothing here is a forecast.
+- Air quality comes from the CAMS models: hourly on about 11 kilometres in
+ Europe, about 45 kilometres elsewhere. The feed starts in 2013; pollen, UV,
+ dust and aerosol depth only from about mid 2022, and pollen only in Europe.
+ A value the feed did not cover is stored as empty, never as zero.
+- Hourly values become a day by fixed rules: means and maxima only for a day
+ with at least 18 of 24 hours, ozone as the highest 8-hour running mean,
+ UV only when the midday hours are all there.
+- These are modelled outdoor conditions for an area, not anyone's personal
+ exposure. Every surface that shows them says so, and none of them says the
+ weather caused anything: a pattern is described as something that occurred
+ together.
+
+## Switches
+
+- **Per account:** Settings → Location & weather → Air quality and pollen.
+ On by default. Off stops the air-quality requests for that account and
+ leaves the air-quality values out of every surface; the days already
+ stored keep theirs.
+- **For the whole instance:** `ENVIRONMENT_AIR_QUALITY_DISABLED=1` turns the
+ air-quality part off for every account (`1`, `true`, `yes` or `on`). The
+ weather keeps working. Settings then says the operator turned it off.
+- The whole module is a normal module switch (Settings → Modules), and an
+ operator can make it unavailable like any other module.
+
+All four variables are on the compose `environment:` whitelist.
+
+## Request budget
+
+The hosted APIs are free for non-commercial use up to 600 calls a minute,
+5,000 an hour and 10,000 a day per instance, where a request for many
+variables or many days counts as several calls. HealthLog keeps its own
+counter in Postgres, shared by the web process and the worker, and stays
+below 500 a minute, 4,000 an hour and 8,000 a day. A request the counter
+refuses is not sent; the nightly run ends early and continues the next night.
+
+For scale: a full two-year backfill of one account is about 150 calls, the
+nightly refresh about 3 per account.
+
+## Nightly fetch and catching up
+
+The `environment-fetch` job runs at 02:10. For each account with a home it
+refreshes the last seven days (weather and air quality together), then fills
+up to 90 stored days that do not have air quality yet, newest first. An
+instance that upgrades to v1.42 with two years of weather per account
+catches up on air quality in about nine nights without any action. The
+backfill button in settings fetches both for the range it is given.
+
+## Locations are encrypted
+
+Since v1.42 the home, each dated location period and each stored day keep
+their coarse location sealed with the instance's encryption key (one value
+per row, `{ lat, lon, label }`, under the label
+`healthlog/environment-location/v1`). The boot-time encryption backfill seals
+rows written by older releases and clears their readable columns; the
+readable columns are dropped in a later release once the backfill reports
+zero rows everywhere. Key rotation (`scripts/rotate-encryption-key.ts`, or the
+in-app rotation) covers all three.
+
+What the person sees is unchanged: the settings page and
+`GET /api/environment` answer with the readable location, opened on the
+server. The Coach and MCP read (`get_environment`) carry no coordinate and no
+place name at all, only how many days were spent away from home.
+
+Backups follow the usual split: a portable export carries the location
+readable, a disaster-recovery backup carries it sealed, and a restore of
+either seals it under the receiving host's key.
+
+## Attribution
+
+Open-Meteo data is CC BY 4.0, and CAMS data asks for the notice "Contains
+modified Copernicus Atmosphere Monitoring Service information" (modified,
+because the hourly values are folded into days). The settings page lists
+both lines under the values, and the Coach and MCP read return them with
+every result.
diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md
index 88b88f0eb..9967227e6 100644
--- a/docs/self-hosting/getting-started.md
+++ b/docs/self-hosting/getting-started.md
@@ -13,7 +13,12 @@ already installed, longer if a TLS-fronted public hostname is in scope
unchanged.
- **2 GB RAM, 10 GB free disk.** The Postgres data volume grows with
measurement history; the Apple Health import worker can briefly
- hold a 1.5 GB upload in `/tmp` while it parses.
+ hold a 1.5 GB upload in `/tmp` while it parses. The app container is
+ capped at `APP_MEMORY_LIMIT`, `1536m` by default (Node then sizes its
+ heap at about 792 MB). On a 2 GB host that leaves too little for
+ Postgres and the system, so set `APP_MEMORY_LIMIT=1g` in `.env`
+ (a heap of about 524 MB); a long-used account then runs closer to
+ that heap, and the weekly backup with it.
- **Outbound HTTPS** to `ghcr.io` (image pulls) plus whichever
integration endpoints you plan to enable (Withings, OpenAI, etc.).
The opt-in environmental-context module additionally reaches
@@ -72,6 +77,26 @@ process (`pnpm dev` against a host Postgres) or a **custom compose** that
does not derive it — in that case uncomment and set `DATABASE_URL`
explicitly in `.env` (see the commented example there).
+### Using an existing or shared Postgres
+
+HealthLog can live in a database on a Postgres instance you already run.
+It stores every time as UTC and pins each connection it opens to UTC
+(`TimeZone=UTC`, sent with the libpq `options` startup parameter), so the
+server's or the database's own `timezone` setting does not matter: an
+instance set to `Europe/Berlin` works the same as one set to UTC. The app
+checks the setting at startup and logs a warning naming the zone if a
+connection still reports something else.
+
+Behind PgBouncer with `DATABASE_SESSION_OPTIONS_DISABLED=1` (see
+[Scaling](./scaling.md#postgres-connection-pool-sizing)) the app pins the
+zone with `SET TIME ZONE 'UTC'` on each new connection instead; PgBouncer
+keeps that setting per client in transaction pooling too. If the startup
+warning still appears, set the database default as well:
+
+```sql
+ALTER DATABASE healthlog SET timezone TO 'UTC';
+```
+
## 3. Bring the stack up
```bash
diff --git a/docs/self-hosting/notifications.md b/docs/self-hosting/notifications.md
index c631f5138..509ac3351 100644
--- a/docs/self-hosting/notifications.md
+++ b/docs/self-hosting/notifications.md
@@ -179,6 +179,12 @@ HealthLog publishes to that topic.
**Server URL** (defaults to `https://ntfy.sh`; point it at your own
server if you self-host ntfy) and **Topic**, then save.
+HealthLog publishes each message as JSON to the server's root URL (the topic
+travels in the body, see ntfy's
+[publish as JSON](https://docs.ntfy.sh/publish/#publish-as-json)), so titles
+and button labels in any language arrive as written. A reverse proxy in front
+of your own ntfy has to pass `POST /` through, not only `POST /`.
+
If your ntfy server requires auth, an access token can be attached; the
server URL is validated at save time and dialled through the egress guard
(`src/lib/notifications/senders/ntfy.ts`). A self-hosted ntfy on your own
@@ -234,6 +240,8 @@ choice existed keeps sending:
message are masked as they are on every other channel.
- `priority` is a word: `urgent` for an event flagged urgent, `high` for
medication reminders, `default` for everything else.
+- `url`, when `APP_URL` is set, is the page the notification leads to (see
+ [Tap targets](#what-tapping-a-notification-opens)). Absent otherwise.
**Gotify** sends the body Gotify's `POST /message` endpoint expects:
@@ -450,6 +458,112 @@ Two gotchas worth knowing before you blame a stale token:
> in the web UI. If you only run the PWA, ignore it entirely — your
> medication reminders come from the server cron over the channels above.
+## Tap targets, content and quick answers
+
+### What tapping a notification opens
+
+Every notification leads somewhere in the app. The server decides where,
+once per event type, and every channel carries the same target:
+
+| Notification | Opens |
+| -------------------------------------------- | ---------------------------------------------------------------------------------- |
+| Mood reminder | `/mood`, with the mood entry sheet already open |
+| Morning briefing | the dashboard |
+| Medication reminder, low supply, shared dose | `/medications` (low supply opens the medication) |
+| Check-up and measurement reminders | `/checkups` |
+| Proactive Coach nudge | the Coach conversation it started |
+| Cycle reminders | `/cycle` (nothing with discreet notifications on) |
+| New sign-in | Settings, Security |
+| Integration sync failing | Settings, Integrations (an admin alert about another account opens the admin page) |
+| Illness red flag | the illness episode |
+| Other system alerts | the dashboard, or the admin status page for operator alerts |
+
+How each channel carries it:
+
+- **ntfy**: the message's `click` field, so tapping the notification opens
+ the page.
+- **Telegram**: an **Open in HealthLog** button under the message, below any
+ action buttons. Telegram only accepts `https` button links on a public
+ host, so a plain-HTTP instance gets a text link instead, and an instance
+ on `localhost` gets no link.
+- **Web Push**: the service worker opens the page, or focuses an open
+ HealthLog tab and navigates it there.
+- **APNs**: the path rides the payload as `url`.
+- **Webhook**: the generic body gains a `url` field; the Gotify body gains
+ `extras["client::notification"].click.url`, which the Gotify app opens.
+- **Email**: a link line under the message.
+
+ntfy, Telegram, the webhook and email need an absolute link, built from
+`APP_URL` (or `NEXT_PUBLIC_APP_URL`). Without one those channels deliver as
+before, just without a link. A notification about a profile you look after
+as a Guardian carries no deep link, because your session opens on your own
+record.
+
+### Health figures in notifications
+
+Settings, Notifications, **Health figures in notifications** decides what a
+notification may say on a lock screen:
+
+- **Off** (the default): a notification says what it is about and shows no
+ health figure. The morning briefing reads "Your morning briefing".
+- **On**: notifications add their figures. The morning briefing becomes
+ "Morning score 78" with the day's one-line summary underneath.
+
+Tap targets and quick answers work the same either way. The setting is
+stored as `content.detail` (`generic` or `detailed`) in the notification
+preferences (`PATCH /api/auth/me/notification-prefs`) and travels with the
+account backup.
+
+### Quick answers on the mood reminder
+
+On **ntfy** the mood reminder carries three buttons, **Good**, **Okay** and
+**Bad**. Tapping one logs a mood entry straight from the notification,
+without opening the app, and clears the notification. They map onto the
+five-point mood scale as Good = 4, Okay = 3, Bad = 2; the outer points
+stay in the app. The entry is marked with the source **Notification**.
+
+Each button calls `POST /api/notifications/action/`. The
+token is the only credential, so it is built to be harmless on its own:
+
+- it is signed with `API_TOKEN_HMAC_KEY` and names one account and one
+ answer, so it cannot be moved to another account or another answer;
+- it expires six hours after the reminder was sent;
+- the three answers of one reminder are one use between them: the first
+ tap logs, every later tap on that reminder is refused, and that stays
+ true when the logged entry is deleted and purged from the trash before
+ the six hours are up;
+- the route is rate-limited before it reads the token, refuses when the
+ mood module has been turned off, and the token is scrubbed from logs.
+
+The buttons need `APP_URL`, because the ntfy app calls the URL from your
+phone. Your phone has to reach the instance at that address.
+
+What the token cannot protect against is the topic itself. The button URLs,
+token included, travel through your ntfy server inside the message, and the
+message is delivered to everyone subscribed to the topic. For the six hours
+a token is valid, anyone who can read the topic can log that one answer on
+your behalf. On the public `ntfy.sh` the topic name is the only access
+control, so:
+
+- use a long, random topic name and do not share it;
+- better, run your own ntfy server with access control and give HealthLog
+ an access token that may only publish to your topic;
+- to invalidate every quick-answer token at once, rotate
+ `API_TOKEN_HMAC_KEY`. The same key also hashes API tokens and session
+ cookies, so rotating it invalidates every issued API token and signs out
+ every browser session and the iOS app as well (see
+ [getting started](./getting-started.md)).
+
+**Telegram** keeps its own keyboard on the mood reminder: the full 1 to 5
+scale, a note and a "later" button.
+
+**Web Push** offers no buttons. Most browsers show at most two actions,
+which cannot carry a three-way answer, and Safari on iPhone shows none.
+Tapping the notification opens the mood entry sheet instead.
+
+**APNs**: the iOS app draws its own buttons from the notification category
+it registers for the mood reminder.
+
## Which channel should I pick?
| Your setup | Recommended channel |
diff --git a/docs/self-hosting/scaling.md b/docs/self-hosting/scaling.md
index 8bde98d1b..c39d30b5a 100644
--- a/docs/self-hosting/scaling.md
+++ b/docs/self-hosting/scaling.md
@@ -163,7 +163,11 @@ the knob as `DB_CONNECTION_LIMIT`, baked into `DATABASE_URL` as
`connection_limit`; `DATABASE_POOL_MAX` remains a working explicit
override. Resolution order: `DB_CONNECTION_LIMIT`, then
`DATABASE_POOL_MAX`, then the `connection_limit` URL parameter, then
-the default of 20.
+the default of 20. In the bundled single-container setup
+(`HEALTHLOG_PROCESS_TYPE=all`) the web requests and the background
+worker run in one process and share that one Prisma pool, so a budget
+sized for the web traffic alone leaves the worker's jobs queueing for
+the same connections.
Plan total Postgres slots as **container_count × budget**.
A stock Postgres 16 container ships with `max_connections = 100`, so
@@ -184,6 +188,20 @@ already`. Either raise `max_connections` on the Postgres side, lower
`DATABASE_POOL_MAX`, or front the database with PgBouncer in
transaction-pooling mode.
+The app sets its statement timeout, `work_mem` and `TimeZone=UTC` on every
+connection through the libpq `options` startup parameter. PgBouncer refuses that
+parameter (`unsupported startup parameter: options`) unless it is listed
+in `ignore_startup_parameters`. Either list it there, or set
+`DATABASE_SESSION_OPTIONS_DISABLED=1` so the app sends no session
+settings; set the timeout and `work_mem` on the database role instead
+(`ALTER ROLE healthlog SET work_mem = '16MB'`) if you still want them.
+The time zone is the exception: with the options off the app pins it with
+`SET TIME ZONE 'UTC'` on each new connection, which PgBouncer tracks per
+client, and warns at startup if a connection still reports another zone.
+The measurement maintenance job (Admin, System status) then applies its own
+settings with `SET` after connecting. Its concurrent index rebuild needs a
+session, so run it through a session-mode pool or a direct connection.
+
### Overriding the per-container pool ceiling
With the bundled compose, set `DB_CONNECTION_LIMIT` in `.env`; it
diff --git a/docs/using/search.md b/docs/using/search.md
new file mode 100644
index 000000000..767d55f6f
--- /dev/null
+++ b/docs/using/search.md
@@ -0,0 +1,55 @@
+# Searching your record
+
+The command palette finds pages and settings, and it searches what you have
+recorded.
+
+## Opening it
+
+Press **⌘K** on a Mac or **Ctrl K** elsewhere, or tap the magnifier at the
+top right, and start typing.
+
+## What it finds
+
+Below the pages and settings, a section **In your records** lists the
+entries that match what you typed:
+
+- visits (procedures included)
+- conditions
+- documents, by title, file name and the text read from them
+- vaccinations
+- lab results
+- medications
+- symptoms
+- workouts
+- notes on mood entries, readings and illness days
+
+Each hit opens where the entry lives: a visit on the check-ups page, a lab
+result on its analyte, a note on a mood entry or a reading on its day, a note
+on an illness day in its illness.
+
+## How matching works
+
+Case and accents do not matter, so "schulter", "Schulter" and "Schülter"
+find the same entries. Umlauts can be typed as `ae`, `oe` and `ue` (and `ß`
+as `ss`), which helps on a keyboard without them.
+
+## Very large records
+
+The search reads each kind of entry newest first and stops after a fixed
+number of entries and a short time limit, so it stays quick on a record that
+holds years of data. When it stopped before reaching the oldest entries, the
+list says so. A more specific search term usually finds an older entry.
+
+## Shared records
+
+When someone has shared their whole record with you, the search works inside
+that record too. An account shared with you section by section cannot be
+searched: the search lays every section side by side, which a partial share
+does not open.
+
+## Privacy
+
+What you type is sent to your instance in the body of the request, not in the
+address, so it does not end up in a reverse proxy's access log or in the
+browser history. The server does not log the search text either, only its
+length and how many hits it found.
diff --git a/e2e/a11y.spec.ts b/e2e/a11y.spec.ts
index b809d4784..abc0c487d 100644
--- a/e2e/a11y.spec.ts
+++ b/e2e/a11y.spec.ts
@@ -6,6 +6,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
import { openMenu } from "./open-menu";
import { POPULATED_SUMMARIES } from "./utils/mock-dashboard-snapshot";
+import { revealDeferredSections } from "./utils/deferred-sections";
type AxeViolation = Awaited<
ReturnType
@@ -41,6 +42,8 @@ type RouteCase = {
name: string;
path: string;
painted: (page: Page) => Locator;
+ /** Runs after navigation, before the painted gate (e.g. mount deferred sections). */
+ prepare?: (page: Page) => Promise;
};
const THEMES: readonly Theme[] = ["light", "dark"];
@@ -325,6 +328,7 @@ async function visitAndScan(
): Promise {
return test.step(`${theme} ${routeCase.name}`, async () => {
await page.goto(routeCase.path, { waitUntil: "domcontentloaded" });
+ await routeCase.prepare?.(page);
return scanPaintedState(
page,
theme,
@@ -773,6 +777,8 @@ const INSIGHTS_ROUTES: readonly RouteCase[] = [
{
name: "/insights overview",
path: "/insights",
+ // Scan the whole overview, the sections that mount near the viewport too.
+ prepare: revealDeferredSections,
// The vitals grid, settled. The scores strip is not a stable gate: it
// unmounts for an account with no score yet, and since v1.39 it no longer
// keeps an empty cell for a cycle ring that has no dial to draw.
@@ -1041,23 +1047,24 @@ test.describe("axe-core authenticated route and state matrix", () => {
);
});
- await test.step(`${theme} /coach open evidence disclosure and source chips`, async () => {
+ await test.step(`${theme} /coach open the areas an older answer looked at`, async () => {
await page.goto(`/coach?c=${A11Y_CONVERSATION_ID}`, {
waitUntil: "domcontentloaded",
});
- const evidence = page.locator('[data-slot="coach-evidence"]').first();
- await expect(evidence).toBeVisible({ timeout: 15_000 });
- await evidence.locator("summary").click();
- await expect(evidence).toHaveAttribute("open", "");
+ const steps = page.locator('[data-slot="coach-turn-steps"]').first();
+ await expect(steps).toBeVisible({ timeout: 15_000 });
+ const toggle = steps.locator('[data-slot="coach-turn-steps-toggle"]');
+ await toggle.click();
+ await expect(toggle).toHaveAttribute("aria-expanded", "true");
await expect(
- evidence.locator('[data-slot="coach-source-chips"]'),
+ steps.locator('[data-slot="coach-turn-areas"]'),
).toBeVisible();
blocking.push(
...(await scanPaintedState(
page,
theme,
- "/coach open evidence disclosure and source chips",
- evidence,
+ "/coach open the areas an older answer looked at",
+ steps,
)),
);
});
diff --git a/e2e/ai-optional-account.spec.ts b/e2e/ai-optional-account.spec.ts
index 3e7f138fe..6a27a80cd 100644
--- a/e2e/ai-optional-account.spec.ts
+++ b/e2e/ai-optional-account.spec.ts
@@ -25,6 +25,7 @@ import {
seedStoredBriefing,
} from "./setup/ai-optional-fixture";
import { expect, test } from "./setup/test";
+import { revealDeferredSectionsSettled } from "./utils/deferred-sections";
test.use({ storageState: AI_OPTIONAL_STORAGE_STATE_PATH });
test.describe.configure({ mode: "serial" });
@@ -36,12 +37,18 @@ test.afterAll(async () => {
await resetAiOptionalAccount();
});
+/**
+ * The overview with every section mounted. Sections past the first three
+ * render as an empty sentinel until they near the viewport, so a count of
+ * zero taken before they mount would pass whatever they paint.
+ */
async function openOverview(page: Page): Promise {
await page.goto("/insights");
await expect(page.locator('[data-slot="insights-hero-strip"]')).toBeVisible({
timeout: 20_000,
});
await page.waitForLoadState("networkidle");
+ await revealDeferredSectionsSettled(page);
}
test("J5: withdrawing AI consent deletes the notes written under it", async ({
@@ -80,12 +87,12 @@ test("J4: the Coach memory stays readable and deletable with the Coach hidden",
const title = `Stored conversation ${Date.now()}`;
await seedHiddenCoachWithConversation(title);
- // Hide Coach takes the Coach page and its launcher away...
- await page.goto("/insights");
- await expect(page.locator('[data-slot="insights-hero-strip"]')).toBeVisible({
- timeout: 20_000,
- });
+ // Hide Coach takes the Coach page, its launcher and every per-card
+ // hand-off away...
+ await openOverview(page);
await expect(page.locator('[data-slot="coach-fab"]')).toHaveCount(0);
+ await expect(page.locator('[data-slot="ask-coach-action"]')).toHaveCount(0);
+ await expect(page.locator('[data-slot="ask-coach-icon"]')).toHaveCount(0);
// ...and Settings, AI lists what the Coach stored.
await page.goto("/settings/ai");
diff --git a/e2e/ai-optional-overview.spec.ts b/e2e/ai-optional-overview.spec.ts
index dac4533bd..ba0158dd0 100644
--- a/e2e/ai-optional-overview.spec.ts
+++ b/e2e/ai-optional-overview.spec.ts
@@ -21,9 +21,19 @@ import type { Page, Request, Response } from "@playwright/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
import { aiBlockUnavailable, serveAiBlock } from "./setup/ai-capabilities";
import { expect, test } from "./setup/test";
+import { revealDeferredSectionsSettled } from "./utils/deferred-sections";
test.use({ storageState: STORAGE_STATE_PATH });
+/**
+ * Mount every overview section, then let their reads land. Sections past the
+ * first three render as an empty sentinel until they near the viewport, so a
+ * count of zero taken before this would pass whatever those sections paint.
+ */
+async function mountWholeOverview(page: Page): Promise {
+ await revealDeferredSectionsSettled(page);
+}
+
/** Routes that call a model or serve model-written text. */
const AI_ROUTE = new RegExp(
[
@@ -89,6 +99,7 @@ test.describe("J1: AI switched off by the operator", () => {
);
// Let every section's reads land before judging what painted.
await page.waitForLoadState("networkidle");
+ await mountWholeOverview(page);
// A read refused for want of AI paints one of these on the overview:
// the section boundary, the error card, or the dense error row.
@@ -137,6 +148,7 @@ test.describe("J3: no AI provider", () => {
timeout: 20_000,
});
await page.waitForLoadState("networkidle");
+ await mountWholeOverview(page);
await expect(page.locator('[data-slot="coach-fab"]')).toHaveCount(0);
await expect(page.locator('[data-slot="daily-briefing"]')).toHaveCount(0);
// The per-card Coach hand-offs paint only while the Coach is available.
@@ -163,6 +175,7 @@ test.describe("J3: no AI provider", () => {
{ timeout: 20_000 },
);
await page.waitForLoadState("networkidle");
+ await mountWholeOverview(page);
await expect(page.locator('[data-slot="ai-setup-hint"]')).toHaveCount(0);
expect(network.aiRequests).toEqual([]);
diff --git a/e2e/ai-provider-dropdown.spec.ts b/e2e/ai-provider-dropdown.spec.ts
index ea2c00bb2..adacbdd2c 100644
--- a/e2e/ai-provider-dropdown.spec.ts
+++ b/e2e/ai-provider-dropdown.spec.ts
@@ -339,4 +339,116 @@ test.describe("Settings → AI provider dropdown UX (B2)", () => {
.locator('button[type="submit"]'),
).toBeEnabled();
});
+
+ // #1126 — the Reasoning select on the Local and OpenAI-compatible forms
+ // saves with the form, and at phone width it sits in the same column as
+ // the model field without pushing the page sideways.
+ test("reasoning setting saves from the Local and gateway forms at 390 px", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ const patchBodies: Array> = [];
+ let providerState: Record = {
+ provider: "LOCAL",
+ model: null,
+ baseUrl: "http://localhost:11434/v1",
+ responseTimeoutSeconds: null,
+ hasAnthropicKey: false,
+ anthropicKeyPreview: null,
+ hasLocalKey: false,
+ hasOpenaiKey: false,
+ openaiKeyPreview: null,
+ compatBaseUrl: "https://gateway.example.org/v1",
+ compatModel: "qwen3",
+ hasCompatKey: false,
+ localReasoningEffort: null,
+ compatReasoningEffort: null,
+ };
+ await page.route("**/api/insights/settings", (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ codexStatus: "disconnected",
+ codexConnectedAt: null,
+ hasAdminKey: false,
+ codexOauthConfigured: true,
+ privacyMode: "aggregated",
+ lastInsightAt: null,
+ },
+ error: null,
+ }),
+ }),
+ );
+ await page.route("**/api/user/ai-provider", async (route) => {
+ if (route.request().method() === "PATCH") {
+ const body = route.request().postDataJSON() as Record;
+ patchBodies.push(body);
+ providerState = { ...providerState, ...body };
+ }
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data: providerState, error: null }),
+ });
+ });
+ await page.route("**/api/insights/provider-chain", (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ activeProvider: "local",
+ cachedActiveProvider: null,
+ configuredChain: [
+ { providerType: "local", enabled: true, available: true },
+ ],
+ },
+ error: null,
+ }),
+ }),
+ );
+
+ const providerHydrated = page.waitForResponse("**/api/user/ai-provider");
+ await page.goto("/settings/ai?provider=local", {
+ waitUntil: "domcontentloaded",
+ });
+ await providerHydrated;
+
+ const localForm = page.getByTestId("ai-provider-config-local");
+ const localReasoning = page.locator("#ai-local-reasoning");
+ await expect(localReasoning).toHaveValue("");
+ // Same column, same width as the model select above it.
+ const [modelBox, reasoningBox] = await Promise.all([
+ page.locator("#ai-local-model").boundingBox(),
+ localReasoning.boundingBox(),
+ ]);
+ expect(reasoningBox?.x).toBe(modelBox?.x);
+ expect(reasoningBox?.width).toBe(modelBox?.width);
+ expect(
+ await page.evaluate(
+ () =>
+ document.documentElement.scrollWidth <=
+ document.documentElement.clientWidth,
+ ),
+ ).toBe(true);
+
+ await localReasoning.selectOption("none");
+ await localForm.locator('button[type="submit"]').click();
+ await expect.poll(() => patchBodies.length).toBe(1);
+ expect(patchBodies[0].localReasoningEffort).toBe("none");
+
+ await page
+ .getByTestId("ai-active-provider-select")
+ .selectOption("openai-compatible");
+ const compatForm = page.getByTestId("ai-provider-config-openai-compatible");
+ const compatReasoning = page.locator("#ai-compat-reasoning");
+ await expect(compatReasoning).toHaveValue("");
+ await compatReasoning.selectOption("high");
+ await compatForm.locator('button[type="submit"]').click();
+ await expect.poll(() => patchBodies.length).toBe(2);
+ expect(patchBodies[1].compatReasoningEffort).toBe("high");
+ expect(patchBodies[1]).not.toHaveProperty("localReasoningEffort");
+ });
});
diff --git a/e2e/chart-text-measurement-host.spec.ts b/e2e/chart-text-measurement-host.spec.ts
new file mode 100644
index 000000000..8da0ea448
--- /dev/null
+++ b/e2e/chart-text-measurement-host.spec.ts
@@ -0,0 +1,104 @@
+import { expect, test } from "./setup/test";
+
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+
+/**
+ * Recharts measures every tick label in one shared off-screen span
+ * (`#recharts_measurement_span`). Under `` each read cost a
+ * document-wide style recalc, because the stylesheet's descendant-subject
+ * `:has()` rules leave Chromium no way to scope the invalidation; on a
+ * seeded dashboard that was about 1.8 s of main-thread time under a 4x CPU
+ * slowdown. The chart runtime now parks the span in a contained host under
+ * `` (`src/components/charts/text-measurement-host.ts`).
+ *
+ * Pinned here, in a real engine:
+ * - the span Recharts uses is the one in the host, not a second one it
+ * created under ``, including after a client-side round trip;
+ * - every string the charts drew measures exactly as it does under
+ * ``, so the move cannot change a single tick or label.
+ */
+test.describe("chart text measurement host", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ async function assertHostInPlace(page: import("@playwright/test").Page) {
+ const placement = await page.evaluate(() => {
+ const spans = document.querySelectorAll("#recharts_measurement_span");
+ const span = spans[0] as HTMLElement | undefined;
+ return {
+ count: spans.length,
+ hostSlot: span?.parentElement?.dataset.slot ?? null,
+ hostParent: span?.parentElement?.parentElement?.tagName ?? null,
+ };
+ });
+ expect(placement).toEqual({
+ count: 1,
+ hostSlot: "chart-text-measurement-host",
+ hostParent: "HTML",
+ });
+ }
+
+ test("the dashboard charts measure text outside , with identical results", async ({
+ page,
+ }) => {
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ await expect(page.locator(".recharts-wrapper").first()).toBeVisible({
+ timeout: 20_000,
+ });
+ await assertHostInPlace(page);
+
+ const parity = await page.evaluate(() => {
+ const host = document.getElementById(
+ "recharts_measurement_span",
+ ) as HTMLElement;
+ const reference = document.createElement("span");
+ document.body.appendChild(reference);
+ // Recharts' own measurement style (`getStringSize`).
+ const base = {
+ position: "absolute",
+ top: "-20000px",
+ left: "0",
+ padding: "0",
+ margin: "0",
+ border: "none",
+ whiteSpace: "pre",
+ };
+ const strings = [
+ ...new Set(
+ [...document.querySelectorAll(".recharts-wrapper text")]
+ .map((el) => el.textContent ?? "")
+ .filter(Boolean),
+ ),
+ ];
+ const mismatches: string[] = [];
+ for (const fontSize of ["10px", "12px", "14px"]) {
+ for (const text of strings) {
+ Object.assign(host.style, base, { fontSize });
+ Object.assign(reference.style, base, { fontSize });
+ host.textContent = text;
+ reference.textContent = text;
+ const a = host.getBoundingClientRect();
+ const b = reference.getBoundingClientRect();
+ if (a.width !== b.width || a.height !== b.height) {
+ mismatches.push(`${text}@${fontSize}`);
+ }
+ }
+ }
+ reference.remove();
+ return { measured: strings.length, mismatches };
+ });
+ // A chart with no labels would make the parity check vacuous.
+ expect(parity.measured).toBeGreaterThan(0);
+ expect(parity.mismatches).toEqual([]);
+
+ // A client-side round trip re-renders the shell; the host must survive
+ // it, or Recharts quietly recreates its span under .
+ await page.locator('a[href="/insights"]:visible').first().click();
+ await page.waitForURL((url) => url.pathname === "/insights");
+ await page.locator('a[href="/"]:visible').first().click();
+ await page.waitForURL((url) => url.pathname === "/");
+ await expect(page.locator(".recharts-wrapper").first()).toBeVisible({
+ timeout: 20_000,
+ });
+ await assertHostInPlace(page);
+ });
+});
diff --git a/e2e/coach-activity.spec.ts b/e2e/coach-activity.spec.ts
new file mode 100644
index 000000000..b48b92f6b
--- /dev/null
+++ b/e2e/coach-activity.spec.ts
@@ -0,0 +1,524 @@
+import AxeBuilder from "@axe-core/playwright";
+import type { Page } from "@playwright/test";
+import { mkdirSync } from "node:fs";
+import { join } from "node:path";
+
+import { expect, test } from "./setup/test";
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
+import {
+ FIXED_NOW,
+ done,
+ end,
+ feed,
+ installLiveCoachStream,
+ posts,
+ serveConversation,
+ tokens,
+ applyTheme,
+ type StubConversation,
+} from "./setup/coach-live-stream";
+
+/**
+ * The thinking row of a Coach turn: one quiet row at the top of the answer.
+ *
+ * Contracts under test, at 1440 and 390 px, light and dark:
+ *
+ * 1. While the turn runs the row says what is happening right now and
+ * moves with the frames: Thinking… → Fetching blood pressure… →
+ * Summarising 214 readings… → Writing the answer…, with the words
+ * shimmering and no clock.
+ * 2. A table the turn has read shows nothing of its own while the turn
+ * runs: no second row, no preview, no sparkline.
+ * 3. Nothing opens by itself: the row stays closed (`aria-expanded`
+ * false) in every state, after the answer and after a reload; a row
+ * opened during the run closes when the answer is done.
+ * 4. After the answer the row reads "Thought process (4 steps)" and the
+ * table the answer points at renders as its chart.
+ * 5. A tap opens the steps; a reasoning round shows its summary under
+ * its title.
+ * 6. No horizontal scroll, a 44 px tap target on phones, axe clean.
+ *
+ * The chat stream is fed frame by frame from the spec
+ * (`installLiveCoachStream`), so every live state is observed rather than
+ * flashing by. Set COACH_SCREENSHOTS_DIR to also write screenshots of each
+ * state; those runs keep motion on so the shimmer shows mid-sweep.
+ */
+
+const CONVERSATION_ID = "coach-activity-e2e";
+const QUESTION = "How was my blood pressure this month?";
+const ANSWER =
+ "Your blood pressure held a steady band this month, around 126/82 mmHg on most days. Nothing stands out against your usual range.";
+
+const BP_VALUES: Array<[number, number]> = [
+ [128, 84],
+ [125, 82],
+ [131, 85],
+ [122, 80],
+ [124, 81],
+ [127, 83],
+ [121, 79],
+ [126, 82],
+ [123, 80],
+ [126, 81],
+];
+
+const BP_TABLE = {
+ ref: "r1",
+ source: {
+ tool: "get_metric_table",
+ domain: "bp",
+ window: "last30days",
+ period: "current",
+ granularity: "day",
+ },
+ shape: "timeSeries",
+ titleKey: "coach.result.title.byDay",
+ title: "Blood pressure by day",
+ rowCount: BP_VALUES.length,
+ chartKind: "line",
+ displayed: false,
+ columns: [
+ {
+ key: "day",
+ kind: "period",
+ labelKey: "coach.result.column.day",
+ label: "Day",
+ },
+ {
+ key: "systolic",
+ kind: "number",
+ labelKey: "coach.result.column.systolic",
+ label: "Systolic",
+ unit: "mmHg",
+ decimals: 0,
+ },
+ {
+ key: "diastolic",
+ kind: "number",
+ labelKey: "coach.result.column.diastolic",
+ label: "Diastolic",
+ unit: "mmHg",
+ decimals: 0,
+ },
+ {
+ key: "readings",
+ kind: "count",
+ labelKey: "coach.result.column.readings",
+ label: "Readings",
+ },
+ ],
+ rows: BP_VALUES.map(([sys, dia], i) => [
+ new Date(Date.UTC(2026, 8, 7 + i)).toISOString().slice(0, 10),
+ sys,
+ dia,
+ i === 0 ? 205 : 1,
+ ]),
+ truncated: false,
+ chart: { kind: "line", x: "day", series: ["systolic", "diastolic"] },
+};
+const BP_SHOWN = { ...BP_TABLE, displayed: true };
+const BP_META = {
+ ref: "r1",
+ source: BP_TABLE.source,
+ shape: BP_TABLE.shape,
+ titleKey: BP_TABLE.titleKey,
+ title: BP_TABLE.title,
+ rowCount: BP_TABLE.rowCount,
+ chartKind: "line",
+ displayed: true,
+};
+
+const STEP = {
+ id: "s1",
+ tool: "get_metric_table",
+ labelKey: "coach.step.readWindow",
+ label: "Checking: Blood pressure, last 30 days",
+ domain: "bp",
+ window: "last30days",
+ granularity: "day",
+};
+
+function entry(
+ id: string,
+ phase: string,
+ status: string,
+ label: string,
+ extra: Record = {},
+) {
+ return {
+ type: "activity",
+ activity: {
+ id,
+ phase,
+ status,
+ round: phase === "answer" ? 2 : 1,
+ labelKey: `insights.coach.activity.${phase}`,
+ label,
+ ...extra,
+ },
+ };
+}
+
+const THINK_TITLE = "Looking at the whole month";
+const THINK_TEXT =
+ "The month has daily readings; a daily table answers this best.";
+
+const ACTIVITY_META = [
+ {
+ id: "a1",
+ phase: "thinking",
+ status: "done",
+ round: 1,
+ labelKey: "insights.coach.activity.thinking",
+ label: "Thinking…",
+ durationMs: 2_400,
+ },
+ {
+ id: "a2",
+ phase: "fetch",
+ status: "done",
+ round: 1,
+ labelKey: "insights.coach.activity.fetching",
+ label: "Fetching blood pressure, last 30 days…",
+ stepRef: "s1",
+ count: 214,
+ durationMs: 900,
+ },
+ {
+ id: "a3",
+ phase: "digest",
+ status: "done",
+ round: 1,
+ labelKey: "insights.coach.activity.digestDone",
+ label: "214 readings from 1 area",
+ count: 214,
+ durationMs: 400,
+ },
+ {
+ id: "a4",
+ phase: "answer",
+ status: "done",
+ round: 2,
+ labelKey: "insights.coach.activity.answer",
+ label: "Writing the answer…",
+ durationMs: 3_000,
+ },
+];
+
+const METRIC_SOURCE = {
+ windows: ["last30days"],
+ metrics: ["bp"],
+ counts: { bp: 214 },
+ steps: [{ ...STEP, status: "done", count: 214, resultRef: "r1" }],
+ activity: ACTIVITY_META,
+ results: [BP_META],
+ method: {
+ entries: [
+ {
+ domain: "bp",
+ window: "last30days",
+ granularity: "day",
+ count: 214,
+ aggregation: "mean",
+ },
+ ],
+ text: "Blood pressure, last 30 days, 214 readings, daily averages.",
+ },
+};
+
+function conversation(): StubConversation {
+ return {
+ id: CONVERSATION_ID,
+ title: "Blood pressure this month",
+ messages: [],
+ results: { "coach-activity-e2e-a1": [BP_SHOWN] },
+ trails: {
+ "coach-activity-e2e-a1": {
+ entries: [{ id: "a1", title: THINK_TITLE, text: THINK_TEXT }],
+ },
+ },
+ };
+}
+
+const VIEWPORTS = [
+ { name: "1440", width: 1440, height: 900 },
+ { name: "390", width: 390, height: 844 },
+] as const;
+
+const SHOTS = process.env.COACH_SCREENSHOTS_DIR;
+
+async function shot(page: Page, name: string) {
+ if (!SHOTS) return;
+ mkdirSync(SHOTS, { recursive: true });
+ // The pointer off the row and the open/rotate transitions done, so the
+ // picture shows the resting state; the shimmer keeps running.
+ await page.mouse.move(0, 0);
+ await page.waitForTimeout(400);
+ await page.screenshot({ path: join(SHOTS, `${name}.png`) });
+}
+
+async function noHorizontalScroll(page: Page) {
+ const overflow = await page.evaluate(
+ () => document.documentElement.scrollWidth - window.innerWidth,
+ );
+ expect(overflow).toBeLessThanOrEqual(0);
+}
+
+test.describe("Coach live trail", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ for (const viewport of VIEWPORTS) {
+ for (const theme of ["light", "dark"] as const) {
+ test(`${viewport.name} px, ${theme}: one quiet line that never opens by itself`, async ({
+ page,
+ }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "sets its own viewports; one project suffices",
+ );
+ const label = `${viewport.name}-${theme}`;
+ await page.setViewportSize(viewport);
+ await page.clock.setFixedTime(FIXED_NOW);
+ await serveAiBlock(page, aiBlockAvailable());
+ const stub = conversation();
+ await serveConversation(page, stub);
+ await installLiveCoachStream(page);
+ await applyTheme(page, theme);
+ if (SHOTS) await page.emulateMedia({ reducedMotion: "no-preference" });
+
+ await page.goto("/coach", { waitUntil: "domcontentloaded" });
+ await page.locator('[data-slot="coach-input-textarea"]').fill(QUESTION);
+ await page.locator('[data-slot="coach-input-send"]').click();
+
+ const turn = page.locator(
+ '[role="log"] [data-slot="coach-bubble-assistant"]',
+ );
+ const line = turn.locator('[data-slot="coach-turn-steps"]');
+ const toggle = line.locator('[data-slot="coach-turn-steps-toggle"]');
+ const current = line.locator('[data-slot="coach-turn-steps-active"]');
+ const chevron = line.locator('[data-slot="coach-turn-steps-chevron"]');
+ const column = turn.locator('[data-slot="coach-answer-column"]');
+
+ // 1. Before any frame: Thinking…, shimmering, nothing to open yet.
+ await expect(line).toHaveAttribute("data-state", "running");
+ await expect(current).toHaveAttribute("data-text", "Thinking…");
+ await expect(current.locator(".text-shimmer")).toHaveText("Thinking");
+ await expect(current.locator(".waiting-dot")).toHaveCount(3);
+ await expect(chevron).toHaveCount(0);
+ await expect(toggle).toHaveCount(0);
+
+ await feed(page, [entry("a1", "thinking", "running", "Thinking…")]);
+ await feed(page, [
+ entry("a1", "thinking", "running", "Thinking…", {
+ title: THINK_TITLE,
+ }),
+ ]);
+ await expect(current).toHaveAttribute("data-text", `${THINK_TITLE}…`);
+ await expect(chevron).toBeVisible();
+ await expect(toggle).toHaveAttribute("aria-expanded", "false");
+
+ await feed(page, [
+ entry("a1", "thinking", "done", "Thinking…", {
+ title: THINK_TITLE,
+ text: THINK_TEXT,
+ durationMs: 2_400,
+ }),
+ { type: "step", step: { ...STEP, status: "running" } },
+ entry(
+ "a2",
+ "fetch",
+ "running",
+ "Fetching blood pressure, last 30 days…",
+ {
+ stepRef: "s1",
+ },
+ ),
+ ]);
+ await expect(current).toHaveAttribute(
+ "data-text",
+ "Fetching blood pressure, last 30 days…",
+ );
+
+ // 2. The table it read shows nothing of its own while the turn runs.
+ await feed(page, [
+ { type: "result", result: BP_TABLE, interim: true },
+ { type: "step", step: { ...STEP, status: "done", count: 214 } },
+ entry(
+ "a2",
+ "fetch",
+ "done",
+ "Fetching blood pressure, last 30 days…",
+ {
+ stepRef: "s1",
+ count: 214,
+ durationMs: 900,
+ },
+ ),
+ entry("a3", "digest", "running", "Summarising 214 readings…", {
+ count: 214,
+ }),
+ ]);
+ await expect(current).toHaveAttribute(
+ "data-text",
+ "Summarising 214 readings…",
+ );
+ // One row and nothing else in the answer's column: no table title,
+ // no sparkline, no clock.
+ await expect(column.locator(":scope > *")).toHaveCount(1);
+ await expect(turn).not.toContainText("Blood pressure by day");
+ await expect(turn.locator("svg polyline")).toHaveCount(0);
+ await expect(line).not.toContainText(/\d\s?s\b/);
+ await expect(
+ turn.locator('[data-slot="coach-answer-bubble"]'),
+ ).toHaveCount(0);
+ await expect(toggle).toHaveAttribute("aria-expanded", "false");
+ await noHorizontalScroll(page);
+ await shot(page, `${label}-1-running`);
+
+ // A tap opens the steps while the turn runs...
+ await toggle.click();
+ await expect(toggle).toHaveAttribute("aria-expanded", "true");
+ await expect(line.locator('[data-slot="coach-turn-step"]')).toHaveCount(
+ 3,
+ );
+ await expect(
+ line.locator('[data-slot="coach-turn-step"]').nth(2),
+ ).toHaveAttribute("data-status", "running");
+ await noHorizontalScroll(page);
+ await shot(page, `${label}-2-running-open`);
+
+ await feed(page, [
+ entry("a3", "digest", "done", "214 readings from 1 area", {
+ count: 214,
+ durationMs: 400,
+ }),
+ entry("a4", "answer", "running", "Writing the answer…"),
+ ...tokens(ANSWER),
+ ]);
+ await expect(current).toHaveAttribute(
+ "data-text",
+ "Writing the answer…",
+ );
+ await expect(
+ turn.locator('[data-slot="coach-answer-bubble"]'),
+ ).toContainText(ANSWER);
+ // ...and stays as the person left it until the turn ends.
+ await expect(toggle).toHaveAttribute("aria-expanded", "true");
+
+ // The persisted copy lands with the refetch after `done`.
+ stub.messages.push(
+ { id: "coach-activity-e2e-u1", role: "user", content: QUESTION },
+ {
+ id: "coach-activity-e2e-a1",
+ role: "assistant",
+ content: ANSWER,
+ metricSource: METRIC_SOURCE,
+ },
+ );
+ await feed(page, [
+ entry("a4", "answer", "done", "Writing the answer…", {
+ durationMs: 3_000,
+ }),
+ { type: "provenance", metricSource: METRIC_SOURCE },
+ { type: "result", result: BP_SHOWN },
+ done(CONVERSATION_ID, "coach-activity-e2e-a1"),
+ ]);
+ await end(page);
+
+ // 4. The thought process and its steps, closed again; the table the
+ // answer points at is its chart.
+ const bubble = page
+ .locator('[data-slot="coach-bubble-assistant"]')
+ .filter({ hasText: ANSWER });
+ const settled = bubble.locator('[data-slot="coach-turn-steps"]');
+ await expect(settled).toHaveAttribute("data-state", "done");
+ await expect(
+ settled.locator('[data-slot="coach-turn-steps-done"]'),
+ ).toHaveText("Thought process (4 steps)");
+ await expect(settled.locator(".text-shimmer")).toHaveCount(0);
+ const settledToggle = settled.locator(
+ '[data-slot="coach-turn-steps-toggle"]',
+ );
+ await expect(settledToggle).toHaveAttribute("aria-expanded", "false");
+ await expect(
+ settled.locator('[data-slot="coach-turn-steps-panel"]'),
+ ).toHaveCount(0);
+ await expect(
+ bubble.locator('[data-slot="coach-result-chart"][data-ref="r1"]'),
+ ).toBeVisible();
+ await expect(
+ bubble.locator('[data-slot="coach-result-chart-plot"]'),
+ ).toBeVisible();
+ expect((await posts(page))[0]).toMatchObject({ message: QUESTION });
+ await noHorizontalScroll(page);
+ await shot(page, `${label}-3-settled`);
+
+ // 6. A 44 px tap target on phones.
+ if (viewport.width < 640) {
+ const box = await settledToggle.boundingBox();
+ expect(box?.height ?? 0).toBeGreaterThanOrEqual(44);
+ }
+
+ // 5. A tap opens the steps; the reasoning round shows its summary
+ // under its title.
+ await settledToggle.click();
+ await expect(settledToggle).toHaveAttribute("aria-expanded", "true");
+ const panel = settled.locator('[data-slot="coach-turn-steps-panel"]');
+ await expect(panel).toBeVisible();
+ const rows = panel.locator('[data-slot="coach-turn-step"]');
+ await expect(rows).toHaveCount(4);
+ await expect(rows.nth(0)).toHaveAttribute("data-phase", "thinking");
+ await expect(rows.nth(1)).toHaveAttribute("data-phase", "fetch");
+ await expect(
+ panel.locator('[data-slot="coach-method-line"]'),
+ ).toContainText(METRIC_SOURCE.method.text);
+ await expect(rows.nth(0)).toContainText(THINK_TITLE);
+ await expect(
+ rows.nth(0).locator('[data-slot="coach-turn-step-detail"]'),
+ ).toHaveText(THINK_TEXT);
+ await expect(
+ panel.locator('[data-slot="coach-turn-step-connector"]'),
+ ).toHaveCount(3);
+ await noHorizontalScroll(page);
+ await shot(page, `${label}-4-settled-open`);
+
+ const axe = await new AxeBuilder({ page })
+ .include('[data-slot="coach-bubble-assistant"]')
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
+ .analyze();
+ expect(
+ axe.violations.map((v) => ({
+ id: v.id,
+ nodes: v.nodes.map((n) => n.target.join(" ")),
+ })),
+ ).toEqual([]);
+
+ // 3. After a reload the trail is closed again, and opening it reads
+ // the stored titles and texts.
+ await page.goto(`/coach?c=${CONVERSATION_ID}`, {
+ waitUntil: "domcontentloaded",
+ });
+ const reloaded = page
+ .locator('[data-slot="coach-bubble-assistant"]')
+ .filter({ hasText: ANSWER })
+ .locator('[data-slot="coach-turn-steps"]');
+ await expect(reloaded).toHaveAttribute("data-state", "done");
+ await expect(
+ reloaded.locator('[data-slot="coach-turn-steps-done"]'),
+ ).toHaveText("Thought process (4 steps)");
+ const reloadedToggle = reloaded.locator(
+ '[data-slot="coach-turn-steps-toggle"]',
+ );
+ await expect(reloadedToggle).toHaveAttribute("aria-expanded", "false");
+ await expect(
+ reloaded.locator('[data-slot="coach-turn-steps-panel"]'),
+ ).toHaveCount(0);
+ await reloadedToggle.click();
+ const first = reloaded.locator('[data-slot="coach-turn-step"]').nth(0);
+ await expect(first).toContainText(THINK_TITLE);
+ await expect(first).toContainText(THINK_TEXT);
+ });
+ }
+ }
+});
diff --git a/e2e/coach-answer-layout.spec.ts b/e2e/coach-answer-layout.spec.ts
new file mode 100644
index 000000000..060ab06b2
--- /dev/null
+++ b/e2e/coach-answer-layout.spec.ts
@@ -0,0 +1,382 @@
+import { mkdirSync } from "node:fs";
+import { join } from "node:path";
+import type { Locator, Page, Route, TestInfo } from "@playwright/test";
+
+import { expect, test } from "./setup/test";
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
+
+/**
+ * The geometry of a Coach answer, at 1440 x 900 (desktop project) and
+ * 390 x 844 (mobile project).
+ *
+ * Contracts under test:
+ *
+ * 1. Under an answer there is exactly one action row, left-aligned on the
+ * bubble's left edge and no taller than one 44 px target: copy, read
+ * aloud, try again, details, then the time. The model and the tokens
+ * are not text on the row; the details icon shows them in a tooltip on
+ * keyboard focus (desktop) and on a tap (phone); Enter and Space keep it
+ * open, and a second tap or Escape closes it.
+ * 2. The follow-up chips sit in the answer's column, right under the
+ * answer and above the action row (the row is the last thing in the
+ * column): their left edge is the bubble's, their text is the
+ * answer's size, and a chip is at most 36 px tall beside a pointer and
+ * at least 44 px on a phone.
+ * 3. Under a question, copy and remember share one row; nothing stacks
+ * below it.
+ * 4. Scrolled away from the end, a round button appears; it brings the
+ * thread back to the end and goes away again.
+ * 5. No thumbs, no evidence disclosure, no horizontal overflow.
+ *
+ * Screenshots are attached to the report (no pixel baselines) and, with
+ * `COACH_SHOT_DIR` set, written there too.
+ */
+
+const CONVERSATION_ID = "coach-answer-layout-e2e";
+const TITLE = "Blood pressure and sleep";
+
+const ANSWERS = [
+ "Your blood pressure stayed in a steady band over the last month. The morning readings sat a little higher than the evening ones, which is common, and none of the weeks stands out from the others.",
+ "Sleep looked regular on most nights. The weekends ran about forty minutes later at both ends, so the total stayed much the same while the timing moved.",
+ "Your resting heart rate held steady across the same weeks. Nothing in it lines up with the later weekend nights, so the two seem to move independently for now.",
+ "The period before looked much the same for blood pressure. Two readings in the second week were higher than the rest, and both were taken shortly after a workout.",
+ "Taken together, the month reads calm: steady blood pressure, regular sleep and a resting heart rate without surprises. Measuring at the same time each morning keeps the trend easy to read.",
+];
+const QUESTIONS = [
+ "How was my blood pressure this month?",
+ "And my sleep?",
+ "What about my resting heart rate?",
+ "Compare blood pressure with the period before",
+ "Sum it up for me",
+];
+
+const STEPS = [
+ {
+ id: "s1",
+ tool: "get_metric_series",
+ labelKey: "coach.step.readWindow",
+ label: "Checking: Blood pressure, last 30 days",
+ domain: "bp",
+ window: "last30days",
+ status: "done",
+ count: 28,
+ },
+ {
+ id: "s2",
+ tool: "get_sleep",
+ labelKey: "coach.step.readWindow",
+ label: "Checking: Sleep, last 30 days",
+ domain: "sleep",
+ window: "last30days",
+ status: "done",
+ count: 30,
+ },
+];
+
+const FOLLOW_UPS = [
+ {
+ id: "f1",
+ kind: "previous_period",
+ labelKey: "coach.followUp.previousPeriod",
+ label: "Compare with the period before",
+ reuse: false,
+ origin: "server",
+ },
+ {
+ id: "f2",
+ kind: "related_metric",
+ labelKey: "coach.followUp.relatedMetric",
+ label: "See also: Resting HR",
+ reuse: false,
+ origin: "server",
+ },
+];
+
+function messages() {
+ // Today, a few minutes apart, so the time reads as a bare clock.
+ const base = Date.now() - 60 * 60 * 1000;
+ const at = (minute: number) =>
+ new Date(base + minute * 60 * 1000).toISOString();
+ return ANSWERS.flatMap((answer, index) => [
+ {
+ id: `${CONVERSATION_ID}-u${index + 1}`,
+ role: "user",
+ content: QUESTIONS[index],
+ createdAt: at(index * 2),
+ metricSource: null,
+ providerType: null,
+ promptVersion: null,
+ tokensUsed: null,
+ model: null,
+ },
+ {
+ id: `${CONVERSATION_ID}-a${index + 1}`,
+ role: "assistant",
+ content: answer,
+ createdAt: at(index * 2 + 1),
+ metricSource: {
+ windows: ["last30days"],
+ metrics: ["bp", "sleep"],
+ steps: STEPS,
+ ...(index === ANSWERS.length - 1 ? { followUps: FOLLOW_UPS } : {}),
+ },
+ providerType: "mock",
+ promptVersion: "e2e",
+ tokensUsed: 1234,
+ model: "gpt-4o-mini-2024-07-18",
+ },
+ ]);
+}
+
+function fulfilJson(route: Route, data: unknown) {
+ return route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data, error: null }),
+ });
+}
+
+async function mockConversation(page: Page) {
+ const summary = {
+ id: CONVERSATION_ID,
+ title: TITLE,
+ createdAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
+ updatedAt: new Date().toISOString(),
+ messageCount: ANSWERS.length * 2,
+ fenced: false,
+ attachments: [],
+ documentTitle: null,
+ };
+ await page.route(
+ /\/api\/insights\/chat(?:\/[^?]+)?(?:\?.*)?$/,
+ async (route) => {
+ const url = new URL(route.request().url());
+ if (url.pathname.startsWith(`/api/insights/chat/${CONVERSATION_ID}/`)) {
+ return fulfilJson(route, { results: [] });
+ }
+ if (url.pathname === `/api/insights/chat/${CONVERSATION_ID}`) {
+ return fulfilJson(route, {
+ ...summary,
+ attachmentCount: 0,
+ summary: null,
+ messages: messages(),
+ });
+ }
+ return fulfilJson(route, { conversations: [summary], nextCursor: null });
+ },
+ );
+ await page.route("**/api/insights/coach/nudge-status*", (route) =>
+ fulfilJson(route, { nudgedAt: null, unread: false }),
+ );
+ await page.route("**/api/coach/about-me/questions*", (route) =>
+ fulfilJson(route, { questions: [] }),
+ );
+}
+
+async function box(locator: Locator) {
+ const b = await locator.boundingBox();
+ expect(b, "element has a box").not.toBeNull();
+ return b!;
+}
+
+async function shoot(page: Page, testInfo: TestInfo, name: string) {
+ const body = await page.screenshot({ fullPage: false });
+ await testInfo.attach(name, { body, contentType: "image/png" });
+ const dir = process.env.COACH_SHOT_DIR;
+ if (dir) {
+ mkdirSync(dir, { recursive: true });
+ await page.screenshot({ path: join(dir, `${name}.png`) });
+ }
+}
+
+const VIEWPORTS = {
+ "chromium-desktop": { width: 1440, height: 900, label: "1440" },
+ "chromium-mobile": { width: 390, height: 844, label: "390" },
+} as const;
+
+test.describe("Coach answer layout", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ test("one action row, chips in the answer column, jump to the latest", async ({
+ page,
+ }, testInfo) => {
+ const viewport = VIEWPORTS[testInfo.project.name as keyof typeof VIEWPORTS];
+ test.skip(!viewport, "measured in the desktop and the mobile project");
+ const desktop = viewport.width >= 640;
+ await page.setViewportSize({
+ width: viewport.width,
+ height: viewport.height,
+ });
+ await page.emulateMedia({ reducedMotion: "reduce" });
+ await serveAiBlock(page, aiBlockAvailable());
+ await mockConversation(page);
+
+ await page.goto(`/coach?c=${CONVERSATION_ID}`, {
+ waitUntil: "domcontentloaded",
+ });
+ const thread = page.locator('[data-slot="coach-message-thread"]');
+ const answers = page.locator('[data-slot="coach-bubble-assistant"]');
+ await expect(answers).toHaveCount(ANSWERS.length, { timeout: 15_000 });
+ const last = answers.last();
+ await expect(last).toContainText(ANSWERS.at(-1)!.slice(0, 40));
+ // The thread opens at its end.
+ await expect
+ .poll(() =>
+ thread.evaluate(
+ (el) => el.scrollHeight - el.scrollTop - el.clientHeight,
+ ),
+ )
+ .toBeLessThanOrEqual(2);
+
+ if (desktop) await last.hover();
+ await shoot(page, testInfo, `answer-${viewport.label}`);
+
+ // 5. Nothing of the old furniture.
+ await expect(
+ page.locator('[data-slot^="coach-message-feedback"]'),
+ ).toHaveCount(0);
+ await expect(page.locator('[data-slot^="coach-evidence"]')).toHaveCount(0);
+ await expect(page.locator('[data-slot="coach-token-footer"]')).toHaveCount(
+ 0,
+ );
+ for (const el of [page.locator("#main-content"), thread]) {
+ const overflow = await el.evaluate((n) => n.scrollWidth - n.clientWidth);
+ expect(overflow, "no horizontal overflow").toBeLessThanOrEqual(0);
+ }
+
+ // 1. One action row under the answer.
+ const bubble = last.locator('[data-slot="coach-answer-bubble"]');
+ const column = last.locator('[data-slot="coach-answer-column"]');
+ const row = last.locator('[data-slot="coach-answer-actions"]');
+ const info = row.locator('[data-slot="coach-answer-info"]');
+ const time = row.locator('[data-slot="coach-message-time"]');
+ await expect(row).toHaveCount(1);
+ await expect(row).toHaveCSS("opacity", "1");
+ await expect(row).not.toContainText("tokens");
+ await expect(row.locator('[data-slot="coach-answer-meta"]')).toHaveCount(0);
+ await expect(info).toHaveAttribute("aria-label", "Answer details");
+ const [bubbleBox, columnBox, rowBox, answerCopyBox, infoBox, timeBox] =
+ await Promise.all(
+ [
+ bubble,
+ column,
+ row,
+ row.locator('[data-slot="coach-copy-message"]'),
+ info,
+ time,
+ ].map(box),
+ );
+ expect(rowBox.height, "the row is one line").toBeLessThanOrEqual(44);
+ expect(Math.abs(answerCopyBox.x - bubbleBox.x)).toBeLessThanOrEqual(1);
+ expect(infoBox.x, "details follows the other icons").toBeGreaterThan(
+ answerCopyBox.x,
+ );
+ expect(timeBox.x, "the time comes last").toBeGreaterThan(infoBox.x);
+ expect(timeBox.x + timeBox.width).toBeLessThanOrEqual(
+ columnBox.x + columnBox.width + 1,
+ );
+ for (const [b, name] of [
+ [infoBox, "details"],
+ [timeBox, "time"],
+ ] as const) {
+ expect(
+ Math.abs(
+ b.y + b.height / 2 - (answerCopyBox.y + answerCopyBox.height / 2),
+ ),
+ `${name} shares the row's line`,
+ ).toBeLessThanOrEqual(2);
+ }
+
+ // The details tooltip: focus opens it on a desktop, a tap on a phone.
+ const tip = page.locator('[data-slot="coach-answer-info-content"]');
+ await expect(tip).toHaveCount(0);
+ if (desktop) await info.focus();
+ else await info.tap();
+ await expect(tip).toBeVisible();
+ await expect(tip).toContainText("Model: gpt-4o-mini-2024-07-18");
+ await expect(tip).toContainText("1,234 tokens");
+ await expect(info).toHaveAttribute("aria-describedby", /.+/);
+ await shoot(page, testInfo, `answer-info-${viewport.label}`);
+ if (desktop) {
+ // Enter and Space on the focused icon keep it open; Escape closes it.
+ for (const key of ["Enter", "Space"]) {
+ await page.keyboard.press(key);
+ await expect(tip).toBeVisible();
+ }
+ await page.keyboard.press("Escape");
+ } else await info.tap();
+ await expect(tip).toHaveCount(0);
+
+ // 2. The chips in the answer's column, at the answer's size.
+ const chips = last.locator('[data-slot="coach-follow-up-chips"]');
+ await expect(chips.locator("[data-follow-up-id]")).toHaveCount(2);
+ const chipsBox = await box(chips);
+ expect(Math.abs(chipsBox.x - bubbleBox.x)).toBeLessThanOrEqual(1);
+ // No empty band between the answer and its replies: the column's own
+ // gap, whatever the hover state of the row below.
+ const chipsGap = chipsBox.y - (bubbleBox.y + bubbleBox.height);
+ expect(chipsGap, "the chips follow the answer").toBeGreaterThanOrEqual(0);
+ expect(chipsGap, "the chips follow the answer").toBeLessThanOrEqual(12);
+ expect(rowBox.y, "the action row comes last").toBeGreaterThanOrEqual(
+ chipsBox.y + chipsBox.height - 1,
+ );
+ const answerSize = await bubble.evaluate(
+ (el) => getComputedStyle(el).fontSize,
+ );
+ for (const chip of await chips.locator("[data-follow-up-id]").all()) {
+ await expect(chip).toHaveCSS("font-size", answerSize);
+ const chipBox = await box(chip);
+ if (desktop) expect(chipBox.height).toBeLessThanOrEqual(36);
+ else expect(chipBox.height).toBeGreaterThanOrEqual(44);
+ }
+
+ // 3. Copy and remember share the question's one row.
+ const question = page.locator('[data-slot="coach-bubble-user"]').last();
+ if (desktop) await question.hover();
+ const userRow = question.locator('[data-slot="coach-user-actions"]');
+ const remember = userRow.locator('[data-slot="coach-remember-message"]');
+ const copy = userRow.locator('[data-slot="coach-copy-message"]');
+ await expect(remember).toHaveAttribute("aria-label", "Remember");
+ const [rememberBox, copyBox] = await Promise.all([remember, copy].map(box));
+ expect(
+ Math.abs(
+ rememberBox.y +
+ rememberBox.height / 2 -
+ (copyBox.y + copyBox.height / 2),
+ ),
+ ).toBeLessThanOrEqual(1);
+ // The question's column holds the bubble and its one row, nothing else.
+ expect(
+ await userRow.evaluate((el) => el.parentElement?.childElementCount),
+ ).toBe(2);
+
+ // 4. Away from the end, the button appears and brings the thread back.
+ const jump = page.locator('[data-slot="coach-scroll-to-bottom"]');
+ await expect(jump).toHaveAttribute("data-visible", "false");
+ await expect(jump).toHaveCSS("opacity", "0");
+ await thread.evaluate((el) => el.scrollTo({ top: 0, behavior: "auto" }));
+ await expect(jump).toHaveAttribute("data-visible", "true");
+ await expect(jump).toHaveCSS("opacity", "1");
+ await shoot(page, testInfo, `scrolled-up-${viewport.label}`);
+ const jumpBox = await box(jump);
+ const threadBox = await box(thread);
+ expect(
+ Math.abs(
+ jumpBox.x + jumpBox.width / 2 - (threadBox.x + threadBox.width / 2),
+ ),
+ "centred over the thread",
+ ).toBeLessThanOrEqual(1);
+ await jump.click();
+ await expect
+ .poll(() =>
+ thread.evaluate(
+ (el) => el.scrollHeight - el.scrollTop - el.clientHeight,
+ ),
+ )
+ .toBeLessThanOrEqual(2);
+ await expect(jump).toHaveAttribute("data-visible", "false");
+ await expect(jump).toHaveAttribute("tabindex", "-1");
+ await expect(page.locator("#coach-composer-textarea")).toBeFocused();
+ });
+});
diff --git a/e2e/coach-clarification.spec.ts b/e2e/coach-clarification.spec.ts
new file mode 100644
index 000000000..594fb17cf
--- /dev/null
+++ b/e2e/coach-clarification.spec.ts
@@ -0,0 +1,267 @@
+import { expect, test } from "./setup/test";
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
+import {
+ FIXED_NOW,
+ done,
+ end,
+ feed,
+ installLiveCoachStream,
+ posts,
+ serveConversation,
+ tokens,
+ applyTheme,
+ type StubConversation,
+} from "./setup/coach-live-stream";
+
+/**
+ * A clarifying question is a conversation, not a form.
+ *
+ * 1. The question is a normal Coach message; its choices are quiet reply
+ * pills under it, the assumed one first. There is no card.
+ * 2. A pill sends its label as the person's own message, with
+ * `clarification: { messageId, choiceId }`.
+ * 3. A typed answer works too, with `clarification: { messageId }`.
+ * 4. The trail above the question stays closed.
+ */
+
+const CONVERSATION_ID = "coach-clarify-e2e";
+const QUESTION = "Is my heart rate okay?";
+const ASKS =
+ "Do you mean your resting heart rate or your heart rate while walking? Otherwise I'll look at your resting heart rate.";
+const ANSWER =
+ "Your walking heart rate sat around 98 bpm over the last 30 days, close to your usual level.";
+
+const CLARIFICATION = {
+ kind: "metric",
+ freeText: true,
+ assumption: "c1",
+ choices: [
+ {
+ id: "c1",
+ labelKey: "insights.coach.metric.resting_hr",
+ label: "Resting HR",
+ value: { metric: "resting_hr" },
+ },
+ {
+ id: "c2",
+ labelKey: "insights.coach.metric.walking_hr",
+ label: "Walking HR",
+ value: { metric: "walking_hr" },
+ },
+ ],
+};
+const ASK_SOURCE = {
+ windows: ["last30days"],
+ metrics: ["resting_hr"],
+ activity: [
+ {
+ id: "a1",
+ phase: "thinking",
+ status: "done",
+ round: 1,
+ labelKey: "insights.coach.activity.thinking",
+ label: "Thinking…",
+ durationMs: 1_200,
+ },
+ {
+ id: "a2",
+ phase: "asking",
+ status: "done",
+ round: 1,
+ labelKey: "insights.coach.activity.asking",
+ label: "Asking you…",
+ durationMs: 300,
+ },
+ ],
+ clarification: CLARIFICATION,
+};
+
+async function ask(
+ page: import("@playwright/test").Page,
+ stub: StubConversation,
+) {
+ await page.locator('[data-slot="coach-input-textarea"]').fill(QUESTION);
+ await page.locator('[data-slot="coach-input-send"]').click();
+ const live = page.locator(
+ '[role="log"] [data-slot="coach-turn-steps-active"]',
+ );
+ await feed(page, [
+ {
+ type: "activity",
+ activity: {
+ ...ASK_SOURCE.activity[0],
+ status: "running",
+ durationMs: undefined,
+ },
+ },
+ {
+ type: "activity",
+ activity: {
+ ...ASK_SOURCE.activity[1],
+ status: "running",
+ durationMs: undefined,
+ },
+ },
+ ]);
+ await expect(live).toHaveAttribute("data-text", "Asking you…");
+ stub.messages.push(
+ { id: "coach-clarify-e2e-u1", role: "user", content: QUESTION },
+ {
+ id: "coach-clarify-e2e-a1",
+ role: "assistant",
+ content: ASKS,
+ metricSource: ASK_SOURCE,
+ },
+ );
+ await feed(page, [
+ ...tokens(ASKS),
+ { type: "activity", activity: ASK_SOURCE.activity[1] },
+ { type: "provenance", metricSource: ASK_SOURCE },
+ { type: "clarification", clarification: CLARIFICATION },
+ done(CONVERSATION_ID, "coach-clarify-e2e-a1"),
+ ]);
+ await end(page);
+}
+
+test.describe("Coach clarifying question", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ test("pills in the conversation, the assumed one first, and a tap answers", async ({
+ page,
+ }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "the flow is viewport-independent; the activity spec covers 390 px",
+ );
+ await page.clock.setFixedTime(FIXED_NOW);
+ await serveAiBlock(page, aiBlockAvailable());
+ const stub: StubConversation = {
+ id: CONVERSATION_ID,
+ title: "Heart rate",
+ messages: [],
+ results: {},
+ trails: {},
+ };
+ await serveConversation(page, stub);
+ await installLiveCoachStream(page);
+ await applyTheme(page, "light");
+ await page.goto("/coach", { waitUntil: "domcontentloaded" });
+
+ await ask(page, stub);
+
+ const asked = page
+ .locator('[data-slot="coach-bubble-assistant"]')
+ .filter({ hasText: ASKS });
+ await expect(asked).toHaveCount(1);
+ // 1. No card anywhere; the pills sit in the answer's own column.
+ await expect(
+ page.locator('[data-slot="coach-clarification-card"]'),
+ ).toHaveCount(0);
+ const pills = asked.locator('[data-slot="coach-suggested-replies"]');
+ await expect(pills).toBeVisible();
+ await expect(pills).toHaveAttribute(
+ "data-message-id",
+ "coach-clarify-e2e-a1",
+ );
+ const choices = pills.locator("[data-choice-id]");
+ await expect(choices).toHaveCount(2);
+ await expect(choices.nth(0)).toHaveAttribute("data-choice-id", "c1");
+ // 4. The row says an answer is needed, and stays closed.
+ const row = asked.locator('[data-slot="coach-turn-steps"]');
+ await expect(row).toHaveAttribute("data-state", "awaiting");
+ await expect(row.locator('[data-slot="coach-turn-steps-done"]')).toHaveText(
+ "Answer needed",
+ );
+ await expect(row.locator(".text-shimmer")).toHaveCount(0);
+ await expect(
+ asked.locator('[data-slot="coach-turn-steps-toggle"]'),
+ ).toHaveAttribute("aria-expanded", "false");
+
+ // 2. A tap sends the label as the person's message.
+ const walking = choices.nth(1);
+ const label = (await walking.textContent())?.trim() ?? "";
+ await walking.click();
+ await expect(
+ page
+ .locator('[data-slot="coach-bubble-user"]')
+ .filter({ hasText: label }),
+ ).toBeVisible();
+ await expect(pills).toHaveCount(0);
+ const sent = await posts(page);
+ expect(sent[1]).toMatchObject({
+ conversationId: CONVERSATION_ID,
+ message: label,
+ clarification: { messageId: "coach-clarify-e2e-a1", choiceId: "c2" },
+ });
+
+ stub.messages.push(
+ { id: "coach-clarify-e2e-u2", role: "user", content: label },
+ {
+ id: "coach-clarify-e2e-a2",
+ role: "assistant",
+ content: ANSWER,
+ metricSource: { windows: ["last30days"], metrics: ["walking_hr"] },
+ },
+ );
+ await feed(page, [
+ ...tokens(ANSWER),
+ done(CONVERSATION_ID, "coach-clarify-e2e-a2"),
+ ]);
+ await end(page);
+ await expect(
+ page
+ .locator('[data-slot="coach-bubble-assistant"]')
+ .filter({ hasText: ANSWER }),
+ ).toBeVisible();
+ await expect(
+ page.locator('[data-slot="coach-suggested-replies"]'),
+ ).toHaveCount(0);
+ });
+
+ test("a typed answer answers the question too", async ({
+ page,
+ }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "viewport-independent; desktop run suffices",
+ );
+ await page.clock.setFixedTime(FIXED_NOW);
+ await serveAiBlock(page, aiBlockAvailable());
+ const stub: StubConversation = {
+ id: CONVERSATION_ID,
+ title: "Heart rate",
+ messages: [],
+ results: {},
+ trails: {},
+ };
+ await serveConversation(page, stub);
+ await installLiveCoachStream(page);
+ await applyTheme(page, "dark");
+ await page.goto("/coach", { waitUntil: "domcontentloaded" });
+
+ await ask(page, stub);
+ await expect(
+ page.locator('[data-slot="coach-suggested-replies"]'),
+ ).toBeVisible();
+
+ await page
+ .locator('[data-slot="coach-input-textarea"]')
+ .fill("the walking one");
+ await page.locator('[data-slot="coach-input-send"]').click();
+ await expect(
+ page
+ .locator('[data-slot="coach-bubble-user"]')
+ .filter({ hasText: "the walking one" }),
+ ).toBeVisible();
+ const sent = await posts(page);
+ expect(sent[1]).toMatchObject({
+ message: "the walking one",
+ clarification: { messageId: "coach-clarify-e2e-a1" },
+ });
+ expect(
+ (sent[1].clarification as Record).choiceId,
+ ).toBeUndefined();
+ await end(page);
+ });
+});
diff --git a/e2e/coach-conversation-delete.spec.ts b/e2e/coach-conversation-delete.spec.ts
new file mode 100644
index 000000000..03b544dce
--- /dev/null
+++ b/e2e/coach-conversation-delete.spec.ts
@@ -0,0 +1,188 @@
+import { expect, test } from "./setup/test";
+import type { Route } from "@playwright/test";
+import { randomBytes } from "node:crypto";
+import { setTimeout as delay } from "node:timers/promises";
+import { hash } from "@node-rs/argon2";
+import pg from "pg";
+
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
+
+const KEEP_ID = "conversation-delete-keep";
+const DROP_ID = "conversation-delete-drop";
+
+function row(id: string, title: string) {
+ return {
+ id,
+ title,
+ createdAt: "2026-07-20T10:00:00.000Z",
+ updatedAt: "2026-07-20T10:00:00.000Z",
+ messageCount: 2,
+ fenced: false,
+ attachments: [],
+ documentTitle: null,
+ };
+}
+
+/**
+ * A deleted conversation must stay deleted. The delete waits out an undo
+ * window before it is sent; a reload inside that window used to drop it, and
+ * once the window ran out the row showed again until the refetch landed.
+ */
+test.describe("Coach conversation delete", () => {
+ test.beforeEach(async ({ page }) => {
+ await serveAiBlock(page, aiBlockAvailable());
+ });
+
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ async function serveConversations(
+ page: import("@playwright/test").Page,
+ deleteDelayMs: number,
+ ) {
+ const state = {
+ rows: [row(KEEP_ID, "Kept thread"), row(DROP_ID, "Dropped thread")],
+ deletes: [] as string[],
+ };
+ // Context-level, so a `keepalive` request sent while the page unloads is
+ // still answered here.
+ await page
+ .context()
+ .route(
+ /\/api\/insights\/chat(?:\/[^?]+)?(?:\?|$)/,
+ async (route: Route) => {
+ const request = route.request();
+ const url = new URL(request.url());
+ if (request.method() === "DELETE") {
+ const id = decodeURIComponent(url.pathname.split("/").pop() ?? "");
+ state.deletes.push(id);
+ state.rows = state.rows.filter((r) => r.id !== id);
+ if (deleteDelayMs > 0) await delay(deleteDelayMs);
+ return route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data: { deleted: true }, error: null }),
+ });
+ }
+ return route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: { conversations: state.rows, nextCursor: null },
+ error: null,
+ }),
+ });
+ },
+ );
+ return state;
+ }
+
+ test("the row does not come back when the window runs out", async ({
+ page,
+ }) => {
+ // A slow DELETE holds the refetch back, which is when the row used to
+ // reappear.
+ const state = await serveConversations(page, 2_000);
+ await page.goto("/coach/conversations", { waitUntil: "domcontentloaded" });
+
+ const items = page.locator('[data-slot="coach-conversations-item"]');
+ await expect(items).toHaveCount(2, { timeout: 10_000 });
+ await items
+ .filter({ hasText: "Dropped thread" })
+ .locator('[data-slot="coach-conversations-delete"]')
+ .click();
+ await expect(items).toHaveCount(1);
+
+ await expect
+ .poll(() => state.deletes, { timeout: 15_000 })
+ .toEqual([DROP_ID]);
+ // Sampled while the DELETE is still in flight.
+ for (let i = 0; i < 6; i += 1) {
+ expect(await items.count()).toBe(1);
+ await delay(250);
+ }
+ await expect(items).toContainText("Kept thread");
+ });
+});
+
+/**
+ * The real server and database, not a mock: the request that matters here is
+ * sent with `keepalive` while the page unloads, which Playwright's routing
+ * never sees. The account is the spec's own, so the conversation it seeds
+ * cannot surface in another spec's Coach.
+ */
+test.describe("Coach conversation delete against the database", () => {
+ test.use({ storageState: { cookies: [], origins: [] } });
+
+ test("a reload inside the undo window still deletes", async ({
+ page,
+ }, testInfo) => {
+ const dbUrl = process.env.DATABASE_URL;
+ test.skip(!dbUrl, "DATABASE_URL is required to seed the conversation");
+
+ const suffix = `${testInfo.project.name}-${randomBytes(4).toString("hex")}`;
+ const username = `e2e-coach-delete-${suffix}`;
+ const password = "Cd5!Vq8nTz3wLp7K";
+ const userId = `e2e-coach-delete-user-${suffix}`;
+ const conversationId = `e2e-coach-delete-${suffix}`;
+ const pool = new pg.Pool({ connectionString: dbUrl });
+ try {
+ await pool.query(
+ `INSERT INTO users
+ (id, username, email, password_hash, role, created_at, updated_at,
+ onboarding_completed_at, onboarding_tour_completed)
+ VALUES ($1, $2, $3, $4, 'USER', now(), now(), now(), true)`,
+ [
+ userId,
+ username,
+ `${username}@healthlog.test`,
+ await hash(password, {
+ memoryCost: 19456,
+ timeCost: 2,
+ outputLen: 32,
+ parallelism: 1,
+ }),
+ ],
+ );
+ // A legacy readable title keeps the seed free of the encryption key.
+ await pool.query(
+ `INSERT INTO coach_conversations (id, user_id, title, updated_at)
+ VALUES ($1, $2, 'Dropped thread', now())`,
+ [conversationId, userId],
+ );
+
+ const login = await page.request.post("/api/auth/login", {
+ data: { email: username, password },
+ });
+ expect(login.status()).toBe(200);
+
+ await page.goto("/coach/conversations", {
+ waitUntil: "domcontentloaded",
+ });
+ const items = page.locator('[data-slot="coach-conversations-item"]');
+ await expect(items).toHaveCount(1, { timeout: 15_000 });
+ await items.locator('[data-slot="coach-conversations-delete"]').click();
+ await expect(items).toHaveCount(0);
+
+ // Well inside the undo window.
+ await page.reload({ waitUntil: "domcontentloaded" });
+
+ await expect
+ .poll(async () => {
+ const { rowCount } = await pool.query(
+ "SELECT 1 FROM coach_conversations WHERE id = $1",
+ [conversationId],
+ );
+ return rowCount;
+ })
+ .toBe(0);
+ await expect(
+ page.locator('[data-slot="coach-conversations-empty"]'),
+ ).toBeVisible({ timeout: 15_000 });
+ await expect(items).toHaveCount(0);
+ } finally {
+ await pool.query("DELETE FROM users WHERE id = $1", [userId]);
+ await pool.end();
+ }
+ });
+});
diff --git a/e2e/coach-dialog.spec.ts b/e2e/coach-dialog.spec.ts
index 513d778e5..2f7581165 100644
--- a/e2e/coach-dialog.spec.ts
+++ b/e2e/coach-dialog.spec.ts
@@ -17,12 +17,13 @@ import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
* puts the table on the clipboard.
* 3. A follow-up chip under the latest reply sends its label with
* `followUp: { messageId, id }`.
- * 4. A reply that ends in a clarifying question shows the choices card
- * and no chips (as the server sends it); a choice sends its label with
- * `clarification: { messageId, choiceId }`, and the card leaves once a
+ * 4. A reply that ends in a clarifying question offers its choices as
+ * reply pills under it and no chips (as the server sends it); there is
+ * no card. A choice sends its label with
+ * `clarification: { messageId, choiceId }`, and the pills leave once a
* plain answer is the latest reply.
- * 5. axe finds nothing on the answer, the chips and the clarification
- * card, in the light and the dark theme.
+ * 5. axe finds nothing on the answer, the chips and the reply pills, in
+ * the light and the dark theme.
*
* The chat POST is a stubbed event stream in the documented frame order,
* and the conversation detail it refetches after `done` returns the same
@@ -441,7 +442,7 @@ async function expectNoAxeViolations(page: Page, label: string) {
const result = await new AxeBuilder({ page })
.include('[data-slot="coach-bubble-assistant"]')
.include('[data-slot="coach-follow-up-chips"]')
- .include('[data-slot="coach-clarification-card"]')
+ .include('[data-slot="coach-suggested-replies"]')
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
expect(
@@ -550,10 +551,11 @@ test.describe("Coach dialog", () => {
await expect(figure).toBeVisible();
await expect(table).toBeHidden();
- // The method line sits in the evidence disclosure.
- await firstBubble.locator('[data-slot="coach-evidence-summary"]').click();
+ // The method line closes the open steps list.
await expect(
- firstBubble.locator('[data-slot="coach-method-line"]'),
+ steps.locator(
+ '[data-slot="coach-turn-steps-panel"] [data-slot="coach-method-line"]',
+ ),
).toContainText(TURN_ONE_METHOD.text);
// 3. A follow-up chip sends which chip it was.
@@ -580,26 +582,31 @@ test.describe("Coach dialog", () => {
});
expect(posts[1].clarification).toBeUndefined();
- // 4. The reply asks; the card offers the choices, and the chips of
- // the earlier reply are gone.
- const card = page.locator('[data-slot="coach-clarification-card"]');
- await expect(card).toBeVisible();
- await expect(card).toHaveAttribute(
+ // 4. The reply asks; pills under it offer the choices, and the chips
+ // of the earlier reply are gone. No card.
+ await expect(
+ page.locator('[data-slot="coach-clarification-card"]'),
+ ).toHaveCount(0);
+ const pills = secondBubble.locator(
+ '[data-slot="coach-suggested-replies"]',
+ );
+ await expect(pills).toBeVisible();
+ await expect(pills).toHaveAttribute(
"data-message-id",
TURNS[1].assistantId,
);
- await expect(card.locator("[data-choice-id]")).toHaveCount(2);
+ await expect(pills.locator("[data-choice-id]")).toHaveCount(2);
await expect(chips).toHaveCount(0);
// The earlier result still renders under its own answer.
await expect(figure).toBeVisible();
- // 5. axe over the answers, the result in both views and the card.
+ // 5. axe over the answers, the result in both views and the pills.
await expectNoAxeViolations(page, `${theme} chart view with a question`);
await figure.locator('[data-slot="coach-result-view-table"]').click();
await expect(table).toBeVisible();
await expectNoAxeViolations(page, `${theme} table view with a question`);
- await card.locator('[data-choice-id="c2"]').click();
+ await pills.locator('[data-choice-id="c2"]').click();
const thirdBubble = page
.locator('[data-slot="coach-bubble-assistant"]')
@@ -615,7 +622,7 @@ test.describe("Coach dialog", () => {
expect(posts[2].followUp).toBeUndefined();
// A plain answer closes the question and offers no chips.
- await expect(card).toHaveCount(0);
+ await expect(pills).toHaveCount(0);
await expect(chips).toHaveCount(0);
await expect(
thirdBubble.locator('[data-slot="coach-turn-steps"]'),
diff --git a/e2e/coach-layout.spec.ts b/e2e/coach-layout.spec.ts
new file mode 100644
index 000000000..ae6bfbaec
--- /dev/null
+++ b/e2e/coach-layout.spec.ts
@@ -0,0 +1,1012 @@
+import AxeBuilder from "@axe-core/playwright";
+import type { Locator, Page, Request, Route, TestInfo } from "@playwright/test";
+
+import { expect, test } from "./setup/test";
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
+import { mockDay } from "./setup/day-mock";
+
+/**
+ * The Coach page frame: the conversation, the conversations panel right of
+ * it, and the day right of that; New conversation floating in the
+ * conversation itself.
+ *
+ * Contracts under test, at 1920, 1440, 1280 and 390 px:
+ *
+ * 1. From 1280 px the panel docks beside the thread, open on a first visit.
+ * From 1440 px the reading column (thread and composer) is exactly as
+ * wide with the panel open as folded; at 1280 the two strips leave the
+ * open column below its cap, so folding widens it up to the cap. Its 40 px strip stays at the right edge,
+ * left of the day's, open or not, and never moves: a click opens the
+ * panel left of the strips, a second click shuts it; the choice
+ * survives a reload. The panel is flush with the strips and runs from
+ * the top of the window to the bottom; the top bar ends at its left
+ * edge and the panel's header border is the top bar's. Both strips run
+ * the full height of the window, from its top edge, and the search
+ * magnifier ends the top bar in front of them, never covered. The strip
+ * is the docked panel's only fold control (the header has none); focus
+ * moves between the strip and the panel's heading, and Escape inside
+ * the open panel folds it.
+ * 2. New conversation is a round 56 px button in the conversation column,
+ * never over the panel, never over the composer's field, and absent
+ * from the panel's header.
+ * 3. A row's menu renames by keyboard (Escape cancels without closing the
+ * panel, Enter saves once) and deletes with an Undo that keeps the
+ * DELETE from ever leaving; a delete left alone commits once.
+ * 4. The gear opens the quick settings as a popover; Escape closes it and
+ * focus returns to the gear. `/coach?settings=data` opens it on "What I
+ * can see" and drops the param from the URL.
+ * 5. axe finds nothing on the open panel or the open settings, in the
+ * light and the dark theme.
+ * 6. The day docks right of the panel. At 1440 only one of the two is
+ * open: a day folds the panel, opening the panel folds the day. At 1920
+ * both stay open and the conversation keeps at least 560 px.
+ * 7. At 390 px the panel is a dialog sheet opened from the top bar's
+ * toggle; Escape closes it and focus returns to the toggle. The gear
+ * opens a bottom sheet that closes on Escape back to the gear. New
+ * conversation sits above the composer. Nothing scrolls sideways.
+ *
+ * Every Coach route is stubbed; the provider chain and provider config are
+ * stubbed too, so the model section renders the same on every machine.
+ * Screenshots are attached to the report (no pixel baselines).
+ */
+
+// A fixed midday, pinned in the browser too (see beforeEach): the panel groups
+// conversations by the browser's local day, so a run just after midnight in
+// the runner's or the profile's zone would move "an hour ago" into yesterday.
+const NOW = Date.parse("2026-06-04T12:00:00Z");
+const HOUR = 60 * 60 * 1000;
+
+interface Conversation {
+ id: string;
+ title: string;
+ updatedAt: string;
+}
+
+function conversations(): Conversation[] {
+ return [
+ {
+ id: "frame-bp",
+ title: "Blood pressure this week",
+ updatedAt: new Date(NOW - HOUR).toISOString(),
+ },
+ {
+ id: "frame-sleep",
+ title: "Sleep since May",
+ updatedAt: new Date(NOW - 26 * HOUR).toISOString(),
+ },
+ {
+ id: "frame-weight",
+ title: "Weight over the summer",
+ updatedAt: new Date(NOW - 10 * 24 * HOUR).toISOString(),
+ },
+ ];
+}
+
+const ANSWER =
+ "Your blood pressure held a steady band this week. The mornings ran a little higher than the evenings, which is common and nothing that stands out on its own.";
+
+function fulfilJson(route: Route, data: unknown, status = 200) {
+ return route.fulfill({
+ status,
+ contentType: "application/json",
+ body: JSON.stringify({ data, error: null }),
+ });
+}
+
+interface CoachMock {
+ patches: Array<{ id: string; title: string }>;
+ deletes: string[];
+}
+
+async function mockCoach(page: Page): Promise {
+ const state = conversations();
+ const mock: CoachMock = { patches: [], deletes: [] };
+
+ const dto = (c: Conversation) => ({
+ id: c.id,
+ title: c.title,
+ createdAt: c.updatedAt,
+ updatedAt: c.updatedAt,
+ messageCount: 2,
+ fenced: false,
+ attachments: [],
+ documentTitle: null,
+ });
+
+ await page.route(
+ /\/api\/insights\/chat(?:\/[^?]+)?(?:\?.*)?$/,
+ async (route: Route, request: Request) => {
+ const url = new URL(request.url());
+ const match = url.pathname.match(/^\/api\/insights\/chat\/([^/]+)$/);
+ if (match) {
+ const id = decodeURIComponent(match[1]);
+ const found = state.find((c) => c.id === id);
+ if (request.method() === "PATCH") {
+ const { title } = request.postDataJSON() as { title: string };
+ mock.patches.push({ id, title });
+ if (found) found.title = title;
+ return fulfilJson(route, { id, title });
+ }
+ if (request.method() === "DELETE") {
+ mock.deletes.push(id);
+ const idx = state.findIndex((c) => c.id === id);
+ if (idx >= 0) state.splice(idx, 1);
+ return fulfilJson(route, { deleted: true });
+ }
+ if (!found) return fulfilJson(route, null, 404);
+ return fulfilJson(route, {
+ ...dto(found),
+ attachmentCount: 0,
+ summary: null,
+ messages: [
+ {
+ id: `${id}-u`,
+ role: "user",
+ content: `Tell me about ${found.title.toLowerCase()}.`,
+ createdAt: found.updatedAt,
+ metricSource: null,
+ providerType: null,
+ promptVersion: null,
+ tokensUsed: null,
+ model: null,
+ },
+ {
+ id: `${id}-a`,
+ role: "assistant",
+ content: ANSWER,
+ createdAt: found.updatedAt,
+ metricSource: null,
+ providerType: "mock",
+ promptVersion: "e2e",
+ tokensUsed: 640,
+ model: "mock",
+ },
+ ],
+ });
+ }
+ if (url.pathname.startsWith("/api/insights/chat/")) {
+ return fulfilJson(route, { results: [] });
+ }
+ const q = (url.searchParams.get("q") ?? "").toLowerCase();
+ return fulfilJson(route, {
+ conversations: state
+ .filter((c) => c.title.toLowerCase().includes(q))
+ .map(dto),
+ nextCursor: null,
+ });
+ },
+ );
+ await page.route("**/api/insights/coach/nudge-status*", (route) =>
+ fulfilJson(route, { nudgedAt: null, unread: false }),
+ );
+ await page.route("**/api/coach/about-me/questions*", (route) =>
+ fulfilJson(route, { questions: [] }),
+ );
+ await page.route("**/api/insights/provider-chain", (route) =>
+ route.request().method() === "GET"
+ ? fulfilJson(route, {
+ activeProvider: "local",
+ cachedActiveProvider: "local",
+ configuredChain: [
+ { providerType: "local", enabled: true, available: true },
+ { providerType: "admin-openai", enabled: true, available: true },
+ ],
+ })
+ : fulfilJson(route, { saved: true }),
+ );
+ await page.route("**/api/user/ai-provider", (route) =>
+ route.request().method() === "GET"
+ ? fulfilJson(route, {
+ provider: "LOCAL",
+ model: "qwen2.5",
+ baseUrl: "http://localhost:11434/v1",
+ hasAnthropicKey: false,
+ anthropicKeyPreview: null,
+ hasLocalKey: false,
+ hasOpenaiKey: false,
+ openaiKeyPreview: null,
+ compatBaseUrl: null,
+ compatModel: null,
+ hasCompatKey: false,
+ responseTimeoutSeconds: null,
+ localReasoningEffort: null,
+ compatReasoningEffort: null,
+ })
+ : fulfilJson(route, { updated: true }),
+ );
+ return mock;
+}
+
+const toggle = (page: Page) => page.locator('[data-slot="coach-panel-toggle"]');
+const collapse = (page: Page) =>
+ page.locator('[data-slot="coach-panel-collapse"]');
+const heading = (page: Page) =>
+ page.locator('[data-slot="coach-panel-heading"]');
+const expand = (page: Page) =>
+ page.locator('[data-slot="coach-panel-strip-toggle"]');
+const strip = (page: Page) => page.locator('[data-slot="coach-panel-strip"]');
+const dayStrip = (page: Page) => page.locator('[data-slot="day-strip"]');
+const fab = (page: Page) => page.locator('[data-slot="coach-new-chat-fab"]');
+const dayPanel = (page: Page) => page.locator('[data-slot="day-panel"]');
+const panel = (page: Page) =>
+ page.locator('[data-slot="coach-conversations-panel"]');
+
+/** Width of the reading column: the thread's capped inner column. */
+async function columnWidths(page: Page) {
+ const thread = page.locator('[data-slot="coach-message-thread"] > *').first();
+ const composer = page.locator('[data-slot="coach-page-composer"] > div');
+ const t = await thread.boundingBox();
+ const c = await composer.boundingBox();
+ expect(t, "thread column").not.toBeNull();
+ expect(c, "composer column").not.toBeNull();
+ return { thread: t!.width, composer: c!.width, threadX: t!.x };
+}
+
+async function expectNoSidewaysScroll(page: Page) {
+ const overflow = await page.evaluate(() => {
+ const doc = document.scrollingElement!;
+ const main = document.getElementById("main-content")!;
+ return {
+ doc: doc.scrollWidth - doc.clientWidth,
+ main: main.scrollWidth - main.clientWidth,
+ };
+ });
+ expect(overflow.doc).toBeLessThanOrEqual(0);
+ expect(overflow.main).toBeLessThanOrEqual(0);
+}
+
+/**
+ * The docked panel is flush with its strips: the conversations' strip sits
+ * right of the panel and the day's right of that, against the viewport's
+ * right edge (no scrollbar gutter beside them). The panel runs from the top
+ * of the window to the bottom, the top bar ends at its left edge, an open
+ * panel's header row has the top bar's bottom border, and its own border
+ * is the left one only. Both strips run the full height of the window, and
+ * the search magnifier at the end of the top bar sits in front of them,
+ * whole and on top.
+ */
+async function expectPanelFlush(page: Page, width: number, height: number) {
+ const geometry = await page.evaluate(() => {
+ const box = (selector: string) =>
+ document.querySelector(selector)?.getBoundingClientRect() ?? null;
+ const outer = document.querySelector(
+ '[data-slot="coach-conversations-panel"]',
+ )!;
+ const inner = outer.querySelector("aside");
+ const style = inner ? getComputedStyle(inner) : null;
+ const main = document.getElementById("main-content")!;
+ const panel = outer.getBoundingClientRect();
+ return {
+ panel: {
+ left: panel.left,
+ right: panel.right,
+ top: panel.top,
+ bottom: panel.bottom,
+ },
+ header:
+ box('[data-slot="coach-conversations-panel-header"]')?.bottom ?? null,
+ bar: box('[data-slot="top-bar"]')!,
+ list: box('[data-slot="coach-panel-strip"]')!,
+ listButton: box('[data-slot="coach-panel-strip-toggle"]')!,
+ day: box('[data-slot="day-strip"]')!,
+ dayButton: box('[data-slot="day-strip-toggle"]')!,
+ search: box('[data-slot="command-palette-trigger"]')!,
+ searchOnTop: (() => {
+ const r = document
+ .querySelector('[data-slot="command-palette-trigger"]')!
+ .getBoundingClientRect();
+ return (
+ document
+ .elementFromPoint(r.left + r.width / 2, r.top + r.height / 2)
+ ?.closest('[data-slot="command-palette-trigger"]') != null
+ );
+ })(),
+ dayDock: box("#day-docked-panel")!,
+ gutter: main.offsetWidth - main.clientWidth,
+ borders: style
+ ? [
+ style.borderTopWidth,
+ style.borderRightWidth,
+ style.borderBottomWidth,
+ style.borderLeftWidth,
+ ]
+ : null,
+ };
+ });
+ expect(geometry.day.right).toBe(width);
+ expect(geometry.list.right).toBe(geometry.day.left);
+ expect(Math.round(geometry.list.width)).toBe(40);
+ expect(Math.round(geometry.day.width)).toBe(40);
+ // Conversations, then the day (open or slid to nothing), then the strips.
+ expect(geometry.panel.right).toBe(geometry.dayDock.left);
+ expect(geometry.dayDock.right).toBe(geometry.list.left);
+ expect(geometry.panel.top).toBe(0);
+ expect(geometry.panel.bottom).toBe(height);
+ expect(geometry.bar.right).toBe(geometry.panel.left);
+ // The strips run the full height, from the top edge of the window.
+ for (const button of [geometry.listButton, geometry.dayButton]) {
+ expect(button.top).toBe(0);
+ expect(button.bottom).toBe(height);
+ }
+ // The magnifier ends the top bar, left of the strips, whole and on top.
+ expect(geometry.search.top).toBeGreaterThanOrEqual(0);
+ expect(geometry.search.bottom).toBeLessThanOrEqual(geometry.bar.bottom);
+ expect(geometry.search.right).toBeLessThanOrEqual(geometry.bar.right);
+ expect(geometry.search.right).toBeLessThanOrEqual(geometry.list.left);
+ expect(geometry.searchOnTop).toBe(true);
+ if (geometry.header !== null) {
+ expect(geometry.header).toBe(geometry.bar.bottom);
+ expect(geometry.borders).toEqual(["0px", "0px", "0px", "1px"]);
+ }
+ expect(geometry.gutter).toBe(0);
+}
+
+/** Where a strip stands on screen. */
+async function stripBox(locator: Locator) {
+ const box = (await locator.boundingBox())!;
+ return { x: Math.round(box.x), width: Math.round(box.width) };
+}
+
+/**
+ * New conversation floats in the conversation column: a round 56 px button
+ * with a plus and its name, inside the column (never over a docked panel
+ * right of it) and clear of the composer's field. The panel's header has no
+ * such control.
+ */
+async function expectNewChatFab(page: Page) {
+ await expect(fab(page)).toHaveCount(1);
+ await expect(fab(page)).toHaveAttribute("aria-label", "New conversation");
+ await expect(fab(page).locator("svg.lucide-plus")).toHaveCount(1);
+ await expect(
+ panel(page).locator('[data-slot="coach-panel-new-chat"]'),
+ ).toHaveCount(0);
+ const box = (await fab(page).boundingBox())!;
+ expect(Math.round(box.width)).toBe(56);
+ expect(Math.round(box.height)).toBe(56);
+ const main = (await page
+ .locator('[data-slot="coach-page-main"]')
+ .boundingBox())!;
+ expect(box.x).toBeGreaterThanOrEqual(main.x);
+ expect(box.x + box.width).toBeLessThanOrEqual(main.x + main.width);
+ const field = (await page
+ .locator(
+ '[data-slot="coach-page-composer"] [data-slot="coach-input-textarea"]',
+ )
+ .boundingBox())!;
+ const overlaps =
+ box.x < field.x + field.width &&
+ box.x + box.width > field.x &&
+ box.y < field.y + field.height &&
+ box.y + box.height > field.y;
+ expect(overlaps, "the button leaves the composer's field free").toBe(false);
+}
+
+/** Rendered heights of the docked panel's header buttons and first row. */
+async function panelControlHeights(page: Page) {
+ const height = async (slot: string) =>
+ Math.round(
+ (await panel(page)
+ .locator(`[data-slot="${slot}"]`)
+ .first()
+ .boundingBox())!.height,
+ );
+ return {
+ plans: await height("coach-panel-plans"),
+ gear: await height("coach-settings"),
+ row: await height("coach-history-select"),
+ };
+}
+
+async function shot(page: Page, testInfo: TestInfo, name: string) {
+ const path = testInfo.outputPath(`${name}.png`);
+ await page.screenshot({ path });
+ await testInfo.attach(name, { path, contentType: "image/png" });
+ const dir = process.env.COACH_SHOTS_DIR;
+ if (dir) await page.screenshot({ path: `${dir}/${name}.png` });
+}
+
+/**
+ * Record every mount of the conversations sheet from the first byte on.
+ * Reads the added nodes rather than the live DOM, so a sheet that mounts and
+ * unmounts again before the observer's callback runs is still counted.
+ */
+async function watchSheetMounts(page: Page) {
+ await page.addInitScript(() => {
+ const w = window as unknown as { __coachSheetSeen?: boolean };
+ w.__coachSheetSeen = false;
+ const sheet = '[role="dialog"][data-slot="coach-conversations-panel"]';
+ new MutationObserver((records) => {
+ for (const record of records) {
+ for (const node of record.addedNodes) {
+ if (
+ node instanceof Element &&
+ (node.matches(sheet) || node.querySelector(sheet))
+ ) {
+ w.__coachSheetSeen = true;
+ }
+ }
+ }
+ }).observe(document, { childList: true, subtree: true });
+ });
+}
+
+async function sheetMounted(page: Page): Promise {
+ return page.evaluate(
+ () =>
+ (window as unknown as { __coachSheetSeen?: boolean }).__coachSheetSeen ===
+ true,
+ );
+}
+
+async function openCoach(page: Page, path = "/coach?c=frame-bp") {
+ await page.goto(path, { waitUntil: "domcontentloaded" });
+ await expect(
+ page.locator('[data-slot="coach-message-thread"]').getByText(ANSWER),
+ ).toBeVisible({ timeout: 15_000 });
+}
+
+async function waitForWidth(locator: Locator, width: number) {
+ await expect
+ .poll(async () => Math.round((await locator.boundingBox())?.width ?? -1))
+ .toBe(width);
+}
+
+test.describe("Coach page frame", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ test.beforeEach(async ({ page }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "each case sets its own viewport; the desktop project runs them all",
+ );
+ await page.clock.setFixedTime(new Date(NOW));
+ await serveAiBlock(page, aiBlockAvailable());
+ await page.emulateMedia({ reducedMotion: "reduce" });
+ });
+
+ for (const width of [1440, 1280]) {
+ test(`${width}: the panel docks, toggles, persists, and the reading column never changes width`, async ({
+ page,
+ }, testInfo) => {
+ await page.setViewportSize({ width, height: 900 });
+ await mockCoach(page);
+ await openCoach(page);
+
+ // First visit: open.
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ await waitForWidth(panel(page), 288);
+ // Docked, the strip is the one fold control: the top bar and the
+ // panel's header carry none.
+ await expect(toggle(page)).toHaveCount(0);
+ await expect(collapse(page)).toHaveCount(0);
+ await expect(expand(page)).toHaveAttribute(
+ "aria-controls",
+ "coach-conversations-panel",
+ );
+ const open = await columnWidths(page);
+ // The full 672 px column at 1440; at 1280 the two strips take 80 px
+ // of it and the column stays well above the 560 px floor.
+ if (width === 1440) expect(open.thread).toBeCloseTo(672, 0);
+ else expect(open.thread).toBeGreaterThanOrEqual(560);
+ expect(Math.abs(open.composer - open.thread)).toBeLessThanOrEqual(1);
+ // The panel sits right of the column, never over it.
+ const aside = (await panel(page).boundingBox())!;
+ expect(aside.x).toBeGreaterThanOrEqual(open.threadX + open.thread);
+ await expectPanelFlush(page, width, 900);
+ await expectNewChatFab(page);
+ // The bar says where the reader is: the page, then the conversation.
+ await expect(
+ page.locator('[data-slot="coach-top-bar-trail"]'),
+ ).toHaveText(/Coach.*Blood pressure this week/);
+ await expectNoSidewaysScroll(page);
+ await shot(page, testInfo, `coach-frame-${width}-open`);
+
+ // The strip stands still through a fold and an open.
+ const stripOpen = await stripBox(strip(page));
+ await expect(expand(page)).toHaveAttribute("aria-expanded", "true");
+
+ // Folded: the panel slides to nothing, the strip takes the focus, and
+ // the top bar ends at the strips.
+ await expand(page).click();
+ await expect(panel(page)).toHaveAttribute("data-state", "closed");
+ await waitForWidth(panel(page), 0);
+ expect(await stripBox(strip(page))).toEqual(stripOpen);
+ await expect(expand(page)).toBeFocused();
+ await expect(expand(page)).toHaveAttribute("aria-expanded", "false");
+ await expect(expand(page)).toHaveAttribute(
+ "aria-label",
+ "Show conversations",
+ );
+ await expect(
+ panel(page).locator('[data-slot="coach-history-select"]'),
+ ).toHaveCount(0);
+ await expectPanelFlush(page, width, 900);
+ const closed = await columnWidths(page);
+ if (width === 1440) {
+ expect(Math.abs(closed.thread - open.thread)).toBeLessThanOrEqual(1);
+ } else {
+ // At 1280 the strips leave the open column below its 672 px cap, so
+ // folding the list gives the column back what it lacked, and no more.
+ expect(closed.thread).toBeGreaterThanOrEqual(open.thread);
+ expect(closed.thread).toBeLessThanOrEqual(672 + 1);
+ }
+ expect(Math.abs(closed.composer - closed.thread)).toBeLessThanOrEqual(1);
+ await expectNewChatFab(page);
+ await expectNoSidewaysScroll(page);
+ await shot(page, testInfo, `coach-frame-${width}-closed`);
+
+ // Remembered across a reload.
+ await openCoach(page);
+ await expect(panel(page)).toHaveAttribute("data-state", "closed");
+
+ // A click on the strip opens it, a second click shuts it, and the
+ // strip does not move either time.
+ await expand(page).click();
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ expect(await stripBox(strip(page))).toEqual(stripOpen);
+ await expand(page).click();
+ await expect(panel(page)).toHaveAttribute("data-state", "closed");
+ await expect(expand(page)).toBeFocused();
+ expect(await stripBox(strip(page))).toEqual(stripOpen);
+
+ // Opened from the keyboard, focus lands on the panel's heading; Escape
+ // from inside folds it again and focus goes back to the strip.
+ await expand(page).focus();
+ await page.keyboard.press("Enter");
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ await expect(heading(page)).toBeFocused();
+ await page.keyboard.press("Escape");
+ await expect(panel(page)).toHaveAttribute("data-state", "closed");
+ await expect(expand(page)).toBeFocused();
+ await page.keyboard.press("Enter");
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ await panel(page)
+ .locator('[data-slot="coach-history-select"]')
+ .first()
+ .focus();
+ await page.keyboard.press("Escape");
+ await expect(panel(page)).toHaveAttribute("data-state", "closed");
+ await expect(expand(page)).toBeFocused();
+ });
+ }
+
+ test("1440: the day and the conversations take turns, focus included", async ({
+ page,
+ }, testInfo) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await mockCoach(page);
+ await mockDay(page);
+ const day = "2026-06-01";
+ await page.goto(`/coach?c=frame-bp&day=${day}`, {
+ waitUntil: "domcontentloaded",
+ });
+ await expect(
+ page.locator('[data-slot="coach-message-thread"]').getByText(ANSWER),
+ ).toBeVisible({ timeout: 15_000 });
+
+ // The day opens docked, left of the strips, and the conversations
+ // close for it; both strips stay.
+ await expect(dayPanel(page)).toHaveAttribute("data-shell", "docked");
+ await expect(panel(page)).toHaveAttribute("data-state", "closed");
+ await waitForWidth(panel(page), 0);
+ const dayBox = (await dayPanel(page).boundingBox())!;
+ const listStrip = (await strip(page).boundingBox())!;
+ expect(Math.round(dayBox.x + dayBox.width)).toBe(Math.round(listStrip.x));
+ expect(Math.round(listStrip.x + listStrip.width)).toBe(1400);
+ await expect(dayStrip(page)).toHaveAttribute("data-state", "open");
+ await expectPanelFlush(page, 1440, 900);
+ await expectNewChatFab(page);
+ expect(
+ (await page.locator('[data-slot="coach-page-main"]').boundingBox())!
+ .width,
+ ).toBeGreaterThanOrEqual(540);
+ await expectNoSidewaysScroll(page);
+ await shot(page, testInfo, "coach-frame-1440-day");
+
+ // Opening the conversations closes the day; its strip keeps the day,
+ // focus goes to the conversations' heading, the day is out of the
+ // URL, and the conversation stays the same.
+ await expand(page).click();
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ await expect(dayPanel(page)).toHaveCount(0);
+ await expect(dayStrip(page)).toHaveAttribute("data-day", day);
+ await expect(dayStrip(page)).toHaveAttribute("data-state", "closed");
+ await expect(heading(page)).toBeFocused();
+ await expect(page).toHaveURL(/\/coach\?c=frame-bp$/);
+ await expectNewChatFab(page);
+ await shot(page, testInfo, "coach-frame-1440-list");
+
+ // And back: the day's strip opens the day, the conversations close.
+ await page.locator('[data-slot="day-strip-toggle"]').click();
+ await expect(dayPanel(page)).toHaveAttribute("data-shell", "docked");
+ await expect(panel(page)).toHaveAttribute("data-state", "closed");
+ await expect(page).toHaveURL(new RegExp(`c=frame-bp&day=${day}$`));
+
+ // Switching conversations keeps the day open.
+ await expand(page).click();
+ await page.locator('[data-slot="day-strip-toggle"]').click();
+ await expect(dayPanel(page)).toBeVisible();
+ await fab(page).click();
+ await expect(page).toHaveURL(new RegExp(`/coach\\?day=${day}$`));
+ await expect(dayPanel(page)).toBeVisible();
+ });
+
+ test("1920: the conversations and the day stay open side by side", async ({
+ page,
+ }, testInfo) => {
+ await page.setViewportSize({ width: 1920, height: 1000 });
+ await mockCoach(page);
+ await mockDay(page);
+ await page.goto("/coach?c=frame-bp&day=2026-06-01", {
+ waitUntil: "domcontentloaded",
+ });
+ await expect(
+ page.locator('[data-slot="coach-message-thread"]').getByText(ANSWER),
+ ).toBeVisible({ timeout: 15_000 });
+ await expect(dayPanel(page)).toHaveAttribute("data-shell", "docked");
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ await waitForWidth(panel(page), 288);
+ const list = (await panel(page).boundingBox())!;
+ const dayBox = (await dayPanel(page).boundingBox())!;
+ expect(list.x + list.width).toBeLessThanOrEqual(dayBox.x + 1);
+ await expectPanelFlush(page, 1920, 1000);
+ expect(
+ (await page.locator('[data-slot="coach-page-main"]').boundingBox())!
+ .width,
+ ).toBeGreaterThanOrEqual(560);
+ await expectNewChatFab(page);
+ await expectNoSidewaysScroll(page);
+ await shot(page, testInfo, "coach-frame-1920-both");
+
+ // Folding one leaves the other alone.
+ await expand(page).click();
+ await expect(panel(page)).toHaveAttribute("data-state", "closed");
+ await expect(dayPanel(page)).toBeVisible();
+ await expand(page).click();
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ await expect(dayPanel(page)).toBeVisible();
+ });
+
+ test("1280 fine pointer: compact header buttons and rows", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1280, height: 900 });
+ await mockCoach(page);
+ await openCoach(page);
+ expect(await panelControlHeights(page)).toEqual({
+ plans: 28,
+ gear: 28,
+ row: 36,
+ });
+ });
+
+ test.describe("touch", () => {
+ // A touch tablet in landscape docks the panel too; the controls keep the
+ // 44 px floor because the size follows the input, not the width.
+ test.use({ hasTouch: true });
+
+ test("1280 touch: header buttons and rows keep the 44 px floor", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1280, height: 900 });
+ await mockCoach(page);
+ await openCoach(page);
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ expect(await panelControlHeights(page)).toEqual({
+ plans: 44,
+ gear: 44,
+ row: 44,
+ });
+ });
+ });
+
+ test("1440: picking a row opens it and New chat clears it, both in the URL", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await mockCoach(page);
+ await openCoach(page);
+
+ const rows = panel(page).locator('[data-slot="coach-history-select"]');
+ await expect(rows).toHaveCount(3);
+ await expect(panel(page).locator('[data-group="today"]')).toContainText(
+ "Blood pressure this week",
+ );
+ await expect(panel(page).locator('[data-group="yesterday"]')).toContainText(
+ "Sleep since May",
+ );
+ await expect(rows.nth(0)).toHaveAttribute("aria-current", "true");
+
+ await rows.nth(1).click();
+ await expect(page).toHaveURL(/\/coach\?c=frame-sleep$/);
+ await expect(rows.nth(1)).toHaveAttribute("aria-current", "true");
+
+ await fab(page).click();
+ await expect(page).toHaveURL(/\/coach$/);
+ await expect(
+ page.locator('[data-slot="coach-input-textarea"]'),
+ ).toBeVisible();
+ await expect(panel(page).locator('[aria-current="true"]')).toHaveCount(0);
+ });
+
+ test("1440: rename by keyboard and delete with undo from the row menu", async ({
+ page,
+ }, testInfo) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ const mock = await mockCoach(page);
+ await openCoach(page);
+
+ const row = panel(page)
+ .locator('[data-slot="coach-history-item"]')
+ .filter({ hasText: "Sleep since May" });
+ const menu = row.locator('[data-slot="coach-history-row-menu"]');
+ await expect(menu).toHaveAttribute(
+ "aria-label",
+ "Actions for Sleep since May",
+ );
+
+ // Keyboard: focus the menu button, open it, pick Rename.
+ await menu.focus();
+ await page.keyboard.press("Enter");
+ await page.getByRole("menuitem", { name: "Rename" }).click();
+ const input = row.locator('[data-slot="coach-conversation-rename-input"]');
+ await expect(input).toBeFocused();
+ await input.fill("Discard this");
+ await page.keyboard.press("Escape");
+ await expect(input).toHaveCount(0);
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ await expect(row).toContainText("Sleep since May");
+ await expect(menu).toBeFocused();
+ expect(mock.patches).toHaveLength(0);
+
+ await page.keyboard.press("Enter");
+ await page.getByRole("menuitem", { name: "Rename" }).click();
+ await expect(input).toBeFocused();
+ await input.fill("Sleep since spring");
+ await page.keyboard.press("Enter");
+ const renamed = panel(page)
+ .locator('[data-slot="coach-history-item"]')
+ .filter({ hasText: "Sleep since spring" });
+ await expect(renamed).toHaveCount(1);
+ await expect(input).toHaveCount(0);
+ expect(mock.patches).toEqual([
+ { id: "frame-sleep", title: "Sleep since spring" },
+ ]);
+
+ // Delete, then Undo: the row returns and no DELETE leaves.
+ await renamed.hover();
+ await renamed.locator('[data-slot="coach-history-row-menu"]').click();
+ await page.getByRole("menuitem", { name: "Delete" }).click();
+ await expect(renamed).toHaveCount(0);
+ await shot(page, testInfo, "coach-frame-1440-delete-toast");
+ await page.getByRole("button", { name: "Undo" }).click();
+ await expect(renamed).toHaveCount(1);
+ await page.waitForTimeout(6_500);
+ expect(mock.deletes).toEqual([]);
+
+ // Delete left alone commits once, after the undo window.
+ await renamed.hover();
+ await renamed.locator('[data-slot="coach-history-row-menu"]').click();
+ await page.getByRole("menuitem", { name: "Delete" }).click();
+ await expect(renamed).toHaveCount(0);
+ await expect
+ .poll(() => mock.deletes, { timeout: 10_000 })
+ .toEqual(["frame-sleep"]);
+ });
+
+ test("1440: the gear opens the quick settings as a popover; Escape returns to the gear", async ({
+ page,
+ }, testInfo) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await mockCoach(page);
+ await openCoach(page);
+
+ const gear = panel(page).locator('[data-slot="coach-settings"]');
+ await gear.click();
+ const popover = page.locator('[data-slot="coach-settings-popover"]');
+ await expect(popover).toBeVisible();
+ await expect(popover).toHaveAttribute("role", "dialog");
+ await expect(
+ popover.locator('[data-slot="coach-quick-provider"]'),
+ ).toHaveValue("local");
+ await expect(
+ popover.locator('[data-slot="coach-quick-model"]'),
+ ).toHaveValue("qwen2.5");
+ await expect(
+ popover.locator('[data-slot="coach-sources-window"]'),
+ ).toBeVisible();
+ // The popover stays inside the viewport.
+ const box = (await popover.boundingBox())!;
+ expect(box.x + box.width).toBeLessThanOrEqual(1440);
+ expect(box.y + box.height).toBeLessThanOrEqual(900);
+ await shot(page, testInfo, "coach-frame-1440-settings");
+
+ await page.keyboard.press("Escape");
+ await expect(popover).toHaveCount(0);
+ await expect(gear).toBeFocused();
+ // Escape on the popover does not also close the panel.
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ });
+
+ test("1440: /coach?settings=data opens the settings on what the Coach sees", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await mockCoach(page);
+ // A closed panel is revealed for the deep link.
+ await page.addInitScript(() => {
+ window.localStorage.setItem("healthlog.coach.panelOpen", "false");
+ });
+ // The hydration render does not know the viewport yet; the phone sheet
+ // must not mount on a desktop even for a moment.
+ await watchSheetMounts(page);
+ await page.goto("/coach?settings=data", { waitUntil: "domcontentloaded" });
+
+ const popover = page.locator('[data-slot="coach-settings-popover"]');
+ await expect(popover).toBeVisible({ timeout: 15_000 });
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ expect(
+ await sheetMounted(page),
+ "the phone sheet flashed on a desktop deep link",
+ ).toBe(false);
+ await expect(
+ popover.locator('[data-slot="coach-settings-data"]'),
+ ).toBeFocused();
+ await expect(page).toHaveURL(/\/coach$/);
+
+ await page.keyboard.press("Escape");
+ await expect(popover).toHaveCount(0);
+ });
+
+ for (const theme of ["light", "dark"] as const) {
+ test(`1440 ${theme}: axe finds nothing on the panel or the quick settings`, async ({
+ page,
+ }, testInfo) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.emulateMedia({ colorScheme: theme, reducedMotion: "reduce" });
+ await page.addInitScript((value: string) => {
+ window.localStorage.setItem("healthlog-theme", value);
+ }, theme);
+ await mockCoach(page);
+ await openCoach(page);
+ await expect(page.locator("html")).toHaveClass(
+ new RegExp(`(^|\\s)${theme}(\\s|$)`),
+ );
+ await expect(panel(page)).toHaveAttribute("data-state", "open");
+ await shot(page, testInfo, `coach-frame-1440-${theme}`);
+
+ const scan = async (selector: string, label: string) => {
+ const result = await new AxeBuilder({ page })
+ .include(selector)
+ .withTags([
+ "wcag2a",
+ "wcag2aa",
+ "wcag21a",
+ "wcag21aa",
+ "best-practice",
+ ])
+ .analyze();
+ expect(
+ result.violations.map((v) => ({
+ id: v.id,
+ nodes: v.nodes.map((n) => n.target.join(" ")),
+ })),
+ `${label} accessibility violations`,
+ ).toEqual([]);
+ };
+
+ await scan('[data-slot="coach-conversations-panel"]', "panel");
+ await shot(page, testInfo, `coach-frame-1440-${theme}-panel`);
+
+ await panel(page).locator('[data-slot="coach-settings"]').click();
+ const popover = page.locator('[data-slot="coach-settings-popover"]');
+ await expect(
+ popover.locator('[data-slot="coach-quick-provider"]'),
+ ).toBeVisible();
+ await scan('[data-slot="coach-settings-popover"]', "settings");
+ await shot(page, testInfo, `coach-frame-1440-${theme}-settings`);
+ });
+ }
+
+ test("390: /coach?settings=data opens the sheet and the settings on what the Coach sees", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await mockCoach(page);
+ await watchSheetMounts(page);
+ await page.goto("/coach?settings=data", { waitUntil: "domcontentloaded" });
+
+ // The settings bottom sheet opens over the conversations sheet and
+ // hides it from the accessibility tree, so the sheet is found by slot.
+ await expect(page.locator('[data-slot="coach-settings-data"]')).toBeVisible(
+ { timeout: 15_000 },
+ );
+ await expect(
+ page.locator('[role="dialog"][data-slot="coach-conversations-panel"]'),
+ ).toBeVisible();
+ // The positive control for the desktop case: the same watcher sees the
+ // sheet mount where it belongs.
+ expect(await sheetMounted(page)).toBe(true);
+ await expect(page).toHaveURL(/\/coach$/);
+ });
+
+ test("390: the panel is a sheet, the gear a bottom sheet, and nothing scrolls sideways", async ({
+ page,
+ }, testInfo) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await mockCoach(page);
+ await openCoach(page);
+
+ // Below 1280 px the panel waits to be asked.
+ await expect(panel(page)).toHaveCount(0);
+ await expect(toggle(page)).toHaveAttribute("aria-expanded", "false");
+ // New conversation sits above the composer, clear of its field.
+ await expectNewChatFab(page);
+ const fabBox = (await fab(page).boundingBox())!;
+ const composerBox = (await page
+ .locator('[data-slot="coach-page-composer"]')
+ .boundingBox())!;
+ expect(fabBox.y + fabBox.height).toBeLessThanOrEqual(composerBox.y);
+ await expectNoSidewaysScroll(page);
+ await shot(page, testInfo, "coach-frame-390-thread");
+
+ await toggle(page).click();
+ const sheet = page.getByRole("dialog", { name: "Conversations" });
+ await expect(sheet).toBeVisible();
+ await expect(toggle(page)).toHaveAttribute("aria-expanded", "true");
+ await expect(
+ sheet.locator('[data-slot="coach-panel-new-chat"]'),
+ ).toHaveCount(0);
+ // The title keeps its whole word beside four header controls.
+ const title = sheet.locator(
+ '[data-slot="coach-conversations-panel-header"] h2',
+ );
+ expect(
+ await title.evaluate((el) => el.scrollWidth <= el.clientWidth),
+ "the sheet title is not truncated",
+ ).toBe(true);
+ // Without a strip, the sheet keeps a clear close button and no fold.
+ await expect(
+ sheet.locator('[data-slot="coach-panel-close"]'),
+ ).toBeVisible();
+ await expect(
+ sheet.locator('[data-slot="coach-panel-close"]'),
+ ).toHaveAttribute("aria-label", "Close");
+ await expect(
+ sheet.locator('[data-slot="coach-panel-collapse"]'),
+ ).toHaveCount(0);
+ await shot(page, testInfo, "coach-frame-390-panel");
+
+ await page.keyboard.press("Escape");
+ await expect(sheet).toHaveCount(0);
+ await expect(toggle(page)).toBeFocused();
+
+ // The gear in the sheet opens a bottom sheet over it.
+ await toggle(page).click();
+ await expect(sheet).toBeVisible();
+ const gear = sheet.locator('[data-slot="coach-settings"]');
+ await gear.click();
+ const settings = page.locator('[data-slot="responsive-sheet-content"]');
+ await expect(settings).toHaveAttribute("data-variant", "sheet");
+ await expect(
+ settings.locator('[data-slot="coach-quick-provider"]'),
+ ).toBeVisible();
+ const box = (await settings.boundingBox())!;
+ expect(Math.round(box.y + box.height)).toBe(844);
+ await shot(page, testInfo, "coach-frame-390-settings");
+ await page.keyboard.press("Escape");
+ await expect(settings).toHaveCount(0);
+ await expect(gear).toBeFocused();
+ await expect(sheet).toBeVisible();
+
+ // Picking a conversation closes the sheet and opens the thread.
+ await sheet
+ .locator('[data-slot="coach-history-select"]')
+ .filter({ hasText: "Weight over the summer" })
+ .click();
+ await expect(sheet).toHaveCount(0);
+ await expect(page).toHaveURL(/\/coach\?c=frame-weight$/);
+ await expectNoSidewaysScroll(page);
+ });
+});
diff --git a/e2e/coach-memory.spec.ts b/e2e/coach-memory.spec.ts
new file mode 100644
index 000000000..e9ed1973d
--- /dev/null
+++ b/e2e/coach-memory.spec.ts
@@ -0,0 +1,436 @@
+import type { Page, Route } from "@playwright/test";
+
+import { expect, test } from "./setup/test";
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
+import {
+ FIXED_NOW,
+ done,
+ end,
+ feed,
+ installLiveCoachStream,
+ posts,
+ serveConversation,
+ tokens,
+ applyTheme,
+ type StubConversation,
+} from "./setup/coach-live-stream";
+
+/**
+ * What the Coach keeps, in the same quiet language as everything else in
+ * the thread, and the thinking depth in its quick settings.
+ *
+ * 1. A fact it saved: one meta line under the answer, "Remembered: …",
+ * with Undo, which forgets it.
+ * 2. A fact it proposes (health): no line; "Yes, remember it" / "No" as
+ * reply pills, a tap answers with `memoryDecision`.
+ * 3. A plan it proposes: "Take on this plan" / "Not now", with
+ * `planDecision`. No plan card in the thread.
+ * 4. The memory list in Settings → Coach: an entry is edited in place.
+ * 5. The thinking depth: levels above the operator's cap are disabled and
+ * say so; with reasoning switched off the field is locked with one
+ * sentence.
+ */
+
+const CONVERSATION_ID = "coach-memory-e2e";
+
+function fulfilJson(route: Route, data: unknown, status = 200) {
+ return route.fulfill({
+ status,
+ contentType: "application/json",
+ body: JSON.stringify({ data, error: null }),
+ });
+}
+
+interface Fact {
+ id: string;
+ category: string;
+ text: string;
+ confidence: number;
+ source: string;
+ createdAt: string;
+}
+
+async function serveFacts(page: Page, facts: Fact[]) {
+ const calls: Array<{ method: string; path: string; body: unknown }> = [];
+ await page.route(
+ /\/api\/insights\/coach\/facts(?:\/[^?]+)?(?:\?.*)?$/,
+ async (route) => {
+ const request = route.request();
+ const url = new URL(request.url());
+ const method = request.method();
+ calls.push({
+ method,
+ path: url.pathname,
+ body: method === "GET" ? null : request.postDataJSON(),
+ });
+ const id = url.pathname.split("/").pop() ?? "";
+ if (method === "DELETE") {
+ const at = facts.findIndex((f) => f.id === id);
+ if (at !== -1) facts.splice(at, 1);
+ return fulfilJson(route, { deleted: true });
+ }
+ if (method === "PATCH") {
+ const fact = facts.find((f) => f.id === id);
+ const next = (request.postDataJSON() as { fact: string }).fact;
+ if (fact) fact.text = next;
+ return fulfilJson(route, { fact });
+ }
+ return fulfilJson(route, { facts });
+ },
+ );
+ return calls;
+}
+
+function stub(): StubConversation {
+ return {
+ id: CONVERSATION_ID,
+ title: "Goals",
+ messages: [],
+ results: {},
+ trails: {},
+ };
+}
+
+async function send(page: Page, text: string) {
+ await page.locator('[data-slot="coach-input-textarea"]').fill(text);
+ await page.locator('[data-slot="coach-input-send"]').click();
+}
+
+async function answer(
+ page: Page,
+ conversation: StubConversation,
+ turn: {
+ userId: string;
+ userText: string;
+ id: string;
+ text: string;
+ metricSource: Record;
+ frames: unknown[];
+ providerType?: string;
+ },
+) {
+ conversation.messages.push(
+ { id: turn.userId, role: "user", content: turn.userText },
+ {
+ id: turn.id,
+ role: "assistant",
+ content: turn.text,
+ metricSource: turn.metricSource,
+ providerType: turn.providerType,
+ },
+ );
+ await feed(page, [
+ ...tokens(turn.text),
+ { type: "provenance", metricSource: turn.metricSource },
+ ...turn.frames,
+ done(CONVERSATION_ID, turn.id),
+ ]);
+ await end(page);
+ await expect(
+ page
+ .locator('[data-slot="coach-bubble-assistant"]')
+ .filter({ hasText: turn.text }),
+ ).toBeVisible();
+}
+
+test.describe("Coach memory and plans", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ test.beforeEach(async ({ page }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "viewport-independent; desktop run suffices",
+ );
+ await page.clock.setFixedTime(FIXED_NOW);
+ await applyTheme(page, "light");
+ });
+
+ test("a saved fact is one quiet line, and Undo forgets it", async ({
+ page,
+ }) => {
+ await serveAiBlock(page, aiBlockAvailable());
+ const facts: Fact[] = [
+ {
+ id: "fact-1",
+ category: "goal",
+ text: "You want to reach 75 kg by December",
+ confidence: 1,
+ source: "coach",
+ createdAt: "2026-10-06T09:00:00.000Z",
+ },
+ ];
+ const calls = await serveFacts(page, facts);
+ const conversation = stub();
+ await serveConversation(page, conversation);
+ await installLiveCoachStream(page);
+ await page.goto("/coach", { waitUntil: "domcontentloaded" });
+
+ await send(page, "I want to reach 75 kg by December.");
+ await answer(page, conversation, {
+ userId: "coach-memory-e2e-u1",
+ userText: "I want to reach 75 kg by December.",
+ id: "coach-memory-e2e-a1",
+ text: "That is about 3 kg from where you are now, a steady pace over ten weeks.",
+ metricSource: {
+ windows: [],
+ metrics: ["weight"],
+ memoryNote: { proposal: false, factId: "fact-1", category: "goal" },
+ },
+ frames: [
+ {
+ type: "memoryNote",
+ note: {
+ proposal: false,
+ factId: "fact-1",
+ category: "goal",
+ fact: "You want to reach 75 kg by December",
+ },
+ },
+ ],
+ });
+
+ const note = page.locator('[data-slot="coach-memory-note"]');
+ await expect(note).toHaveText(
+ /Remembered: You want to reach 75 kg by December\.\s*Undo/,
+ );
+ // No card, no accent: a meta line.
+ await expect(note).toHaveClass(/text-muted-foreground/);
+ await note.locator('[data-slot="coach-memory-note-undo"]').click();
+ await expect(note).toHaveCount(0);
+ expect(
+ calls.some((c) => c.method === "DELETE" && c.path.endsWith("/fact-1")),
+ ).toBe(true);
+ });
+
+ test("a proposed fact and a proposed plan are answered with reply pills", async ({
+ page,
+ }) => {
+ await serveAiBlock(page, aiBlockAvailable());
+ await serveFacts(page, []);
+ const conversation = stub();
+ await serveConversation(page, conversation);
+ await installLiveCoachStream(page);
+ await page.goto("/coach", { waitUntil: "domcontentloaded" });
+
+ // A health fact is proposed, never saved by itself.
+ await send(page, "I started taking ramipril in May.");
+ await answer(page, conversation, {
+ userId: "coach-memory-e2e-u1",
+ userText: "I started taking ramipril in May.",
+ id: "coach-memory-e2e-a1",
+ text: "Your readings since May sit a little lower. Should I remember that you take ramipril since May?",
+ metricSource: {
+ windows: [],
+ metrics: ["bp"],
+ memoryNote: {
+ proposal: true,
+ proposalId: "p1",
+ category: "medication",
+ },
+ },
+ frames: [
+ {
+ type: "memoryNote",
+ note: {
+ proposal: true,
+ proposalId: "p1",
+ category: "medication",
+ fact: "Takes ramipril since May",
+ },
+ },
+ ],
+ });
+ await expect(page.locator('[data-slot="coach-memory-note"]')).toHaveCount(
+ 0,
+ );
+ const pills = page.locator('[data-slot="coach-suggested-replies"]');
+ await expect(pills.locator("[data-reply-id]")).toHaveText([
+ "Yes, remember it",
+ "No",
+ ]);
+ await pills.locator('[data-reply-id="memory-accept"]').click();
+ await expect(
+ page
+ .locator('[data-slot="coach-bubble-user"]')
+ .filter({ hasText: "Yes, remember it" }),
+ ).toBeVisible();
+ expect((await posts(page))[1]).toMatchObject({
+ message: "Yes, remember it",
+ memoryDecision: {
+ messageId: "coach-memory-e2e-a1",
+ proposalId: "p1",
+ accept: true,
+ },
+ });
+ await answer(page, conversation, {
+ userId: "coach-memory-e2e-u2",
+ userText: "Yes, remember it",
+ id: "coach-memory-e2e-a2",
+ text: "Got it, I'll remember.",
+ providerType: "decision",
+ metricSource: { windows: [], metrics: [] },
+ frames: [],
+ });
+ await expect(pills).toHaveCount(0);
+
+ // A plan is one sentence in the answer, then two pills. No card.
+ await send(page, "How do I stop snacking late?");
+ await answer(page, conversation, {
+ userId: "coach-memory-e2e-u3",
+ userText: "How do I stop snacking late?",
+ id: "coach-memory-e2e-a3",
+ text: "When you finish dinner, then brush your teeth right away. I'll check in with you in 14 days.",
+ metricSource: {
+ windows: [],
+ metrics: [],
+ planProposal: { planId: "plan-1", metric: "WEIGHT", reviewInDays: 14 },
+ },
+ frames: [
+ {
+ type: "planProposal",
+ proposal: {
+ planId: "plan-1",
+ metric: "WEIGHT",
+ reviewInDays: 14,
+ ifCue: "When you finish dinner",
+ thenAction: "brush your teeth right away",
+ },
+ },
+ ],
+ });
+ await expect(
+ page.locator('[data-slot="coach-plan-proposal-card"]'),
+ ).toHaveCount(0);
+ await expect(pills.locator("[data-reply-id]")).toHaveText([
+ "Take on this plan",
+ "Not now",
+ ]);
+ await pills.locator('[data-reply-id="plan-decline"]').click();
+ expect((await posts(page))[3]).toMatchObject({
+ message: "Not now",
+ planDecision: {
+ messageId: "coach-memory-e2e-a3",
+ planId: "plan-1",
+ accept: false,
+ },
+ });
+ await end(page);
+ });
+
+ test("an entry in the memory list is edited in place", async ({ page }) => {
+ await serveAiBlock(page, aiBlockAvailable());
+ const calls = await serveFacts(page, [
+ {
+ id: "fact-1",
+ category: "goal",
+ text: "Wants to reach 80 kg by autumn",
+ confidence: 1,
+ source: "user",
+ createdAt: "2026-10-01T09:00:00.000Z",
+ },
+ {
+ id: "fact-2",
+ category: "preference",
+ text: "Prefers morning workouts",
+ confidence: 0.8,
+ source: "coach",
+ createdAt: "2026-10-02T09:00:00.000Z",
+ },
+ ]);
+ await page.goto("/settings/coach#coach-memory", {
+ waitUntil: "domcontentloaded",
+ });
+ const card = page.locator("#coach-memory");
+ const rows = card.locator('[data-testid="settings-coach-memory-fact"]');
+ await expect(rows).toHaveCount(2);
+ await expect(rows.nth(0)).toContainText("from you");
+ await expect(rows.nth(1)).toContainText("from the Coach");
+
+ await rows
+ .nth(0)
+ .locator('[data-slot="settings-coach-memory-edit"]')
+ .click();
+ const input = rows
+ .nth(0)
+ .locator('[data-slot="settings-coach-memory-edit-input"]');
+ await input.fill("Wants to reach 78 kg by December");
+ await rows
+ .nth(0)
+ .locator('[data-slot="settings-coach-memory-edit-save"]')
+ .click();
+ await expect(rows.nth(0)).toContainText("Wants to reach 78 kg by December");
+ expect(
+ calls.find((c) => c.method === "PATCH" && c.path.endsWith("/fact-1"))
+ ?.body,
+ ).toEqual({ fact: "Wants to reach 78 kg by December" });
+ });
+
+ for (const scenario of [
+ {
+ name: "capped at Medium",
+ block: {
+ level: "medium",
+ preference: "medium",
+ maxLevel: "medium",
+ available: true,
+ offIsReal: true,
+ source: "admin_cap",
+ },
+ },
+ {
+ name: "switched off by the operator",
+ block: {
+ level: "off",
+ preference: "medium",
+ maxLevel: "off",
+ available: false,
+ offIsReal: true,
+ source: "admin_off",
+ },
+ },
+ ]) {
+ test(`thinking depth in the quick settings, ${scenario.name}`, async ({
+ page,
+ }) => {
+ await page.route("**/api/auth/me", async (route) => {
+ const response = await route.fetch();
+ const body = (await response.json()) as {
+ data: Record | null;
+ };
+ if (body.data) {
+ body.data.ai = aiBlockAvailable();
+ body.data.coachReasoning = scenario.block;
+ }
+ await route.fulfill({ response, json: body });
+ });
+ await serveFacts(page, []);
+ await serveConversation(page, stub());
+ await page.goto("/coach", { waitUntil: "domcontentloaded" });
+ await page.locator('[data-slot="coach-settings"]').first().click();
+ const select = page.locator('[data-slot="coach-reasoning-select"]');
+ await expect(select).toBeVisible();
+ await expect(select.locator("option")).toHaveText([
+ "Off",
+ "Low",
+ "Medium",
+ scenario.block.available ? "High (limited by the admin)" : "High",
+ ]);
+ const field = page.locator('[data-slot="coach-reasoning-field"]');
+ if (scenario.block.available) {
+ await expect(select).toBeEnabled();
+ await expect(select.locator('option[value="high"]')).toBeDisabled();
+ await expect(field).toContainText("a little more waiting");
+ } else {
+ await expect(select).toBeDisabled();
+ await expect(field).toContainText(
+ "The admin has turned deeper thinking off.",
+ );
+ }
+ // The memory link names how much the Coach keeps.
+ await expect(
+ page.locator('[data-slot="coach-settings-memory-link"]'),
+ ).toHaveText("Memory (0)");
+ });
+ }
+});
diff --git a/e2e/coach-open-at-end.spec.ts b/e2e/coach-open-at-end.spec.ts
new file mode 100644
index 000000000..a86f0426c
--- /dev/null
+++ b/e2e/coach-open-at-end.spec.ts
@@ -0,0 +1,220 @@
+import type { Page, Route } from "@playwright/test";
+
+import { expect, test } from "./setup/test";
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
+
+/**
+ * A saved conversation opens at its last answer, and stays there after that
+ * answer's chart arrives.
+ *
+ * Before: on a wide screen the thread opened at the top (scroll 0), and on a
+ * phone it stopped about 70 px short of the end, because the answer's chart
+ * loaded after the jump. The conversation here is long enough to scroll on
+ * both widths, and the stored table under the last answer is served late on
+ * purpose, so the end moves after the thread first paints.
+ */
+
+const CONVERSATION_ID = "coach-open-at-end-e2e";
+const LAST_ID = "coach-open-at-end-last";
+
+const TABLE = {
+ ref: "r1",
+ source: {
+ tool: "get_metric_table",
+ domain: "pulse",
+ window: "last30days",
+ period: "current",
+ granularity: "day",
+ },
+ shape: "timeSeries",
+ titleKey: "coach.result.title.byDay",
+ title: "Pulse by day",
+ rowCount: 14,
+ chartKind: "line",
+ displayed: true,
+ columns: [
+ {
+ key: "day",
+ kind: "period",
+ labelKey: "coach.result.column.day",
+ label: "Day",
+ },
+ {
+ key: "pulse",
+ kind: "number",
+ labelKey: "coach.result.column.value",
+ label: "Pulse",
+ unit: "bpm",
+ decimals: 0,
+ },
+ ],
+ rows: Array.from({ length: 14 }, (_, i) => [
+ new Date(Date.UTC(2026, 8, 10 + i)).toISOString().slice(0, 10),
+ 60 + (i % 5),
+ ]),
+ truncated: false,
+ chart: { kind: "line", x: "day", series: ["pulse"] },
+};
+
+const PARAGRAPH =
+ "Your resting pulse held steady across the fortnight, with the usual small dip after the longer nights and a slight rise on the two days you logged a cold.";
+
+function messages() {
+ const out: Array> = [];
+ for (let i = 0; i < 8; i += 1) {
+ const at = `2026-09-24T09:${String(i * 2).padStart(2, "0")}:00.000Z`;
+ out.push({
+ id: `u-${i}`,
+ role: "user",
+ content: `Question ${i + 1}: how did my pulse look?`,
+ createdAt: at,
+ metricSource: null,
+ providerType: null,
+ promptVersion: null,
+ tokensUsed: null,
+ model: null,
+ });
+ const last = i === 7;
+ out.push({
+ id: last ? LAST_ID : `a-${i}`,
+ role: "assistant",
+ content: `${PARAGRAPH} ${PARAGRAPH}`,
+ createdAt: at,
+ metricSource: last
+ ? {
+ windows: ["last30days"],
+ metrics: ["pulse"],
+ results: [
+ {
+ ref: TABLE.ref,
+ source: TABLE.source,
+ shape: TABLE.shape,
+ titleKey: TABLE.titleKey,
+ title: TABLE.title,
+ rowCount: TABLE.rowCount,
+ chartKind: TABLE.chartKind,
+ displayed: TABLE.displayed,
+ },
+ ],
+ }
+ : null,
+ providerType: "mock",
+ promptVersion: "e2e",
+ tokensUsed: 42,
+ model: "mock",
+ });
+ }
+ return out;
+}
+
+function fulfilJson(route: Route, data: unknown) {
+ return route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data, error: null }),
+ });
+}
+
+async function mockConversation(page: Page) {
+ await page.route(
+ /\/api\/insights\/chat(?:\/[^?]+)?(?:\?.*)?$/,
+ async (route) => {
+ const url = new URL(route.request().url());
+ if (
+ url.pathname ===
+ `/api/insights/chat/${CONVERSATION_ID}/messages/${LAST_ID}/results`
+ ) {
+ // Late on purpose: the chart lands after the thread has painted.
+ await new Promise((resolve) => setTimeout(resolve, 900));
+ return fulfilJson(route, { results: [TABLE] });
+ }
+ if (url.pathname.startsWith(`/api/insights/chat/${CONVERSATION_ID}/`)) {
+ return fulfilJson(route, { results: [] });
+ }
+ if (url.pathname === `/api/insights/chat/${CONVERSATION_ID}`) {
+ return fulfilJson(route, {
+ id: CONVERSATION_ID,
+ title: "Pulse over two weeks",
+ createdAt: "2026-09-24T09:00:00.000Z",
+ updatedAt: "2026-09-24T09:14:00.000Z",
+ messageCount: 16,
+ fenced: false,
+ attachments: [],
+ documentTitle: null,
+ attachmentCount: 0,
+ summary: null,
+ messages: messages(),
+ });
+ }
+ return fulfilJson(route, { conversations: [], nextCursor: null });
+ },
+ );
+ await page.route("**/api/insights/coach/nudge-status*", (route) =>
+ fulfilJson(route, { nudgedAt: null, unread: false }),
+ );
+ await page.route("**/api/coach/about-me/questions*", (route) =>
+ fulfilJson(route, { questions: [] }),
+ );
+}
+
+/** Distance from the thread's current view to its end, in px. */
+async function distanceFromEnd(page: Page): Promise {
+ return page
+ .locator('[data-slot="coach-message-thread"]')
+ .evaluate((el) => el.scrollHeight - el.scrollTop - el.clientHeight);
+}
+
+test.describe("Coach: a saved conversation opens at its end", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ test.beforeEach(async ({ page }) => {
+ await serveAiBlock(page, aiBlockAvailable());
+ await mockConversation(page);
+ });
+
+ for (const width of [390, 1440] as const) {
+ test(`at ${width} px it lands on the last answer and stays after its chart loads`, async ({
+ page,
+ }) => {
+ await page.setViewportSize({
+ width,
+ height: width === 390 ? 844 : 900,
+ });
+ await page.goto(`/coach?c=${CONVERSATION_ID}`, {
+ waitUntil: "domcontentloaded",
+ });
+
+ // The empty state carries the same `coach-message-thread` slot, so the
+ // thread being visible says nothing about the conversation having
+ // arrived; the height used to be read from the empty state on a slow
+ // run. Wait for the frame only a loaded thread renders, and for every
+ // stored answer in it.
+ await expect(
+ page.locator('[data-slot="coach-message-thread-frame"]'),
+ ).toBeVisible({ timeout: 15_000 });
+ await expect(
+ page.locator('[data-slot="coach-assistant-turn"]'),
+ ).toHaveCount(8, { timeout: 15_000 });
+ const thread = page.locator(
+ '[data-slot="coach-message-thread-frame"] [data-slot="coach-message-thread"]',
+ );
+ // The thread scrolls at all: otherwise "at the end" proves nothing.
+ await expect
+ .poll(() =>
+ thread.evaluate((el) => el.scrollHeight > el.clientHeight + 200),
+ )
+ .toBe(true);
+
+ const chart = page.locator(
+ '[data-slot="coach-result-chart"][data-ref="r1"]',
+ );
+ await expect(chart).toBeVisible({ timeout: 15_000 });
+ // The chart grew the thread after the first jump; the view followed.
+ await expect.poll(() => distanceFromEnd(page)).toBeLessThanOrEqual(2);
+ await expect(
+ page.locator(`[data-slot="coach-assistant-turn"]`).last(),
+ ).toBeInViewport();
+ });
+ }
+});
diff --git a/e2e/coach-results.spec.ts b/e2e/coach-results.spec.ts
index a0f4d67af..ca211ec63 100644
--- a/e2e/coach-results.spec.ts
+++ b/e2e/coach-results.spec.ts
@@ -289,7 +289,7 @@ test.describe("Coach result tables and charts", () => {
});
for (const theme of ["light", "dark"] as const) {
- test(`theme=${theme}: chart first, table on toggle, data used in the evidence`, async ({
+ test(`theme=${theme}: chart first, table on toggle, data used in the steps list`, async ({
page,
}) => {
await useTheme(page, theme);
@@ -342,12 +342,18 @@ test.describe("Coach result tables and charts", () => {
await table.locator('[data-slot="coach-result-view-chart"]').click();
await expect(figure).toBeVisible();
- // The table the answer only used waits in the evidence disclosure.
+ // The table the answer only used waits in the open steps list.
await expect(
bubble.locator('[data-slot="coach-result-table"][data-ref="r2"]'),
).toBeHidden();
- await bubble.locator('[data-slot="coach-evidence-summary"]').click();
- const dataUsed = bubble.locator('[data-slot="coach-data-used"]');
+ const stepsToggle = bubble.locator(
+ '[data-slot="coach-turn-steps-toggle"]',
+ );
+ await expect(stepsToggle).toHaveAttribute("aria-expanded", "false");
+ await stepsToggle.click();
+ const dataUsed = bubble.locator(
+ '[data-slot="coach-turn-steps-panel"] [data-slot="coach-data-used"]',
+ );
await expect(dataUsed).toContainText("Data used (1)");
const workouts = dataUsed.locator(
'[data-slot="coach-result-table"][data-ref="r2"]',
diff --git a/e2e/command-palette.spec.ts b/e2e/command-palette.spec.ts
new file mode 100644
index 000000000..cae66b948
--- /dev/null
+++ b/e2e/command-palette.spec.ts
@@ -0,0 +1,275 @@
+import AxeBuilder from "@axe-core/playwright";
+import type { Page } from "@playwright/test";
+
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { expect, test } from "./setup/test";
+
+/**
+ * v1.42 — the command palette.
+ *
+ * - Cmd/Ctrl+K, "sleep", Enter lands on the sleep page;
+ * - a settings card is a link with its anchor, and the page scrolls to it;
+ * - a module's entry is a switch: Enter sends the module write and the
+ * palette stays open (the write is answered by a route mock, so the
+ * shared account's modules never move);
+ * - the top bar carries a magnifier at every width, named "Search",
+ * with a tooltip that gives the shortcut; on a phone it opens a
+ * full-height sheet with the field focused;
+ * - the open palette passes axe;
+ * - a hit from the record search ("In your records", answered by a route
+ * mock so the shared account's records never move) lists after the local
+ * results, Enter on it opens its page, and a visit lands on the visits
+ * tab; on a phone the hit fits the width.
+ *
+ * Stable hooks only: `command-palette` (+ `data-variant`),
+ * `command-palette-input`, `command-palette-trigger`,
+ * `command-palette-tooltip`,
+ * options by `data-entry`.
+ */
+
+test.use({ storageState: STORAGE_STATE_PATH });
+
+const palette = (page: Page) => page.locator('[data-slot="command-palette"]');
+
+/** Answer `POST /api/search` with one visit hit, and count the requests. */
+async function mockRecordSearch(page: Page): Promise {
+ const queries: string[] = [];
+ await page.route("**/api/search", async (route) => {
+ // The query rides the JSON body, never the URL.
+ expect(route.request().method()).toBe("POST");
+ expect(route.request().url()).not.toContain("?");
+ const body = route.request().postDataJSON() as { q?: string } | null;
+ queries.push(body?.q ?? "");
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ groups: [
+ {
+ kind: "visit",
+ more: false,
+ hits: [
+ {
+ id: "visit:e2e-visit",
+ kind: "visit",
+ title: { text: "Arthroscopy", marks: [] },
+ context: [
+ { text: "Shoulder (left)", marks: [[0, 8]] },
+ { text: "A practitioner with a long name", marks: [] },
+ ],
+ snippet: {
+ text: "Follow-up for the shoulder after the operation, physiotherapy twice a week",
+ marks: [[18, 26]],
+ },
+ day: "2026-03-02",
+ path: "/checkups?visit=e2e-visit",
+ source: null,
+ },
+ ],
+ },
+ ],
+ truncated: false,
+ },
+ error: null,
+ }),
+ });
+ });
+ return queries;
+}
+const input = (page: Page) =>
+ page.locator('[data-slot="command-palette-input"]');
+
+async function shellReady(page: Page): Promise {
+ await expect(page.locator("#main-content")).toBeVisible({ timeout: 20_000 });
+ await expect(page.locator('[data-slot="top-bar"]')).toBeVisible();
+}
+
+test.describe("command palette", () => {
+ test.beforeEach(async ({ context }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "palette journeys run on the desktop project (the phone case resizes)",
+ );
+ const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
+ await context.addCookies([
+ { name: "healthlog-locale", value: "en", url: baseURL },
+ ]);
+ });
+
+ test("Cmd/Ctrl+K, sleep, Enter opens the sleep page", async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto("/");
+ await shellReady(page);
+ await page.keyboard.press("ControlOrMeta+k");
+ await expect(palette(page)).toHaveAttribute("data-variant", "dialog");
+ await expect(input(page)).toBeFocused();
+ await input(page).fill("sleep");
+ // The best hit is the highlighted option.
+ const active = await input(page).getAttribute("aria-activedescendant");
+ await expect(page.locator(`[id="${active}"]`)).toHaveAttribute(
+ "data-entry",
+ "insights:sleep",
+ );
+ await page.keyboard.press("Enter");
+ await expect(page).toHaveURL(/\/insights\/sleep$/);
+ await expect(palette(page)).toHaveCount(0);
+ });
+
+ test("a settings card opens at its anchor", async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto("/");
+ await shellReady(page);
+ await page.keyboard.press("ControlOrMeta+k");
+ await input(page).fill("passkeys");
+ await page.locator('[data-entry="settings:security#passkeys"]').click();
+ await expect(page).toHaveURL(/\/settings\/security#passkeys$/);
+ await expect(page.locator("#passkeys")).toBeInViewport({ timeout: 10_000 });
+ });
+
+ test("a module is a switch; Enter flips it and the palette stays", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ const writes: unknown[] = [];
+ await page.route("**/api/auth/me/modules", async (route) => {
+ if (route.request().method() !== "PATCH") return route.fallback();
+ writes.push(route.request().postDataJSON());
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ modules: { timeline: true },
+ updatedAt: new Date().toISOString(),
+ },
+ error: null,
+ }),
+ });
+ });
+ await page.goto("/");
+ await shellReady(page);
+ await page.keyboard.press("ControlOrMeta+k");
+ await input(page).fill("timeline");
+ const option = page.locator('[data-entry="module:timeline"]');
+ await expect(option).toHaveAttribute("aria-checked", "false");
+ await option.hover();
+ await page.keyboard.press("Enter");
+ await expect.poll(() => writes.length).toBe(1);
+ expect(writes[0]).toMatchObject({ timeline: true });
+ await expect(palette(page)).toBeVisible();
+ await expect(input(page)).toHaveValue("timeline");
+ });
+
+ test("a hit from the record opens its page", async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ const queries = await mockRecordSearch(page);
+ // Start on the page the hit opens, on another tab: the link has to move
+ // the open page, not only a fresh one.
+ await page.goto("/checkups");
+ await shellReady(page);
+ await expect(
+ page.locator('[data-testid="checkups-tab-visits"]'),
+ ).toHaveAttribute("aria-selected", "false");
+ await page.keyboard.press("ControlOrMeta+k");
+ await input(page).fill("shoulder");
+ const hit = page.locator('[data-entry="record:visit:e2e-visit"]');
+ await expect(hit).toBeVisible();
+ // Asked once the typing paused, not once per letter.
+ expect(queries).toEqual(["shoulder"]);
+ await expect(
+ page.locator('[data-group="records"] mark').first(),
+ ).toHaveText("Shoulder");
+
+ const axe = await new AxeBuilder({ page })
+ .include('[data-slot="command-palette"]')
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
+ .analyze();
+ expect(axe.violations).toEqual([]);
+
+ // The arrows reach it after the local options, and Enter opens it.
+ for (let i = 0; i < 40; i++) {
+ const active = await input(page).getAttribute("aria-activedescendant");
+ if (active && (await hit.getAttribute("id")) === active) break;
+ await page.keyboard.press("ArrowDown");
+ }
+ await expect(hit).toHaveAttribute("aria-selected", "true");
+ await page.keyboard.press("Enter");
+ await expect(page).toHaveURL(/\/checkups\?visit=e2e-visit$/);
+ await expect(palette(page)).toHaveCount(0);
+ await expect(
+ page.locator('[data-testid="checkups-tab-visits"]'),
+ ).toHaveAttribute("aria-selected", "true");
+ });
+
+ test("the top bar: a magnifier on a wide desktop, with the shortcut in its tooltip", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto("/");
+ await shellReady(page);
+ const magnifier = page.locator('[data-slot="command-palette-trigger"]');
+ await expect(magnifier).toHaveCount(1);
+ await expect(magnifier).toBeVisible();
+ await expect(magnifier).toHaveAttribute("aria-label", "Search");
+ await expect(magnifier).toHaveAttribute(
+ "aria-keyshortcuts",
+ /^(Meta|Control)\+K$/,
+ );
+ // An icon, not a field: no visible words in the button.
+ expect((await magnifier.innerText()).trim()).toBe("");
+ await magnifier.hover();
+ await expect(
+ page.locator('[data-slot="command-palette-tooltip"]').first(),
+ ).toHaveText(/^Search \((⌘K|Ctrl K)\)$/);
+ await magnifier.click();
+ await expect(palette(page)).toBeVisible();
+ await expect(input(page)).toBeFocused();
+
+ const axe = await new AxeBuilder({ page })
+ .include('[data-slot="command-palette"]')
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
+ .analyze();
+ expect(axe.violations).toEqual([]);
+
+ await page.keyboard.press("Escape");
+ await expect(palette(page)).toHaveCount(0);
+ });
+
+ test("on a phone: the magnifier opens a full-height sheet", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto("/");
+ await shellReady(page);
+ const magnifier = page.locator('[data-slot="command-palette-trigger"]');
+ await expect(magnifier).toBeVisible();
+ await expect(magnifier).toHaveAttribute("aria-label", "Search");
+ await magnifier.click();
+ await expect(palette(page)).toHaveAttribute("data-variant", "sheet");
+ await expect(input(page)).toBeFocused();
+ const box = await palette(page).boundingBox();
+ expect(box?.height ?? 0).toBeGreaterThanOrEqual(844 - 1);
+ await input(page).fill("weight");
+ await expect(page.locator('[data-entry="insights:weight"]')).toBeVisible();
+ });
+
+ test("on a phone: a hit from the record fits the width", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await mockRecordSearch(page);
+ await page.goto("/");
+ await shellReady(page);
+ await page.locator('[data-slot="command-palette-trigger"]').click();
+ await input(page).fill("shoulder");
+ const hit = page.locator('[data-entry="record:visit:e2e-visit"]');
+ await expect(hit).toBeVisible();
+ const box = await hit.boundingBox();
+ expect((box?.x ?? 0) + (box?.width ?? 0)).toBeLessThanOrEqual(390);
+ const overflow = await page
+ .locator('[data-slot="command-palette-results"]')
+ .evaluate((el) => el.scrollWidth - el.clientWidth);
+ expect(overflow).toBeLessThanOrEqual(0);
+ await hit.click();
+ await expect(page).toHaveURL(/\/checkups\?visit=e2e-visit$/);
+ });
+});
diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts
index f76e12c9c..4b69093d9 100644
--- a/e2e/dashboard.spec.ts
+++ b/e2e/dashboard.spec.ts
@@ -115,6 +115,9 @@ test.describe("authenticated dashboard render", () => {
score: { value: 82, band: "green", delta: 2 },
topSignal: null,
briefingLead: "Your week is trending steady.",
+ lead: { text: "Your week is trending steady.", source: "briefing" },
+ today: [],
+ restMode: null,
line: "Your week is trending steady.",
worthALook: [
{
diff --git a/e2e/day-doors.spec.ts b/e2e/day-doors.spec.ts
new file mode 100644
index 000000000..c290471b5
--- /dev/null
+++ b/e2e/day-doors.spec.ts
@@ -0,0 +1,330 @@
+import type { Locator, Page, TestInfo } from "@playwright/test";
+
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
+import { DAY_DOORS_ANALYTE } from "./setup/day-doors-fixture";
+import { mockDay } from "./setup/day-mock";
+import {
+ DAY_DOORS_STORAGE_STATE_PATH,
+ MOBILE_ROUTES_STORAGE_STATE_PATH,
+} from "./setup/global-setup";
+import { expect, test } from "./setup/test";
+
+/**
+ * v1.42 — every chart drawn in days opens its days, the same way.
+ *
+ * The day view first reached six metric pages; the recovery page beside them
+ * offered no day at all. These journeys walk the surfaces that came after,
+ * each through the same doors: on a fine pointer a click on a point (or a
+ * calendar cell) opens the day beside the page; on a touch screen the first
+ * tap shows the value and the tooltip's "View the whole day" opens it as a
+ * bottom sheet, the tapped value on top. A date in a list opens in one tap.
+ * The dashed line (or outline) marks the open day, the row of dots and the
+ * caption sit under every chart, and the data table's dates are the
+ * keyboard way to the same days.
+ *
+ * - recovery (the page the beta reported), sleep, a lab marker, a chart
+ * with its comparison period on and a dashboard chart: line charts;
+ * - the mood calendar: a heatmap;
+ * - a metric's "all values" list: a date in a row.
+ *
+ * The day itself is a route mock in the contract's shape, as in
+ * `day-view.spec.ts`: these journeys pin the client's doors, not the day.
+ * Stable data attributes only.
+ */
+
+const panel = (page: Page) => page.locator('[data-slot="day-panel"]');
+const isPhone = (testInfo: TestInfo) =>
+ testInfo.project.name === "chromium-mobile";
+
+/** Open the day of the point at 80 % across a day-linked plot. */
+async function openFromPlot(page: Page, plot: Locator, testInfo: TestInfo) {
+ await plot.scrollIntoViewIfNeeded();
+ await expect(plot).toHaveAttribute("data-day-links", "true");
+ const box = (await plot.boundingBox())!;
+ const x = box.x + box.width * 0.8;
+ const y = box.y + box.height / 2;
+ if (isPhone(testInfo)) {
+ await page.touchscreen.tap(x, y);
+ const open = page.locator('[data-slot="chart-tooltip-open-day"]:visible');
+ await expect(open).toBeVisible();
+ // A tap alone never opens a sheet: a point is a small target.
+ await expect(panel(page)).toHaveCount(0);
+ // The button is a calm, full 44 px row.
+ expect((await open.boundingBox())!.height).toBeGreaterThanOrEqual(44);
+ await open.tap();
+ await expect(panel(page)).toHaveAttribute("data-shell", "bottom");
+ } else {
+ await page.mouse.move(x, y);
+ await page.mouse.click(x, y);
+ await expect(panel(page)).toHaveAttribute("data-shell", "docked");
+ }
+ await expect(page).toHaveURL(/[?&]day=\d{4}-\d{2}-\d{2}/);
+ await expect(page.locator('[data-slot="day-view"]')).toBeVisible();
+}
+
+test.describe("every chart drawn in days opens its days", () => {
+ test.use({ storageState: DAY_DOORS_STORAGE_STATE_PATH });
+
+ test.beforeEach(async ({ page, context }, testInfo) => {
+ const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
+ await context.addCookies([
+ { name: "healthlog-locale", value: "en", url: baseURL },
+ ]);
+ await mockDay(page, { index: "full" });
+ });
+
+ test("recovery: a point opens its day, and the open day is marked", async ({
+ page,
+ }, testInfo) => {
+ if (!isPhone(testInfo)) {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ }
+ await page.goto("/insights/recovery");
+ const plot = page
+ .locator('[data-slot="chart-plot"][data-day-links="true"]')
+ .first();
+ await expect(plot).toBeVisible();
+ // Under the chart: the row of day dots, which draws the
+ // days the index names, one of them ringed as notable.
+ const rug = page.locator('[data-slot="day-rug"]').first();
+ await expect(rug.locator("[data-day]").first()).toBeAttached();
+ await expect(rug.locator('[data-notable="true"]')).toHaveCount(1);
+
+ await openFromPlot(page, plot, testInfo);
+ const day = await page
+ .locator('[data-slot="day-view"]')
+ .getAttribute("data-day");
+ // The value the person came from heads the day.
+ await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
+ // The open day's dot is the filled one.
+ await expect(rug.locator(`[data-day="${day}"]`)).toHaveAttribute(
+ "data-open",
+ "true",
+ );
+ });
+
+ test("recovery: the data table's dates are the keyboard way to a day", async ({
+ page,
+ }, testInfo) => {
+ test.skip(isPhone(testInfo), "keyboard path");
+ await page.goto("/insights/recovery");
+ const toggle = page
+ .locator('[data-slot="chart-data-table-toggle"]')
+ .first();
+ await toggle.scrollIntoViewIfNeeded();
+ await toggle.focus();
+ await page.keyboard.press("Enter");
+ const link = page
+ .locator('[data-slot="chart-data-table-region"] [data-slot="day-link"]')
+ .first();
+ await link.focus();
+ const day = await link.getAttribute("data-day");
+ await page.keyboard.press("Enter");
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day!,
+ );
+ // Escape closes the day and gives the keyboard back to the date.
+ await page.keyboard.press("Escape");
+ await expect(panel(page)).toHaveCount(0);
+ });
+
+ test("sleep: a night's point opens its day", async ({ page }, testInfo) => {
+ await page.goto("/insights/sleep");
+ const plot = page
+ .locator('[data-slot="chart-plot"][data-day-links="true"]')
+ .first();
+ await expect(plot).toBeVisible();
+ await openFromPlot(page, plot, testInfo);
+ });
+
+ test("the mood calendar: a logged day's cell opens it", async ({
+ page,
+ }, testInfo) => {
+ await page.goto("/insights/mood");
+ // A logged, openable cell: it carries the pointer cursor.
+ const cell = page.locator("rect.cursor-pointer[data-day]").last();
+ await cell.scrollIntoViewIfNeeded();
+ await expect(cell).toBeVisible();
+ const day = await cell.getAttribute("data-day");
+ if (isPhone(testInfo)) {
+ await cell.tap();
+ const open = page.locator(
+ '[data-slot="heatmap-tooltip"] [data-slot="chart-tooltip-open-day"]',
+ );
+ await expect(open).toBeVisible();
+ await expect(panel(page)).toHaveCount(0);
+ await open.tap();
+ await expect(panel(page)).toHaveAttribute("data-shell", "bottom");
+ } else {
+ await cell.click();
+ }
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day!,
+ );
+ // The open day's cell is outlined.
+ await expect(page.locator('[data-slot="heatmap-open-day"]')).toHaveCount(1);
+ });
+
+ test("a lab marker's reading opens the day it was taken", async ({
+ page,
+ }, testInfo) => {
+ await page.goto("/labs");
+ await page
+ .locator('[data-slot="lab-list-analyte"]')
+ .filter({ hasText: DAY_DOORS_ANALYTE })
+ .first()
+ .click();
+ const plot = page
+ .locator('[data-slot="chart-plot"][data-day-links="true"]')
+ .first();
+ await expect(plot).toBeVisible();
+ await openFromPlot(page, plot, testInfo);
+ // The reading on top, with the one before it beside it.
+ await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
+ });
+
+ test("a chart with its comparison period on still opens its days", async ({
+ page,
+ }, testInfo) => {
+ await page.goto("/insights/weight");
+ const plot = page
+ .locator('[data-slot="chart-plot"][data-day-links="true"]')
+ .first();
+ await expect(plot).toBeVisible();
+ await page
+ .locator('[data-slot="chart-overlay-controls-trigger"]')
+ .first()
+ .click();
+ await page
+ .locator('[data-slot="chart-overlay-comparison-lastMonth"]')
+ .click();
+ await page.keyboard.press("Escape");
+ // The overlay menu is closed before the chart is touched, so the tap
+ // lands on the chart and not on the menu's outside-dismiss.
+ await expect(
+ page.locator('[data-slot="chart-overlay-controls-content"]'),
+ ).toHaveCount(0);
+ await expect(
+ page.locator('[data-slot="chart-compare-caption"]').first(),
+ ).toBeAttached();
+ await openFromPlot(page, plot, testInfo);
+ await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
+ });
+});
+
+test.describe("the dashboard and the list of all values", () => {
+ test.use({ storageState: DAY_DOORS_STORAGE_STATE_PATH });
+
+ test.beforeEach(async ({ page, context }, testInfo) => {
+ const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
+ await context.addCookies([
+ { name: "healthlog-locale", value: "en", url: baseURL },
+ ]);
+ await mockDay(page, { index: "full" });
+ });
+
+ test("a dashboard chart opens its day; the today hero offers none", async ({
+ page,
+ }, testInfo) => {
+ if (!isPhone(testInfo)) {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ }
+ await page.goto("/");
+ await expect(
+ page.locator(
+ '[data-slot^="today-hero"] [data-slot="day-link"], [data-slot^="today-hero"] [data-day-links="true"]',
+ ),
+ ).toHaveCount(0);
+ const plot = page
+ .locator('[data-slot="chart-plot"][data-day-links="true"]')
+ .first();
+ await expect(plot).toBeVisible({ timeout: 20_000 });
+ await openFromPlot(page, plot, testInfo);
+ // Closing leaves the dashboard on today: a sheet from its close button,
+ // the docked day from its strip, the only control it has.
+ await page
+ .locator(
+ isPhone(testInfo)
+ ? '[data-slot="day-close"]'
+ : '[data-slot="day-strip-toggle"]',
+ )
+ .click();
+ await expect(panel(page)).toHaveCount(0);
+ await expect(page).toHaveURL(/\/$/);
+ });
+
+ test("a date in a metric's list of all values opens its day", async ({
+ page,
+ }, testInfo) => {
+ await page.goto("/insights/values/WEIGHT");
+ const link = page.locator('[data-slot="day-link"]:visible').first();
+ await expect(link).toBeVisible();
+ const day = await link.getAttribute("data-day");
+ if (isPhone(testInfo)) await link.tap();
+ else await link.click();
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day!,
+ );
+ });
+
+ test("the docked day, open or collapsed, moves the Coach button off it", async ({
+ page,
+ }, testInfo) => {
+ test.skip(isPhone(testInfo), "docked from 1280 px");
+ await page.setViewportSize({ width: 1440, height: 900 });
+ // The launcher follows the `coach` capability, which needs a provider
+ // the suite cannot reach, so the capability reads available here; this
+ // journey is about where the button sits, not what it opens.
+ await serveAiBlock(page, aiBlockAvailable());
+ await page.goto("/insights/weight");
+ const fab = page.locator('[data-slot="coach-fab"]');
+ await expect(fab).toBeVisible();
+ const plot = page
+ .locator('[data-slot="chart-plot"][data-day-links="true"]')
+ .first();
+ await openFromPlot(page, plot, testInfo);
+ const panelBox = (await panel(page).boundingBox())!;
+ // Wait out the 150 ms move, then the launcher sits left of the column.
+ await expect
+ .poll(async () => {
+ const b = (await fab.boundingBox())!;
+ return b.x + b.width;
+ })
+ .toBeLessThanOrEqual(panelBox.x);
+
+ // Folded, the day keeps its strip; the launcher stays clear of it.
+ await page.locator('[data-slot="day-strip-toggle"]').click();
+ await expect(panel(page)).toHaveCount(0);
+ const edge = (await page.locator('[data-slot="day-strip"]').boundingBox())!;
+ await expect
+ .poll(async () => {
+ const b = (await fab.boundingBox())!;
+ return b.x + b.width;
+ })
+ .toBeLessThanOrEqual(edge.x);
+ });
+});
+
+test.describe("the readiness link into a medication's edit form", () => {
+ test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
+
+ test("?edit=1 opens the medication's edit form and leaves the URL", async ({
+ page,
+ }) => {
+ const res = await page.request.get("/api/medications");
+ const body = (await res.json()) as {
+ data: Array<{ id: string }> | { medications?: Array<{ id: string }> };
+ };
+ const list = Array.isArray(body.data)
+ ? body.data
+ : (body.data.medications ?? []);
+ expect(list.length).toBeGreaterThan(0);
+ await page.goto(`/medications/${list[0]!.id}?edit=1`);
+ await expect(page.getByRole("dialog")).toBeVisible();
+ // A reload after closing the form must not open it again.
+ await expect(page).not.toHaveURL(/[?&]edit=1/);
+ });
+});
diff --git a/e2e/day-view.spec.ts b/e2e/day-view.spec.ts
new file mode 100644
index 000000000..0aae88187
--- /dev/null
+++ b/e2e/day-view.spec.ts
@@ -0,0 +1,466 @@
+import type { Page, TestInfo } from "@playwright/test";
+
+import { mockDay } from "./setup/day-mock";
+import { expect, test } from "./setup/test";
+import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
+import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture";
+
+/**
+ * v1.42 — the day view and its doors.
+ *
+ * The record is the phone-sweep account `globalSetup` seeds: a week of blood
+ * pressure, two lab readings, mood and an episode. The day itself
+ * (`GET /api/day/{date}`, `GET /api/day/index`) is answered by a route mock in
+ * the contract's shape, so the journeys pin the client: which door opens which
+ * day, what the history does, and where no door is.
+ *
+ * - a chart point opens the day beside the chart (desktop);
+ * - on a touch screen the first tap shows the value and the tooltip offers
+ * the day, the second opens it as a bottom sheet;
+ * - a date in a list opens the day while the row keeps its own target;
+ * - `?day=` opens the layer on any page, Back and Escape close it, a date in
+ * the future is dropped;
+ * - stepping to the neighbouring days never adds history entries;
+ * - docked, the day folds to its strip, which runs the full height of the
+ * window right of the top bar, is the panel's only open and close
+ * control and brings the day back on any page; the search magnifier at
+ * the end of the top bar is never covered by it; a sheet closes for good
+ * from its close button;
+ * - the dashboard's today area offers no door; its charts below do.
+ *
+ * Stable data attributes only: `day-panel` (+ `data-shell`), `day-link`,
+ * the header's `day-date-button` and its `day-date-picker` (days keyed by
+ * `data-date-key`, dotted by `data-entries`, and `day-date-today`),
+ * `chart-plot[data-day-links]`, `chart-tooltip-open-day`, `day-prev` /
+ * `day-next` / `day-close`, and the docked day's strip `day-strip`
+ * (+ `data-day`, `data-state`) with its `day-strip-toggle`.
+ */
+
+const panel = (page: Page) => page.locator('[data-slot="day-panel"]');
+
+async function shot(page: Page, testInfo: TestInfo, name: string) {
+ const path = testInfo.outputPath(`${name}.png`);
+ await page.screenshot({ path });
+ await testInfo.attach(name, { path, contentType: "image/png" });
+ const dir = process.env.DAY_SHOTS_DIR;
+ if (dir) {
+ await page.screenshot({
+ path: `${dir}/${name}-${testInfo.project.name}.png`,
+ });
+ }
+}
+
+function isoDaysAgo(days: number): string {
+ const d = new Date(Date.now() - days * 86_400_000);
+ return d.toISOString().slice(0, 10);
+}
+
+test.describe("the day view", () => {
+ test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
+
+ test.beforeEach(async ({ page, context }, testInfo) => {
+ const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
+ await context.addCookies([
+ { name: "healthlog-locale", value: "en", url: baseURL },
+ ]);
+ await mockDay(page);
+ });
+
+ test("a chart point opens its day beside the chart; Back closes it", async ({
+ page,
+ }, testInfo) => {
+ test.skip(testInfo.project.name !== "chromium-desktop", "desktop click");
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto("/insights/blood-pressure");
+ const plot = page.locator(
+ '[data-slot="chart-plot"][data-day-links="true"]',
+ );
+ await expect(plot).toBeVisible();
+ // The plot's right half holds the most recent points.
+ const box = (await plot.boundingBox())!;
+ await page.mouse.move(box.x + box.width * 0.8, box.y + box.height / 2);
+ await page.mouse.click(box.x + box.width * 0.8, box.y + box.height / 2);
+
+ await expect(panel(page)).toBeVisible();
+ await expect(panel(page)).toHaveAttribute("data-shell", "docked");
+ // A landmark beside the page, named by its date.
+ await expect(panel(page)).toHaveJSProperty("tagName", "ASIDE");
+ await expect(page).toHaveURL(/[?&]day=\d{4}-\d{2}-\d{2}/);
+ await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
+ // The chart beside it stays usable: the panel is not modal.
+ await expect(page.locator('[data-slot="sheet-overlay"]')).toHaveCount(0);
+
+ await page.goBack();
+ await expect(panel(page)).toHaveCount(0);
+ await expect(page).not.toHaveURL(/[?&]day=/);
+ });
+
+ test("a date in a list opens the day; the row keeps its own target", async ({
+ page,
+ }) => {
+ await page.goto("/labs");
+ await page
+ .locator('[data-slot="lab-list-analyte"]')
+ .filter({ hasText: MOBILE_ROUTES_ANALYTE })
+ .first()
+ .click();
+ // The marker's readings, each dated.
+ await page.locator('a[href$="/values"]').first().click();
+ const link = page.locator('[data-slot="day-link"]:visible').first();
+ await expect(link).toBeVisible();
+ const day = await link.getAttribute("data-day");
+ const before = page.url();
+ await link.click();
+ await expect(panel(page)).toBeVisible();
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day!,
+ );
+ // Same page underneath: the click opened the day, not the reading.
+ expect(new URL(page.url()).pathname).toBe(new URL(before).pathname);
+ // The reading the person came from sits on top, with the one before it.
+ await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
+ });
+
+ test("?day= opens the layer on any page; Escape closes it", async ({
+ page,
+ }) => {
+ const day = isoDaysAgo(3);
+ await page.goto(`/mood?day=${day}`);
+ await expect(panel(page)).toBeVisible();
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day,
+ );
+ await page.keyboard.press("Escape");
+ await expect(panel(page)).toHaveCount(0);
+ await expect(page).toHaveURL(/\/mood$/);
+ });
+
+ test("the day's scores sit below its values, each opening its own page", async ({
+ page,
+ }) => {
+ await page.goto(`/mood?day=${isoDaysAgo(3)}`);
+ await expect(panel(page)).toBeVisible();
+ const scores = page.locator('[data-slot="day-scores"]');
+ await expect(scores).toBeVisible();
+ const tiles = scores.locator('[data-slot="day-score"]');
+ await expect(tiles).toHaveCount(2);
+ await expect(
+ scores.locator('[data-slot="day-score"][data-score="healthScore"]'),
+ ).toHaveAttribute("href", "/insights/health-score");
+ await expect(
+ scores.locator('[data-slot="day-score"][data-score="strain"]'),
+ ).toHaveAttribute("href", "/insights/scores/strain");
+ // A score the day does not hold has no tile.
+ await expect(
+ scores.locator('[data-slot="day-score"][data-score="readiness"]'),
+ ).toHaveCount(0);
+ // Below the values, above what happened.
+ const order = await page
+ .locator(
+ '[data-slot="day-values"], [data-slot="day-scores"], [data-slot="day-events"]',
+ )
+ .evaluateAll((nodes) =>
+ nodes.map((node) => node.getAttribute("data-slot")),
+ );
+ expect(order).toEqual(["day-values", "day-scores", "day-events"]);
+ });
+
+ test("the docked day folds beside its strip and comes back on another page", async ({
+ page,
+ }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "docked from 1280 px",
+ );
+ const day = isoDaysAgo(3);
+ await page.goto(`/mood?day=${day}`);
+ await expect(panel(page)).toHaveAttribute("data-shell", "docked");
+
+ // Docked, the header has no fold button: the strip is the toggle.
+ await expect(page.locator('[data-slot="day-close"]')).toHaveCount(0);
+ const rail = page.locator('[data-slot="day-strip"]');
+ const expand = page.locator('[data-slot="day-strip-toggle"]');
+ await expect(expand).toHaveAttribute("aria-expanded", "true");
+ await expand.click();
+ await expect(panel(page)).toHaveCount(0);
+ await expect(page).toHaveURL(/\/mood$/);
+ // The strip stays, holding the day, and focus waits on it.
+ await expect(rail).toHaveAttribute("data-day", day);
+ await expect(rail).toHaveAttribute("data-state", "closed");
+ await expect(expand).toBeFocused();
+ await expect(expand).toHaveAttribute("aria-expanded", "false");
+ await expect(expand).toHaveAttribute("aria-controls", "day-docked-panel");
+ expect(await expand.getAttribute("aria-label")).toMatch(/^Show /);
+ expect((await rail.boundingBox())!.width).toBeLessThanOrEqual(40);
+
+ // Another page: the edge is still there and brings the same day back,
+ // from the keyboard too.
+ await page.goto("/labs");
+ await expect(rail).toHaveAttribute("data-day", day);
+ await expand.focus();
+ await page.keyboard.press("Enter");
+ await expect(panel(page)).toHaveAttribute("data-shell", "docked");
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day,
+ );
+ await expect(page).toHaveURL(new RegExp(`/labs\\?day=${day}$`));
+ await expect(panel(page).locator("h2").first()).toBeFocused();
+ // Open, the strip stays where it was and says so.
+ await expect(rail).toHaveAttribute("data-state", "open");
+ await expect(expand).toHaveAttribute("aria-expanded", "true");
+ });
+
+ test("the date opens a calendar; a picked day opens, Today goes to today", async ({
+ page,
+ }, testInfo) => {
+ const desktop = testInfo.project.name === "chromium-desktop";
+ // The days of the month that hold anything carry a dot.
+ await mockDay(page, { index: "full" });
+ const day = isoDaysAgo(5);
+ await page.goto(`/mood?day=${day}`);
+ await expect(panel(page)).toHaveAttribute(
+ "data-shell",
+ desktop ? "docked" : "bottom",
+ );
+ const date = panel(page).locator('[data-slot="day-date-button"]');
+ const picker = page.locator('[data-slot="day-date-picker"]');
+ // The date stays one line in the header, the arrows beside it.
+ await expect(date).toHaveAttribute("aria-expanded", "false");
+ await expect(panel(page).locator('[data-slot="day-prev"]')).toBeVisible();
+ await shot(page, testInfo, "day-date-closed");
+
+ await date.click();
+ await expect(picker).toBeVisible();
+ await expect(date).toHaveAttribute("aria-expanded", "true");
+ await expect(picker.locator(`[data-date-key="${day}"]`)).toHaveAttribute(
+ "data-selected-single",
+ "true",
+ );
+ await expect(picker.locator('[data-entries="true"]').first()).toBeVisible();
+ // Nothing past today can be picked.
+ const tomorrow = picker.locator(`[data-date-key="${isoDaysAgo(-2)}"]`);
+ if ((await tomorrow.count()) > 0) await expect(tomorrow).toBeDisabled();
+ await shot(page, testInfo, "day-date-open");
+
+ // Another day of the same month: the 1st, or the 2nd when the open day
+ // is the 1st itself.
+ const first = `${day.slice(0, 8)}01`;
+ const target = first === day ? isoDaysAgo(4) : first;
+ const start = await page.evaluate(() => window.history.length);
+ await picker.locator(`[data-date-key="${target}"]`).click();
+ await expect(picker).toHaveCount(0);
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ target,
+ );
+ await expect(page).toHaveURL(new RegExp(`[?&]day=${target}`));
+ // Like a step: no new history entry, and focus back on the date.
+ expect(await page.evaluate(() => window.history.length)).toBe(start);
+ await expect(date).toBeFocused();
+
+ // The keyboard: Enter opens, Escape closes the calendar only.
+ await page.keyboard.press("Enter");
+ await expect(picker).toBeVisible();
+ // The calendar has the keyboard before Escape is pressed.
+ await expect
+ .poll(() =>
+ page.evaluate(
+ () =>
+ document.activeElement?.closest(
+ '[role="dialog"]:not([data-slot="day-panel"])',
+ ) != null,
+ ),
+ )
+ .toBe(true);
+ // On a phone the calendar is a sheet over the day's own sheet. Focus
+ // arrives before Radix has handed it the Escape key: in between, the day
+ // underneath still holds it. The handover shows as the day's layer
+ // turning inert (`pointer-events: none`) once the calendar is the top
+ // layer; wait for that, not for a fixed time. The docked column is no
+ // dialog and has no such handover.
+ if (!desktop)
+ await expect
+ .poll(() =>
+ panel(page).evaluate(
+ (el) =>
+ new Promise((resolve) =>
+ requestAnimationFrame(() =>
+ setTimeout(() => resolve(getComputedStyle(el).pointerEvents)),
+ ),
+ ),
+ ),
+ )
+ .toBe("none");
+ await page.keyboard.press("Escape");
+ await expect(picker).toHaveCount(0);
+ await expect(panel(page)).toBeVisible();
+ await expect(date).toBeFocused();
+
+ // Today.
+ await page.keyboard.press("Space");
+ await expect(picker).toBeVisible();
+ await picker.locator('[data-slot="day-date-today"]').click();
+ await expect(picker).toHaveCount(0);
+ await expect(panel(page).locator('[data-slot="day-next"]')).toBeDisabled();
+ });
+
+ test("a sheet closes for good and leaves no strip behind", async ({
+ page,
+ }, testInfo) => {
+ const desktop = testInfo.project.name === "chromium-desktop";
+ // Below 1280 px the day is a sheet from the right; on a phone, from the
+ // bottom. Neither collapses.
+ if (desktop) await page.setViewportSize({ width: 1024, height: 800 });
+ const day = isoDaysAgo(3);
+ await page.goto(`/mood?day=${day}`);
+ await expect(panel(page)).toHaveAttribute(
+ "data-shell",
+ desktop ? "sheet" : "bottom",
+ );
+ await expect(page.locator('[data-slot="day-close"]')).toHaveAttribute(
+ "aria-label",
+ "Close day",
+ );
+ await page.locator('[data-slot="day-close"]').click();
+ await expect(panel(page)).toHaveCount(0);
+ await expect(page).toHaveURL(/\/mood$/);
+ await expect(page.locator('[data-slot="day-strip"]')).toHaveCount(0);
+ });
+
+ test("the strip runs the full height; the search magnifier stays clear of it", async ({
+ page,
+ }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "docked from 1280 px",
+ );
+ const day = isoDaysAgo(3);
+ const strip = page.locator('[data-slot="day-strip"]');
+ const search = page.locator('[data-slot="command-palette-trigger"]');
+ const topBar = page.locator('[data-slot="top-bar"]');
+ for (const width of [1280, 1440, 1920]) {
+ await page.setViewportSize({ width, height: 900 });
+ for (const url of ["/mood", `/mood?day=${day}`]) {
+ await page.goto(url);
+ await expect(strip).toBeVisible();
+ if (url.includes("day=")) {
+ await expect(panel(page)).toHaveAttribute("data-shell", "docked");
+ // The open panel runs the full height beside its strip.
+ await expect
+ .poll(async () => (await panel(page).boundingBox())?.width)
+ .toBeGreaterThan(400);
+ const p = (await panel(page).boundingBox())!;
+ expect(p.y).toBe(0);
+ expect(Math.round(p.y + p.height)).toBe(900);
+ }
+ const s = (await strip.boundingBox())!;
+ // Top edge at the very top of the window, down to the bottom.
+ expect(s.y).toBe(0);
+ expect(Math.round(s.height)).toBe(900);
+ expect(Math.round(s.x + s.width)).toBe(width);
+ // The top bar ends left of the strips, so the magnifier sits fully
+ // inside it, in front of them.
+ const bar = (await topBar.boundingBox())!;
+ expect(bar.x + bar.width).toBeLessThanOrEqual(s.x + 0.5);
+ await expect(search).toBeVisible();
+ const m = (await search.boundingBox())!;
+ expect(m.x).toBeGreaterThanOrEqual(0);
+ expect(m.y).toBeGreaterThanOrEqual(0);
+ expect(m.x + m.width).toBeLessThanOrEqual(s.x);
+ expect(m.y + m.height).toBeLessThanOrEqual(bar.y + bar.height);
+ // Nothing paints over its centre: the hit test lands on it.
+ const hit = await page.evaluate(
+ ([x, y]) =>
+ document
+ .elementFromPoint(x, y)
+ ?.closest('[data-slot="command-palette-trigger"]') != null,
+ [m.x + m.width / 2, m.y + m.height / 2],
+ );
+ expect(hit).toBe(true);
+ // No horizontal scroll anywhere on the page.
+ const overflow = await page.evaluate(
+ () =>
+ document.documentElement.scrollWidth -
+ document.documentElement.clientWidth,
+ );
+ expect(overflow).toBeLessThanOrEqual(0);
+ }
+ }
+ });
+
+ test("a future or malformed ?day= is dropped without a word", async ({
+ page,
+ }) => {
+ await page.goto("/mood?day=2999-01-01");
+ await expect(page).toHaveURL(/\/mood$/);
+ await expect(panel(page)).toHaveCount(0);
+ await page.goto("/mood?day=2026-02-30");
+ await expect(page).toHaveURL(/\/mood$/);
+ await expect(panel(page)).toHaveCount(0);
+ });
+
+ test("stepping between days never adds a history entry", async ({ page }) => {
+ await page.goto("/mood");
+ await page.locator('[data-slot="day-link"]:visible').first().click();
+ await expect(panel(page)).toBeVisible();
+ const start = await page.evaluate(() => window.history.length);
+ const first = await page
+ .locator('[data-slot="day-view"]')
+ .getAttribute("data-day");
+ for (let i = 0; i < 3; i += 1) {
+ await page.locator('[data-slot="day-prev"]').click();
+ }
+ await expect(page.locator('[data-slot="day-view"]')).not.toHaveAttribute(
+ "data-day",
+ first!,
+ );
+ expect(await page.evaluate(() => window.history.length)).toBe(start);
+ // One Back leaves the layer, however far it stepped.
+ await page.goBack();
+ await expect(panel(page)).toHaveCount(0);
+ await expect(page).toHaveURL(/\/mood$/);
+ });
+
+ test("on a phone the first tap shows the value, the second opens the day", async ({
+ page,
+ }, testInfo) => {
+ test.skip(testInfo.project.name !== "chromium-mobile", "touch only");
+ await page.goto("/insights/blood-pressure");
+ const plot = page.locator(
+ '[data-slot="chart-plot"][data-day-links="true"]',
+ );
+ await plot.scrollIntoViewIfNeeded();
+ await expect(plot).toBeVisible();
+ const box = (await plot.boundingBox())!;
+ await page.touchscreen.tap(box.x + box.width * 0.8, box.y + box.height / 2);
+ const open = page.locator('[data-slot="chart-tooltip-open-day"]');
+ await expect(open).toBeVisible();
+ // A tap alone never opens a sheet.
+ await expect(panel(page)).toHaveCount(0);
+ await open.tap();
+ await expect(panel(page)).toBeVisible();
+ await expect(panel(page)).toHaveAttribute("data-shell", "bottom");
+ // The tapped value heads the day.
+ await expect(page.locator('[data-slot="day-focus"]')).toBeVisible();
+ });
+
+ test("the dashboard's today area offers no day door", async ({ page }) => {
+ await page.goto("/");
+ await expect(
+ page.locator('[data-slot="main-content-wrapper"]'),
+ ).toBeVisible();
+ // The hero is today: nothing in it opens another day. (The charts
+ // further down do; `day-doors.spec.ts` walks one.)
+ await expect(
+ page.locator('[data-slot^="today-hero"]').first(),
+ ).toBeVisible();
+ await expect(
+ page.locator(
+ '[data-slot^="today-hero"] [data-slot="day-link"], [data-slot^="today-hero"] [data-day-links="true"]',
+ ),
+ ).toHaveCount(0);
+ });
+});
diff --git a/e2e/delegated-writes.spec.ts b/e2e/delegated-writes.spec.ts
index 68150fffc..5c7cfa565 100644
--- a/e2e/delegated-writes.spec.ts
+++ b/e2e/delegated-writes.spec.ts
@@ -221,7 +221,14 @@ test.describe("delegated writes", () => {
// reads "Add") and then for a `value` input (the form opens on blood
// pressure, which has three). Both were wrong from the day they were
// written and nobody found out, because the whole file was skipping.
- await page.locator('[data-slot="measurement-add"]').click();
+ // The owner's list may be empty, in which case its empty state carries
+ // the add action and the header drops its own.
+ await page
+ .locator(
+ '[data-slot="measurement-add"], [data-slot="measurement-add-first"]',
+ )
+ .first()
+ .click();
await page.locator("#sys").fill("124");
await page.locator("#dia").fill("78");
await page.getByRole("button", { name: /^save$/i }).click();
diff --git a/e2e/dialog-horizontal-overflow.spec.ts b/e2e/dialog-horizontal-overflow.spec.ts
index 54267f858..95302437e 100644
--- a/e2e/dialog-horizontal-overflow.spec.ts
+++ b/e2e/dialog-horizontal-overflow.spec.ts
@@ -70,13 +70,15 @@ const DIALOGS: DialogCase[] = [
{
name: "measurements add",
path: "/measurements",
- trigger: /^add$/i,
+ // While a list is empty the header drops its add button and the empty
+ // state carries the action, so either one opens the dialog.
+ trigger: /^(add|add your first measurement)$/i,
ready: '[data-slot="date-time-field"]',
},
{
name: "mood add",
path: "/mood",
- trigger: /^add$/i,
+ trigger: /^(add|log your first mood)$/i,
// The mood sheet opens on the five-face quick check; the timestamp row
// (and everything else) appears once a face is picked. Measuring the
// quick check alone would measure a sheet that has almost nothing in it.
@@ -92,7 +94,7 @@ const DIALOGS: DialogCase[] = [
{
name: "labs add",
path: "/labs",
- trigger: /^add$/i,
+ trigger: /^(add|add your first result)$/i,
ready: '[data-slot="date-time-field"]',
},
// The labs sheet's footer holds three buttons (Cancel, Save & add another,
@@ -101,7 +103,7 @@ const DIALOGS: DialogCase[] = [
{
name: "labs add (German)",
path: "/labs",
- trigger: /^hinzufügen$/i,
+ trigger: /^(hinzufügen|ersten wert erfassen)$/i,
ready: '[data-slot="date-time-field"]',
locale: "de",
},
diff --git a/e2e/documents-ai.spec.ts b/e2e/documents-ai.spec.ts
index 9f2fad569..216db06ec 100644
--- a/e2e/documents-ai.spec.ts
+++ b/e2e/documents-ai.spec.ts
@@ -427,7 +427,7 @@ test.describe("document vault — AI assist + content search", () => {
// The PDF is refused locally (tesseract reads images only) — the error
// shows and the server suggest route is never called.
await expect(
- sheet.getByText("Local OCR reads images only — not this file."),
+ sheet.getByText("Local OCR reads images only, not this file."),
).toBeVisible();
await page.waitForTimeout(300);
expect(suggestCalls).toBe(0);
diff --git a/e2e/documents-procedure-filter.spec.ts b/e2e/documents-procedure-filter.spec.ts
new file mode 100644
index 000000000..28f6aa563
--- /dev/null
+++ b/e2e/documents-procedure-filter.spec.ts
@@ -0,0 +1,131 @@
+/**
+ * Document vault — the procedure filter.
+ *
+ * The facet beside the condition filter: it lists the procedures that hold a
+ * document, labelled the way the visits page heads them, narrows the list to
+ * one procedure's documents through the real `encounterId` filter, writes the
+ * choice to the URL (`?encounter=`), comes back from a deep link, and clears
+ * with the one clear control. Run at a 1440 px desktop and a 390 px phone,
+ * where the filter row must stay a single line with no sideways scroll.
+ *
+ * Fixture: `ensureProcedureFixture` (two procedures over a namespaced trio of
+ * documents), upserted before every test because the visits spec clears the
+ * account's visits; the dates sit far outside that spec's ±7-day window.
+ */
+import type { Page } from "@playwright/test";
+import { expect, test } from "./setup/test";
+
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import {
+ ensureProcedureFixture,
+ ensureVaultFixture,
+ PROCEDURE_DOC_PREFIX,
+ PROCEDURE_KNEE_ID,
+} from "./setup/vault-fixture";
+
+function card(page: Page, n: number) {
+ return page
+ .getByRole("button", {
+ name: `Open ${PROCEDURE_DOC_PREFIX} ${String(n).padStart(3, "0")}`,
+ })
+ .first();
+}
+
+/** The filter row is one line and never scrolls sideways. */
+async function expectOneRow(page: Page) {
+ const bar = page.locator('[data-slot="document-filter-bar"]');
+ const row = bar.locator("> div").first();
+ const box = await row.boundingBox();
+ expect(box).not.toBeNull();
+ expect(box!.height).toBeLessThan(48);
+ const overflow = await page.evaluate(
+ () => document.documentElement.scrollWidth - window.innerWidth,
+ );
+ expect(overflow).toBeLessThanOrEqual(0);
+}
+
+async function shot(page: Page, name: string, isMobile: boolean) {
+ const dir = process.env.E2E_SHOT_DIR;
+ if (!dir) return;
+ await page.screenshot({
+ path: `${dir}/${name}-${isMobile ? "390" : "1440"}.png`,
+ animations: "disabled",
+ });
+}
+
+test.describe("document vault — procedure filter", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ test.beforeAll(async () => {
+ await ensureVaultFixture();
+ });
+
+ test.beforeEach(async ({ page, isMobile }) => {
+ await ensureProcedureFixture();
+ await page.setViewportSize(
+ isMobile ? { width: 390, height: 844 } : { width: 1440, height: 900 },
+ );
+ });
+
+ test("picks a procedure, narrows to its documents, and clears", async ({
+ page,
+ isMobile,
+ }) => {
+ await page.goto(`/documents?q=${PROCEDURE_DOC_PREFIX}`);
+ await expect(card(page, 3)).toBeVisible();
+ await expect(card(page, 1)).toBeVisible();
+
+ const trigger = page.locator('[data-slot="document-procedure-filter"]');
+ await expect(trigger).toBeVisible();
+ await expect(trigger).toHaveAttribute("aria-label", "All procedures");
+ await expectOneRow(page);
+ await shot(page, "procedure-filter-idle", isMobile);
+
+ await trigger.click();
+ const menu = page.getByRole("menu");
+ await expect(menu).toBeVisible();
+ await expect(menu).toContainText("Procedures and surgeries");
+ // Headed like the visits page, with the body site and side beneath.
+ const knee = menu.getByRole("menuitemcheckbox", {
+ name: /Knee arthroscopy/,
+ });
+ await expect(knee).toContainText("Knee (Left)");
+ await expect(
+ menu.getByRole("menuitemcheckbox", { name: /Appendectomy/ }),
+ ).toBeVisible();
+ await shot(page, "procedure-filter-menu", isMobile);
+ await knee.click();
+
+ await expect(page).toHaveURL(new RegExp(`encounter=${PROCEDURE_KNEE_ID}`));
+ await expect(card(page, 1)).toBeVisible();
+ await expect(card(page, 2)).toBeVisible();
+ await expect(card(page, 3)).toHaveCount(0);
+ await expect(trigger).toHaveAttribute("aria-label", "Knee arthroscopy");
+ // Picked: the label shows at every width, phone included.
+ await expect(trigger).toContainText("Knee arthroscopy");
+ await expectOneRow(page);
+ // The search field never collapses under the facets.
+ const search = await page
+ .getByRole("searchbox", { name: "Search documents" })
+ .boundingBox();
+ expect(search!.width).toBeGreaterThanOrEqual(48);
+ await shot(page, "procedure-filter-active", isMobile);
+
+ await page.getByRole("button", { name: "Clear filters" }).click();
+ await expect(page).not.toHaveURL(/encounter=/);
+ await expect(trigger).toHaveAttribute("aria-label", "All procedures");
+ });
+
+ test("a deep link lands filtered and the facet names the procedure", async ({
+ page,
+ }) => {
+ await page.goto(
+ `/documents?q=${PROCEDURE_DOC_PREFIX}&encounter=${PROCEDURE_KNEE_ID}`,
+ );
+ await expect(card(page, 2)).toBeVisible();
+ await expect(card(page, 3)).toHaveCount(0);
+ await expect(
+ page.locator('[data-slot="document-procedure-filter"]'),
+ ).toHaveAttribute("aria-label", "Knee arthroscopy");
+ });
+});
diff --git a/e2e/documents.spec.ts b/e2e/documents.spec.ts
index ac82ceebe..a186b6996 100644
--- a/e2e/documents.spec.ts
+++ b/e2e/documents.spec.ts
@@ -151,6 +151,61 @@ test.describe("document vault", () => {
await ensureVaultFixture();
});
+ // ── v1.42 — the archive as a month grid of preview tiles ──────────────
+
+ for (const [width, columns] of [
+ [390, 2],
+ [1440, 4],
+ ] as const) {
+ test(`the archive is a month grid of preview tiles, ${columns} across at ${width} px`, async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width, height: width === 390 ? 844 : 900 });
+ await page.goto("/documents");
+ const firstRow = page.locator('[data-slot="document-month-row"]').first();
+ await expect(firstRow).toBeVisible({ timeout: 15_000 });
+
+ // The months run side by side by default: a month is marked above
+ // its first document instead of taking a heading row of its own.
+ await expect(
+ page.locator('[data-slot="document-timeline"]'),
+ ).toHaveAttribute("data-arrangement", "flow");
+ await expect(
+ page.locator('[data-slot="document-flow-month"]').first(),
+ ).toBeVisible();
+
+ // Tiles with the preview on top, and at most `columns` side by side:
+ // the fullest mounted row holds exactly that many, on one line.
+ const tiles = page.locator(
+ '[data-slot="document-month-row"] [data-slot="document-card"]',
+ );
+ await expect(tiles.first()).toHaveAttribute("data-variant", "tile");
+ await expect(
+ tiles.first().locator('[data-slot="document-preview"]'),
+ ).toBeVisible();
+ const rows = await page
+ .locator('[data-slot="document-month-row"]')
+ .evaluateAll((els) =>
+ els.map((row) =>
+ Array.from(row.querySelectorAll('[data-slot="document-card"]')).map(
+ (el) => Math.round(el.getBoundingClientRect().top),
+ ),
+ ),
+ );
+ const fullest = rows.reduce((a, b) => (b.length > a.length ? b : a), []);
+ expect(fullest.length).toBe(columns);
+ expect(new Set(fullest).size).toBe(1);
+
+ // Nothing on the page scrolls sideways.
+ const overflow = await page.evaluate(
+ () =>
+ document.documentElement.scrollWidth -
+ document.documentElement.clientWidth,
+ );
+ expect(overflow).toBeLessThanOrEqual(0);
+ });
+ }
+
// ── §1A — the 30-second doctor flow, three independent routes ─────────
test("doctor flow A: the type filter reaches the MRT report", async ({
@@ -177,6 +232,45 @@ test.describe("document vault", () => {
await expect(sheet.locator("iframe")).toBeVisible({ timeout: 5_000 });
});
+ test("a PDF opens in a new browser tab from the fetched bytes", async ({
+ page,
+ }) => {
+ // Headless Chromium has no PDF viewer and turns a PDF tab into a
+ // download, so the tab itself is not observable here. Record what the
+ // control hands to `window.open` instead, and read the bytes behind it.
+ await page.addInitScript(() => {
+ const calls: unknown[][] = [];
+ (window as unknown as { __opened: unknown[][] }).__opened = calls;
+ window.open = (...args: unknown[]) => {
+ calls.push(args);
+ return null;
+ };
+ });
+ await page.goto(`/documents?episode=${KNIE_EPISODE_ID}&view=compact&q=MRT`);
+ await openButton(page, "MRT Knie").click();
+ const sheet = page.getByRole("dialog", { name: "MRT Knie" });
+ await expect(sheet.locator("iframe")).toBeVisible({ timeout: 5_000 });
+ const open = sheet.locator('[data-slot="document-open-in-tab"]');
+ // Enabled once the preview's bytes are in hand.
+ await expect(open).toBeEnabled();
+ if (process.env.E2E_SHOT_DIR) {
+ await page.screenshot({
+ path: `${process.env.E2E_SHOT_DIR}/open-in-tab-${test.info().project.name}.png`,
+ });
+ }
+ await open.click();
+ const opened = await page.evaluate(
+ () => (window as unknown as { __opened: string[][] }).__opened,
+ );
+ // The object URL the preview already shows, not the serve route: a bare
+ // navigation would carry no record-session assertion.
+ const previewSrc = await sheet.locator("iframe").getAttribute("src");
+ expect(previewSrc).toMatch(/^blob:/);
+ expect(opened).toEqual([[previewSrc, "_blank", "noopener"]]);
+ // The sheet stays where it was.
+ await expect(sheet).toBeVisible();
+ });
+
test("doctor flow B: the condition filter reaches the MRT report", async ({
page,
}) => {
@@ -413,6 +507,8 @@ test.describe("document vault", () => {
const sheet = page.getByRole("dialog", { name: `${title}.pdf` });
await expect(sheet).toBeVisible();
await sheet.getByRole("button", { name: "Delete" }).click();
+ // v1.42 — the sheet's Delete asks first, like every vault delete.
+ await page.locator('[data-slot="document-detail-delete-confirm"]').click();
// Undo toast → restore → the card returns.
//
@@ -714,7 +810,7 @@ test.describe("document vault", () => {
await uploadViaPicker(page, `${marker}-copy.pdf`, bytes);
await expect(
- page.getByText("Already stored — highlighting the existing document."),
+ page.getByText("Already stored. The existing document is highlighted."),
).toBeVisible();
// One row, not two — the copy never landed.
await expect(openButton(page, `${marker}-copy.pdf`)).not.toBeVisible();
diff --git a/e2e/keyboard-shortcuts.spec.ts b/e2e/keyboard-shortcuts.spec.ts
new file mode 100644
index 000000000..d796998db
--- /dev/null
+++ b/e2e/keyboard-shortcuts.spec.ts
@@ -0,0 +1,310 @@
+import AxeBuilder from "@axe-core/playwright";
+import type { Page } from "@playwright/test";
+
+import { mockDay } from "./setup/day-mock";
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { expect, test } from "./setup/test";
+import { mockTimeline } from "./utils/mock-timeline";
+
+/**
+ * v1.42 — the keyboard shortcuts.
+ *
+ * - `g t` opens the timeline while its module is on, and does nothing while
+ * it is off;
+ * - keys typed into a field stay in the field;
+ * - `?` opens the list: a titled dialog that keeps focus inside and closes
+ * on Escape; the account menu opens the same list;
+ * - `n` opens the add menu;
+ * - `[` and `]` step the open day, never past today;
+ * - `g p` opens and closes the day like a click on its strip (focus into
+ * the panel, back to the strip); below the docking width it opens the
+ * sheet on the last day and closes it again.
+ *
+ * The module map is the account's real `/api/auth/me` with only `timeline`
+ * rewritten, so the switch is never moved on a shared account. Desktop only:
+ * a phone has no keyboard to press these on.
+ */
+
+test.use({ storageState: STORAGE_STATE_PATH });
+
+const TODAY = new Intl.DateTimeFormat("en-CA", {
+ timeZone: "Europe/Berlin",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+}).format(new Date());
+
+function shiftDay(key: string, delta: number): string {
+ const d = new Date(`${key}T12:00:00Z`);
+ d.setUTCDate(d.getUTCDate() + delta);
+ return d.toISOString().slice(0, 10);
+}
+
+async function withTimelineModule(page: Page, on: boolean): Promise {
+ await page.route("**/api/auth/me", async (route) => {
+ const res = await route.fetch();
+ const json = await res.json().catch(() => null);
+ if (!json?.data?.modules) return route.fulfill({ response: res });
+ json.data.modules.timeline = on;
+ return route.fulfill({
+ response: res,
+ body: JSON.stringify(json),
+ headers: { ...res.headers(), "content-type": "application/json" },
+ });
+ });
+}
+
+/** The shell is up and its listener registered. */
+async function shellReady(page: Page): Promise {
+ await expect(page.locator("#main-content")).toBeVisible({ timeout: 20_000 });
+ await expect(page.locator('[data-slot="top-bar"]')).toBeVisible();
+}
+
+const helpDialog = (page: Page) =>
+ page.getByRole("dialog", { name: "Keyboard shortcuts" });
+
+test.describe("keyboard shortcuts", () => {
+ test.beforeEach(async ({ context }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "keyboard journeys run on the desktop project",
+ );
+ const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
+ await context.addCookies([
+ { name: "healthlog-locale", value: "en", url: baseURL },
+ ]);
+ });
+
+ test("g t opens the timeline while its module is on", async ({ page }) => {
+ await withTimelineModule(page, true);
+ await mockTimeline(page, "ready", TODAY);
+ await page.goto("/");
+ await shellReady(page);
+ await page.keyboard.press("g");
+ await page.keyboard.press("t");
+ await expect(page).toHaveURL(/\/timeline$/);
+ });
+
+ test("g t does nothing while the timeline module is off", async ({
+ page,
+ }) => {
+ await withTimelineModule(page, false);
+ // Every client navigation, in order, so "nothing happened" is a fact
+ // rather than a wait.
+ await page.addInitScript(() => {
+ const log: string[] = [];
+ (window as unknown as { __pushed: string[] }).__pushed = log;
+ const push = history.pushState.bind(history);
+ history.pushState = (state, unused, url) => {
+ if (url != null) log.push(String(url));
+ push(state, unused, url);
+ };
+ });
+ await page.goto("/insights");
+ await shellReady(page);
+ await page.keyboard.press("g");
+ await page.keyboard.press("t");
+ // The positive control: the same reader opens a page that is on.
+ await page.keyboard.press("g");
+ await page.keyboard.press("d");
+ await expect(page).toHaveURL(/\/$/);
+ const pushed = await page.evaluate(
+ () => (window as unknown as { __pushed: string[] }).__pushed,
+ );
+ expect(pushed.some((url) => url.includes("/timeline"))).toBe(false);
+ });
+
+ test("keys typed into a field stay in the field", async ({ page }) => {
+ await withTimelineModule(page, true);
+ await page.goto("/");
+ const main = page.locator("#main-content");
+ await expect(main).toBeVisible({ timeout: 20_000 });
+ // A plain field in the page: the rule is about the focused element, not
+ // about which page carries it.
+ await page.evaluate(() => {
+ const field = document.createElement("input");
+ field.setAttribute("data-testid", "probe-field");
+ field.setAttribute("aria-label", "Probe");
+ document.getElementById("main-content")?.prepend(field);
+ });
+ const field = page.getByTestId("probe-field");
+ await expect(field).toBeVisible();
+ await field.focus();
+ await page.keyboard.type("gt n ? [");
+ await expect(field).toHaveValue("gt n ? [");
+ await expect(page).toHaveURL(/\/$/);
+ await expect(helpDialog(page)).toHaveCount(0);
+ await expect(page.getByTestId("capture-picker-options")).toHaveCount(0);
+ });
+
+ test("? opens a titled list that keeps focus and closes on Escape", async ({
+ page,
+ }) => {
+ await page.goto("/");
+ await shellReady(page);
+ await page.keyboard.press("Shift+?");
+ const dialog = helpDialog(page);
+ await expect(dialog).toBeVisible();
+ await expect(
+ dialog.locator('[data-slot="keyboard-shortcuts"] [data-group]'),
+ ).toHaveCount(3);
+ await expect(dialog.locator('[data-shortcut="go-d"] kbd')).toHaveText([
+ "g",
+ "d",
+ ]);
+ await expect(
+ dialog.locator('[data-group="day"] [data-shortcut="day-toggle"] kbd'),
+ ).toHaveText(["g", "p"]);
+
+ // Focus trap: Tab and Shift+Tab never leave the dialog.
+ for (const step of ["Tab", "Tab", "Tab", "Shift+Tab", "Shift+Tab"]) {
+ await page.keyboard.press(step);
+ expect(
+ await dialog.evaluate((el) => el.contains(document.activeElement)),
+ ).toBe(true);
+ }
+
+ // While the list is open, the page's shortcuts are off.
+ await page.keyboard.press("g");
+ await page.keyboard.press("i");
+ await expect(page).toHaveURL(/\/$/);
+
+ const axe = await new AxeBuilder({ page })
+ .include('[role="dialog"]')
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
+ .analyze();
+ expect(axe.violations).toEqual([]);
+
+ await page.keyboard.press("Escape");
+ await expect(dialog).toHaveCount(0);
+ });
+
+ test("the account menu opens the same list", async ({ page }) => {
+ await page.goto("/");
+ await shellReady(page);
+ await page
+ .getByRole("button", { name: "User menu" })
+ .locator("visible=true")
+ .first()
+ .click();
+ await page.locator('[data-slot="open-keyboard-shortcuts"]').click();
+ await expect(helpDialog(page)).toBeVisible();
+ });
+
+ test("n opens the add menu", async ({ page }) => {
+ await page.goto("/insights");
+ await shellReady(page);
+ await page.keyboard.press("n");
+ await expect(page.getByTestId("capture-picker-options")).toBeVisible();
+ await page.keyboard.press("Escape");
+ await expect(page.getByTestId("capture-picker-options")).toHaveCount(0);
+ });
+
+ test("[ and ] step the open day, never past today", async ({ page }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await mockDay(page);
+ const yesterday = shiftDay(TODAY, -1);
+ await page.goto(`/insights?day=${yesterday}`);
+ await shellReady(page);
+ const panel = page.locator('[data-slot="day-panel"]');
+ await expect(panel).toHaveAttribute("data-shell", "docked");
+ // Focus on the page, not in the panel: docked, the keys still reach it.
+ await page.locator("#main-content").focus();
+
+ await page.keyboard.press("[");
+ await expect(page).toHaveURL(new RegExp(`[?&]day=${shiftDay(TODAY, -2)}`));
+ await page.keyboard.press("]");
+ await page.keyboard.press("]");
+ await expect(page).toHaveURL(new RegExp(`[?&]day=${TODAY}`));
+ await page.keyboard.press("]");
+ await expect(page).toHaveURL(new RegExp(`[?&]day=${TODAY}`));
+ });
+
+ test("g p opens and closes the docked day, focus like its strip", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await mockDay(page);
+ await page.goto("/insights");
+ await shellReady(page);
+ const panel = page.locator('[data-slot="day-panel"]');
+ const strip = page.locator('[data-slot="day-strip-toggle"]');
+ await expect(strip).toHaveAttribute("aria-expanded", "false");
+ await expect(panel).toHaveCount(0);
+
+ await page.keyboard.press("g");
+ await page.keyboard.press("p");
+ await expect(panel).toHaveAttribute("data-shell", "docked");
+ await expect(page).toHaveURL(new RegExp(`[?&]day=${TODAY}`));
+ await expect(strip).toHaveAttribute("aria-expanded", "true");
+ // Into the panel, on its heading, the way the strip opens it.
+ await expect(panel.locator("h2").first()).toBeFocused();
+
+ await page.keyboard.press("g");
+ await page.keyboard.press("p");
+ await expect(panel).toHaveCount(0);
+ await expect(page).toHaveURL(/\/insights$/);
+ await expect(strip).toHaveAttribute("aria-expanded", "false");
+ // Back on the strip, which brings the day back.
+ await expect(strip).toBeFocused();
+ });
+
+ test("below the docking width g p opens the sheet on the last day and closes it", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1024, height: 800 });
+ await mockDay(page);
+ const day = shiftDay(TODAY, -3);
+ // Open and close a day once, so it is the one remembered.
+ await page.goto(`/insights?day=${day}`);
+ await shellReady(page);
+ const panel = page.locator('[data-slot="day-panel"]');
+ await expect(panel).toHaveAttribute("data-shell", "sheet");
+ await page.keyboard.press("Escape");
+ await expect(panel).toHaveCount(0);
+ await page.locator("#main-content").focus();
+
+ await page.keyboard.press("g");
+ await page.keyboard.press("p");
+ await expect(panel).toHaveAttribute("data-shell", "sheet");
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day,
+ );
+ // Over the sheet, the same keys close it.
+ await page.keyboard.press("g");
+ await page.keyboard.press("p");
+ await expect(panel).toHaveCount(0);
+ await expect(page).toHaveURL(/\/insights$/);
+ });
+
+ test("g p stays out of a field and out of another dialog", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await mockDay(page);
+ await page.goto("/insights");
+ await shellReady(page);
+ const panel = page.locator('[data-slot="day-panel"]');
+ await expect(page.locator('[data-slot="day-strip"]')).toBeVisible();
+ await page.evaluate(() => {
+ const field = document.createElement("input");
+ field.setAttribute("data-testid", "probe-field");
+ field.setAttribute("aria-label", "Probe");
+ document.getElementById("main-content")?.prepend(field);
+ });
+ await page.getByTestId("probe-field").focus();
+ await page.keyboard.type("gp");
+ await expect(page.getByTestId("probe-field")).toHaveValue("gp");
+ await expect(panel).toHaveCount(0);
+
+ await page.locator("#main-content").focus();
+ await page.keyboard.press("Shift+?");
+ await expect(helpDialog(page)).toBeVisible();
+ await page.keyboard.press("g");
+ await page.keyboard.press("p");
+ await expect(panel).toHaveCount(0);
+ await page.keyboard.press("Escape");
+ await expect(helpDialog(page)).toHaveCount(0);
+ });
+});
diff --git a/e2e/labs-list-wide.spec.ts b/e2e/labs-list-wide.spec.ts
new file mode 100644
index 000000000..4ca0fe357
--- /dev/null
+++ b/e2e/labs-list-wide.spec.ts
@@ -0,0 +1,140 @@
+import { expect, test } from "./setup/test";
+import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
+import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture";
+import {
+ expectNoHorizontalOverflow,
+ settleForOverflowMeasurement,
+} from "./utils/horizontal-overflow";
+
+/**
+ * The lab list uses the width it has, at every width, inside a frame that does
+ * not move.
+ *
+ * The signed-in frame is 1280 px wide on purpose, so a page never jumps
+ * sideways when you move between routes. Inside it the compact list was one
+ * five-column row whose only flexible column was the name: on a wide window the
+ * name column sat mostly empty while the range bar and the trend stayed small
+ * at the right edge. The columns now follow the width of the list's own card
+ * (a container query), not the window's:
+ *
+ * - below 64 rem of card the row is the `lg` table it always was, and the
+ * range bar keeps its 12 rem;
+ * - from 64 rem the bar follows its column up to 18 rem, and the trend
+ * column stays as wide as its 72 px sparkline.
+ *
+ * From `lg` a long name wraps instead of being cut off. The tiers are measured
+ * on the card's width and not the viewport's, so the spec reads the card and
+ * asserts the tier that width belongs to; a width at which no card ever reaches
+ * 64 rem would otherwise pass the wide check by never running it.
+ *
+ * The account is the one `globalSetup` seeds for the mobile route sweep: a long
+ * analyte name, readings with reference ranges. Desktop only; the phone layout
+ * has its own specs.
+ */
+
+const WIDTHS = [1280, 1920, 3440] as const;
+const REM = 16;
+const BAR = '[data-slot="lab-reference-range-bar"]';
+
+test.describe("the lab list uses its width", () => {
+ test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
+
+ test.beforeEach(async ({ context }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "desktop-only spec",
+ );
+ const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
+ await context.addCookies([
+ { name: "healthlog-locale", value: "de", url: baseURL },
+ ]);
+ });
+
+ for (const width of WIDTHS) {
+ test(`at ${width} px: no sideways scroll, names read in full, the bar follows its tier`, async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width, height: 1000 });
+ await page.goto("/labs", { waitUntil: "domcontentloaded" });
+ const names = page.locator('[data-slot="lab-list-analyte"]');
+ await expect(names.first()).toBeVisible();
+ await settleForOverflowMeasurement(page);
+
+ await expectNoHorizontalOverflow(page, `/labs at ${width}px`);
+
+ // The long name is the point of the wrap: it is fully on screen.
+ const long = page
+ .locator('[data-slot="lab-list-analyte"]')
+ .filter({ hasText: MOBILE_ROUTES_ANALYTE });
+ await expect(long.first()).toBeVisible();
+ const clipped = await names.evaluateAll(
+ (nodes) =>
+ nodes.filter((node) => node.scrollWidth > node.clientWidth + 1)
+ .length,
+ );
+ expect(clipped, "an analyte name is cut off").toBe(0);
+
+ const cardWidth = await page
+ .locator('[data-slot="lab-list"] [data-slot="card"]')
+ .first()
+ .evaluate((node) => node.getBoundingClientRect().width);
+ const bars = page.locator(BAR);
+ expect(await bars.count()).toBeGreaterThan(0);
+ const barWidths = await bars.evaluateAll((nodes) =>
+ nodes.map((node) => node.getBoundingClientRect().width),
+ );
+
+ if (cardWidth < 64 * REM) {
+ // The narrow tier: exactly what the list had, 12 rem.
+ for (const w of barWidths) expect(Math.round(w)).toBe(12 * REM);
+ } else {
+ // The wide tier: wider than 12 rem, no wider than its column's cap.
+ for (const w of barWidths) {
+ expect(w).toBeGreaterThan(12 * REM);
+ expect(w).toBeLessThanOrEqual(18 * REM + 1);
+ }
+ }
+
+ // The trend column is exactly as wide as the sparkline it holds, in both
+ // tiers. The sparkline is a fixed 72 px glyph; a wider column would only
+ // open an empty strip between it and the chevron.
+ const tracks = await page
+ .locator('[data-slot="lab-list"] [data-slot="card-content"]')
+ .first()
+ .evaluate((node) => getComputedStyle(node).gridTemplateColumns);
+ const trendTrack = tracks.split(" ")[3];
+ expect(trendTrack, `trend column in "${tracks}"`).toBe("72px");
+
+ // The widest windows must actually reach the wide tier, or the branch
+ // above never ran.
+ if (width >= 1920) {
+ expect(
+ cardWidth,
+ "the card never reached 64 rem",
+ ).toBeGreaterThanOrEqual(64 * REM);
+ }
+ });
+ }
+
+ test("the frame is the same width on /labs, /measurements and /mood at every viewport", async ({
+ page,
+ }) => {
+ for (const width of WIDTHS) {
+ await page.setViewportSize({ width, height: 1000 });
+ const frameWidths: Record = {};
+ for (const path of ["/labs", "/measurements", "/mood"]) {
+ await page.goto(path, { waitUntil: "domcontentloaded" });
+ const frame = page.locator('[data-slot="main-content-wrapper"]');
+ await expect(frame).toBeVisible();
+ frameWidths[path] = Math.round(
+ await frame.evaluate((node) => node.getBoundingClientRect().width),
+ );
+ }
+ const widths = Object.values(frameWidths);
+ expect(
+ new Set(widths).size,
+ `the frame differs between routes at ${width}px: ${JSON.stringify(frameWidths)}`,
+ ).toBe(1);
+ }
+ });
+});
diff --git a/e2e/measurement-flow.spec.ts b/e2e/measurement-flow.spec.ts
index 28b48c145..62e4d2137 100644
--- a/e2e/measurement-flow.spec.ts
+++ b/e2e/measurement-flow.spec.ts
@@ -135,7 +135,13 @@ test.describe("add measurement flow", () => {
// v1.5 phase-5: the menu items now have distinct labels — the
// measurement entry says "Measurement" / "Messung" instead of "Add",
// so we can target it directly.
- await page.getByRole("menuitem", { name: /measurement|messung/i }).click();
+ // The desktop rail opens a dropdown, the phone shell the capture sheet
+ // the bottom nav's plus opens; the measurement entry is in either.
+ await page
+ .getByRole("menuitem", { name: /measurement|messung/i })
+ .or(page.getByRole("button", { name: /^(measurement|messung)\b/i }))
+ .first()
+ .click();
// Form should be visible inside the dialog. Switch type to WEIGHT
// (combo-box defaults to BLOOD_PRESSURE).
diff --git a/e2e/mobile-horizontal-overflow.spec.ts b/e2e/mobile-horizontal-overflow.spec.ts
index 20e57df8d..c955b9fc9 100644
--- a/e2e/mobile-horizontal-overflow.spec.ts
+++ b/e2e/mobile-horizontal-overflow.spec.ts
@@ -9,6 +9,7 @@ import {
PHONE_WIDTHS,
settleForOverflowMeasurement,
} from "./utils/horizontal-overflow";
+import { revealDeferredSections } from "./utils/deferred-sections";
/**
* The content-heavy routes must not scroll sideways at any phone width.
@@ -73,6 +74,9 @@ test.describe("content-heavy routes have no horizontal page scroll at phone widt
for (const width of PHONE_WIDTHS) {
await page.setViewportSize({ width, height: 851 });
await page.goto(routeCase.path, { waitUntil: "domcontentloaded" });
+ // Measure the whole page, the overview sections that wait for the
+ // viewport included, or a wide card far down would go unmeasured.
+ if (routeCase.path === "/insights") await revealDeferredSections(page);
await expect(page.locator(routeCase.ready).first()).toBeVisible();
await settleForOverflowMeasurement(page);
await expectNoHorizontalOverflow(page, `${routeCase.path} @${width}px`);
@@ -93,6 +97,7 @@ test.describe("content-heavy routes have no horizontal page scroll at phone widt
await mockPopulatedInsights(page);
await page.setViewportSize({ width: 390, height: 851 });
await page.goto("/insights", { waitUntil: "domcontentloaded" });
+ await revealDeferredSections(page);
const signals = page.locator('[data-slot="coincident-deviation-section"]');
await expect(signals).toBeVisible();
@@ -129,6 +134,7 @@ test.describe("content-heavy routes have no horizontal page scroll at phone widt
await mockPopulatedInsights(page);
await page.setViewportSize({ width: 390, height: 851 });
await page.goto("/insights", { waitUntil: "domcontentloaded" });
+ await revealDeferredSections(page);
// Wait on the CAPTION, not the heading: the card first paints a skeleton
// whose heading carries no action slot, then swaps to the resolved shell.
@@ -179,6 +185,7 @@ test.describe("content-heavy routes have no horizontal page scroll at phone widt
await mockPopulatedInsights(page);
await page.setViewportSize({ width: 390, height: 851 });
await page.goto("/insights", { waitUntil: "domcontentloaded" });
+ await revealDeferredSections(page);
// Wait for the same readiness signal and settle step every route case
// above uses. This was the one test in the file that measured and injected
// straight after `domcontentloaded`, against a tree that was still
diff --git a/e2e/mobile-route-overflow.spec.ts b/e2e/mobile-route-overflow.spec.ts
index 2529cacdf..0972d705c 100644
--- a/e2e/mobile-route-overflow.spec.ts
+++ b/e2e/mobile-route-overflow.spec.ts
@@ -48,6 +48,8 @@ const NOT_SIGNED_IN: Record = {
"/invite/[token]":
"an invitation landing page for somebody without an account",
"/c/[token]": "the clinician share-link page, opened without a session",
+ "/claim/[token]":
+ "the managed-profile handover link, opened by the person without a session (covered by v142-managed-profile-handover.spec.ts)",
"/confirm-access": "the re-proof page, reached only from a stale session",
"/enroll-mfa": "the forced second-factor enrolment of a policy-bound account",
"/onboarding": "the first-run setup flow; covered by setup-flow-*.spec.ts",
diff --git a/e2e/modules-off.spec.ts b/e2e/modules-off.spec.ts
index ceb79df73..2995a8c30 100644
--- a/e2e/modules-off.spec.ts
+++ b/e2e/modules-off.spec.ts
@@ -25,6 +25,7 @@ import type { Page } from "@playwright/test";
import { MODULES_STORAGE_STATE_PATH } from "./setup/global-setup";
import { modulesSeedState } from "./setup/modules-fixture";
import { expect, test } from "./setup/test";
+import { revealDeferredSections } from "./utils/deferred-sections";
test.use({ storageState: MODULES_STORAGE_STATE_PATH });
test.describe.configure({ mode: "serial" });
@@ -130,6 +131,8 @@ test("mood and medications, switched off, leave every surface they own", async (
await expect(
page.locator('[data-slot="insights-tab-strip-pill"][href="/insights"]'),
).toBeVisible({ timeout: 20_000 });
+ // The trends row sits below the sections that mount with the page.
+ await revealDeferredSections(page);
await expect(page.locator('[data-slot="trends-row"]')).toBeVisible();
await expect(page.locator(moodPill)).toHaveCount(0);
await expect(page.locator(medsPill)).toHaveCount(0);
diff --git a/e2e/mood-timestamp.spec.ts b/e2e/mood-timestamp.spec.ts
index 28daa3df8..f157aa674 100644
--- a/e2e/mood-timestamp.spec.ts
+++ b/e2e/mood-timestamp.spec.ts
@@ -32,7 +32,9 @@ test.describe("mood timestamp capture", () => {
});
await page.goto("/mood", { waitUntil: "domcontentloaded" });
- await page.getByRole("button", { name: "Add", exact: true }).click();
+ // The list is mocked empty, so its empty state carries the add action
+ // (the header drops its own while there is nothing to list).
+ await page.getByRole("button", { name: "Log your first mood" }).click();
await page.getByRole("radio", { name: "Amazing" }).click();
const details = page.getByRole("button", { name: "Note & details" });
diff --git a/e2e/onboarding-tour-passthrough.spec.ts b/e2e/onboarding-tour-passthrough.spec.ts
index 003bd5e01..433797e8b 100644
--- a/e2e/onboarding-tour-passthrough.spec.ts
+++ b/e2e/onboarding-tour-passthrough.spec.ts
@@ -141,7 +141,10 @@ test.describe("v1.4.33 F2 — onboarding tour passes clicks through to the page"
// so a naive "dim everything outside the spotlight rect" overlay
// covers it. The fix makes the entire tour layer pointer-events:
// none — every dim panel and the spotlight ring are visual only.
- const quickAddButton = page.locator('[data-tour-id="dashboard-quick-add"]');
+ // One add button per shell carries the tour id; the other is hidden.
+ const quickAddButton = page
+ .locator('[data-tour-id="dashboard-quick-add"]')
+ .filter({ visible: true });
await expect(quickAddButton).toBeVisible();
// Forensic guard — at the quick-add button's centre, every element
@@ -153,9 +156,11 @@ test.describe("v1.4.33 F2 — onboarding tour passes clicks through to the page"
// the stack is blocking, instead of just observing the symptom
// ("button click did not toggle aria-expanded").
const tourBlockers = await page.evaluate(() => {
- const btn = document.querySelector(
- '[data-tour-id="dashboard-quick-add"]',
- );
+ const btn = [
+ ...document.querySelectorAll(
+ '[data-tour-id="dashboard-quick-add"]',
+ ),
+ ].find((el) => el.getClientRects().length > 0);
if (!btn) return ["no-button"];
const rect = btn.getBoundingClientRect();
const x = rect.left + rect.width / 2;
@@ -192,7 +197,14 @@ test.describe("v1.4.33 F2 — onboarding tour passes clicks through to the page"
await quickAddButton.click();
await expect(quickAddButton).toHaveAttribute("aria-expanded", "true");
- await expect(page.getByRole("menuitem").first()).toBeVisible();
+ // The desktop rail opens a dropdown; the phone shell opens the capture
+ // sheet the bottom nav's plus opens. Either proves the click landed.
+ await expect(
+ page
+ .getByRole("menuitem")
+ .or(page.locator('[role="dialog"][data-state="open"]'))
+ .first(),
+ ).toBeVisible();
// The tour stays mounted — the click went to the underlying button,
// not anything in the tour layer.
diff --git a/e2e/pwa-mobile.spec.ts b/e2e/pwa-mobile.spec.ts
new file mode 100644
index 000000000..dc93574f1
--- /dev/null
+++ b/e2e/pwa-mobile.spec.ts
@@ -0,0 +1,239 @@
+import type { Page } from "@playwright/test";
+
+import { expect, test } from "./setup/test";
+import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
+
+/**
+ * The installed-app behaviours a phone depends on, measured in the Pixel 5
+ * project (a touch screen: `pointer: coarse`).
+ *
+ * 1. The "new version" hint stands above the bottom bar instead of on it,
+ * and its Reload action reloads the page.
+ * 2. With the on-screen keyboard open, a bottom sheet stands on the
+ * keyboard (its Save button stays visible) and the bottom bar steps
+ * aside. Playwright has no on-screen keyboard, so the visual viewport is
+ * replaced by a stand-in whose height the test shrinks the way a
+ * keyboard does; what is asserted is the page's answer to that signal.
+ * 3. No form field a phone can focus is set below 16 px, in portrait or
+ * held sideways, because iOS Safari zooms the page into such a field.
+ * 4. Held sideways, the sign-in card is not centred out of reach.
+ *
+ * The account is the populated mobile-sweep account; nothing here writes.
+ */
+
+const VISUAL_VIEWPORT_STAND_IN = () => {
+ const stand = new EventTarget() as EventTarget & {
+ height: number;
+ width: number;
+ offsetTop: number;
+ offsetLeft: number;
+ pageTop: number;
+ pageLeft: number;
+ scale: number;
+ };
+ stand.height = window.innerHeight;
+ stand.width = window.innerWidth;
+ stand.offsetTop = 0;
+ stand.offsetLeft = 0;
+ stand.pageTop = 0;
+ stand.pageLeft = 0;
+ stand.scale = 1;
+ Object.defineProperty(window, "visualViewport", {
+ configurable: true,
+ get: () => stand,
+ });
+ (window as unknown as { __viewport: typeof stand }).__viewport = stand;
+};
+
+async function setKeyboard(page: Page, covered: number) {
+ await page.evaluate((px) => {
+ const stand = (
+ window as unknown as {
+ __viewport: EventTarget & { height: number };
+ }
+ ).__viewport;
+ stand.height = window.innerHeight - px;
+ stand.dispatchEvent(new Event("resize"));
+ }, covered);
+}
+
+async function openCaptureForm(page: Page) {
+ await page.goto("/");
+ await page.getByTestId("bottom-nav-capture").click();
+ const picker = page.locator('[data-slot="responsive-sheet-content"]');
+ await expect(picker).toBeVisible();
+ await picker
+ .locator('[data-slot="responsive-sheet-body"] button')
+ .first()
+ .click();
+ const form = page
+ .locator('[data-slot="responsive-sheet-content"]')
+ .filter({ has: page.locator('[data-slot="responsive-sheet-footer"]') });
+ await expect(form).toBeVisible();
+ return form;
+}
+
+/** Every visible field a phone can focus, with its computed font size. */
+async function fieldFontSizes(page: Page) {
+ return page.evaluate(() =>
+ Array.from(
+ document.querySelectorAll(
+ "input:not([type=hidden]):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]),textarea,select",
+ ),
+ )
+ .filter((el) => {
+ const r = el.getBoundingClientRect();
+ return r.width > 2 && r.height > 2;
+ })
+ .map((el) => ({
+ field:
+ el.getAttribute("name") ||
+ el.id ||
+ el.getAttribute("aria-label") ||
+ el.tagName,
+ px: parseFloat(getComputedStyle(el).fontSize),
+ })),
+ );
+}
+
+test.describe("installed app on a phone", () => {
+ test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
+
+ test.beforeEach(({}, testInfo) => {
+ test.skip(testInfo.project.name !== "chromium-mobile", "mobile-only spec");
+ });
+
+ test("the new-version hint stands above the bottom bar and reloads", async ({
+ page,
+ }) => {
+ await page.route("**/api/version", (route) =>
+ route.fulfill({
+ json: {
+ data: { version: "999.0.0", buildSha: "e2e" },
+ error: null,
+ },
+ }),
+ );
+ await page.goto("/");
+ const toast = page.locator("[data-sonner-toast]").filter({
+ has: page.getByRole("button", { name: "Reload" }),
+ });
+ // The poller's first check runs five seconds after mount.
+ await expect(toast).toBeVisible({ timeout: 15_000 });
+
+ const nav = page.locator('[data-slot="bottom-nav"]');
+ const navBox = (await nav.boundingBox())!;
+ // Polled: the toast slides in from below, so an early sample is
+ // mid-animation.
+ await expect
+ .poll(async () => {
+ const b = (await toast.boundingBox())!;
+ return b.y + b.height;
+ })
+ .toBeLessThanOrEqual(navBox.y);
+ // The bottom bar's controls are still reachable with the hint up.
+ await expect(page.getByTestId("bottom-nav-more")).toBeVisible();
+
+ const reloaded = page.waitForEvent("load");
+ await toast.getByRole("button", { name: "Reload" }).click();
+ await reloaded;
+ });
+
+ test("a bottom sheet stands on the on-screen keyboard and the bottom bar steps aside", async ({
+ page,
+ }) => {
+ await page.addInitScript(VISUAL_VIEWPORT_STAND_IN);
+ const form = await openCaptureForm(page);
+ const save = form
+ .locator('[data-slot="responsive-sheet-footer"] button')
+ .last();
+ const viewportHeight = page.viewportSize()!.height;
+
+ const bottomEdge = async () => {
+ const b = (await form.boundingBox())!;
+ return Math.round(b.y + b.height);
+ };
+ // Closed keyboard: the sheet sits on the bottom edge (polled: it slides
+ // in from below).
+ await expect.poll(bottomEdge).toBe(viewportHeight);
+
+ const keyboard = 300;
+ await setKeyboard(page, keyboard);
+ await expect(page.locator("html")).toHaveAttribute("data-keyboard", "open");
+ await expect.poll(bottomEdge).toBe(viewportHeight - keyboard);
+ const box = (await form.boundingBox())!;
+ expect(box.y).toBeGreaterThanOrEqual(0);
+ const saveBox = (await save.boundingBox())!;
+ expect(saveBox.y + saveBox.height).toBeLessThanOrEqual(
+ viewportHeight - keyboard,
+ );
+ await expect(page.locator('[data-slot="bottom-nav"]')).toBeHidden();
+
+ // A toolbar-sized change is not a keyboard.
+ await setKeyboard(page, 60);
+ await expect(page.locator("html")).not.toHaveAttribute("data-keyboard");
+ await expect.poll(bottomEdge).toBe(viewportHeight);
+ });
+
+ for (const viewport of [
+ { width: 390, height: 844 },
+ { width: 844, height: 390 },
+ ]) {
+ test(`no focusable field is below 16 px at ${viewport.width}x${viewport.height} (iOS focus zoom)`, async ({
+ page,
+ }) => {
+ await page.setViewportSize(viewport);
+ const offenders: string[] = [];
+ for (const path of [
+ "/settings",
+ "/documents",
+ "/settings/notifications",
+ ]) {
+ await page.goto(path);
+ await page.waitForLoadState("networkidle");
+ const fields = await fieldFontSizes(page);
+ expect(fields.length, `${path} renders no field`).toBeGreaterThan(0);
+ for (const f of fields)
+ if (f.px < 16) offenders.push(`${path} ${f.field} ${f.px}px`);
+ }
+ if (viewport.width < 768) {
+ const form = await openCaptureForm(page);
+ await expect(form.locator("input").first()).toBeVisible();
+ for (const f of await fieldFontSizes(page))
+ if (f.px < 16) offenders.push(`capture ${f.field} ${f.px}px`);
+ }
+ expect(offenders).toEqual([]);
+ });
+ }
+});
+
+test.describe("signed out, held sideways", () => {
+ test.use({ viewport: { width: 844, height: 390 } });
+
+ test.beforeEach(({}, testInfo) => {
+ test.skip(testInfo.project.name !== "chromium-mobile", "mobile-only spec");
+ });
+
+ test("the sign-in card stays reachable from its top edge", async ({
+ page,
+ }) => {
+ await page.goto("/auth/login");
+ const card = page.locator("main#main-content > *").first();
+ await expect(card).toBeVisible();
+ // Open the password form, the tallest state of the card.
+ await page
+ .getByRole("button", { name: /password|passwort/i })
+ .first()
+ .click();
+ await expect(page.locator("input[type=password]").first()).toBeVisible();
+ const box = (await card.boundingBox())!;
+ expect(box.y).toBeGreaterThanOrEqual(0);
+ // And the document scrolls to reach whatever lies below the fold.
+ const scrollable = await page.evaluate(
+ () =>
+ document.documentElement.scrollHeight >= window.innerHeight &&
+ getComputedStyle(document.documentElement).overflowY !== "hidden",
+ );
+ expect(scrollable).toBe(true);
+ });
+});
diff --git a/e2e/pwa-offline.spec.ts b/e2e/pwa-offline.spec.ts
new file mode 100644
index 000000000..05701ce0d
--- /dev/null
+++ b/e2e/pwa-offline.spec.ts
@@ -0,0 +1,64 @@
+import { expect, test } from "./setup/test";
+import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
+
+/**
+ * What the installed app shows without a connection, with the service worker
+ * actually running.
+ *
+ * Runs in the `chromium-service-worker` project, the one that lets the worker
+ * register (every other project blocks it; see `playwright.config.ts`), and
+ * like the other spec there it uses no route mocks: the claim is about what
+ * the worker itself serves.
+ *
+ * 1. A page that is open when the connection drops stays the app and says
+ * so (the offline strip).
+ * 2. Opening a page without a connection gets the worker's offline page:
+ * the word "Offline" and a working retry control, never the browser's
+ * error. (Signed-in pages are never cached, by design: the proxy pins
+ * `private, no-store` on them because some carry the record in the
+ * HTML. So this is what an offline launch of the installed app shows.)
+ * 3. When the connection returns, that page reloads into the app by
+ * itself.
+ *
+ * The account is the populated mobile-sweep account; nothing here writes.
+ */
+test.describe("offline, with the service worker", () => {
+ test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
+
+ test("an open page says it is offline, a new one opens the offline page, and both recover", async ({
+ page,
+ context,
+ }) => {
+ await page.goto("/medications");
+ await page.waitForFunction(
+ () => navigator.serviceWorker?.controller !== null,
+ undefined,
+ { timeout: 20_000 },
+ );
+ await expect(page.locator("#main-content")).toBeVisible();
+ await page.waitForLoadState("networkidle");
+
+ try {
+ await context.setOffline(true);
+ await expect(page.locator('[data-slot="offline-banner"]')).toBeVisible();
+
+ const response = await page.goto("/timeline");
+ expect(response?.status()).toBe(503);
+ await expect(page.getByText("Offline", { exact: true })).toBeVisible();
+ const retry = page.getByRole("link", { name: "Reload" });
+ await expect(retry).toBeVisible();
+ const box = (await retry.boundingBox())!;
+ expect(box.width).toBeGreaterThanOrEqual(44);
+ expect(box.height).toBeGreaterThanOrEqual(44);
+
+ // Back online: the offline page reloads itself into the app.
+ const reloaded = page.waitForEvent("load");
+ await context.setOffline(false);
+ await reloaded;
+ await expect(page.locator("#main-content")).toBeVisible();
+ expect(new URL(page.url()).pathname).toBe("/timeline");
+ } finally {
+ await context.setOffline(false);
+ }
+ });
+});
diff --git a/e2e/score-history.spec.ts b/e2e/score-history.spec.ts
new file mode 100644
index 000000000..b91dd5d01
--- /dev/null
+++ b/e2e/score-history.spec.ts
@@ -0,0 +1,366 @@
+import type { Page, Route } from "@playwright/test";
+
+import { mockDay } from "./setup/day-mock";
+import { mockPopulatedInsights } from "./utils/mock-populated-insights";
+import { SCORE_HISTORY_STORAGE_STATE_PATH } from "./setup/global-setup";
+import { expect, test } from "./setup/test";
+import type { DailyDigest } from "@/lib/daily/digest";
+import { DIGEST_AI_AVAILABLE } from "@/__tests__/helpers/ai-capability-fixtures";
+
+/**
+ * v1.42 — the score histories and the dashboard's top card switch.
+ *
+ * - The health score's page charts its daily course with the range tabs
+ * every chart offers; a tab asks the server for its window, the choice
+ * survives a reload, and a point opens its day.
+ * - Readiness, recovery, the sleep score and strain carry the same four
+ * tabs in the same component.
+ * - The top card goes away when the layout settings switch it off, and
+ * comes back when they switch it on.
+ *
+ * The score history and the derived scores are route mocks in the
+ * contract's shape (the server's answers are pinned by the integration
+ * suite); the range preference and the layout are this account's own, saved
+ * for real. Stable data attributes only.
+ */
+
+test.use({ storageState: SCORE_HISTORY_STORAGE_STATE_PATH });
+// Every test writes this one account's layout blob, a Serializable
+// read-modify-write; run in parallel they would abort each other's write.
+test.describe.configure({ mode: "serial" });
+
+const RANGES = ["7", "30", "90", "0"];
+
+function dayKey(offset: number): string {
+ const now = new Date();
+ const local = new Date(
+ now.toLocaleString("en-US", { timeZone: "Europe/Berlin" }),
+ );
+ local.setDate(local.getDate() + offset);
+ const y = local.getFullYear();
+ const m = String(local.getMonth() + 1).padStart(2, "0");
+ const d = String(local.getDate()).padStart(2, "0");
+ return `${y}-${m}-${d}`;
+}
+
+/** Answer the score history with one point per day of the window, max 40. */
+async function mockScoreHistory(page: Page): Promise {
+ const asked: number[] = [];
+ await page.route(/\/api\/insights\/score-history\?/, (route: Route) => {
+ const url = new URL(route.request().url());
+ const days = Number(url.searchParams.get("days"));
+ asked.push(days);
+ const count = Math.min(days, 40);
+ const points = Array.from({ length: count }, (_, i) => ({
+ day: dayKey(i - count + 1),
+ value: 60 + ((i * 7) % 25),
+ seamBreak: false,
+ }));
+ return route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ score: url.searchParams.get("score"),
+ days,
+ points,
+ band: { lo: 64, hi: 76, n: 30 },
+ },
+ error: null,
+ }),
+ });
+ });
+ return asked;
+}
+
+/** An `ok` derived score, so the score page draws its history under it. */
+async function mockDerived(page: Page) {
+ await page.route(/\/api\/insights\/derived\?/, (route: Route) => {
+ const metric = new URL(route.request().url()).searchParams.get("metric");
+ return route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ metric,
+ status: "ok",
+ value: {
+ score: 72,
+ band: "green",
+ components: [],
+ subScores: [],
+ night: dayKey(0),
+ asleepMinutes: 420,
+ inBedMinutes: 450,
+ windowNights: 20,
+ },
+ coverage: {
+ requiredInputs: 1,
+ presentInputs: 1,
+ historyDays: 30,
+ missing: [],
+ },
+ confidence: "high",
+ provenance: {
+ inputs: [],
+ source: "rollup",
+ windowDays: 30,
+ computedAt: new Date().toISOString(),
+ },
+ reason: null,
+ assessment: null,
+ },
+ error: null,
+ }),
+ });
+ });
+}
+
+async function resetRange(page: Page, chartKey: string) {
+ const res = await page.request.put("/api/dashboard/chart-overlay-prefs", {
+ data: {
+ chartKey,
+ prefs: {
+ showTrendIndicator: false,
+ showTrendArrow: false,
+ showTargetRange: false,
+ comparisonBaseline: "none",
+ rangePoints: 30,
+ },
+ },
+ });
+ expect(res.status()).toBe(200);
+}
+
+async function expectSharedTabs(page: Page, chart: string, pressed: string) {
+ const tabs = page.locator(`${chart} [data-slot="chart-range-tab"]`);
+ await expect(tabs).toHaveCount(4);
+ expect(
+ await tabs.evaluateAll((nodes) =>
+ nodes.map((node) => node.getAttribute("data-range")),
+ ),
+ ).toEqual(RANGES);
+ await expect(
+ page.locator(`${chart} [data-slot="chart-range-tab"][aria-pressed="true"]`),
+ ).toHaveAttribute("data-range", pressed);
+}
+
+test.describe("score histories", () => {
+ test.beforeEach(async ({ page, context }, testInfo) => {
+ const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
+ await context.addCookies([
+ { name: "healthlog-locale", value: "en", url: baseURL },
+ ]);
+ await mockDay(page, { index: "full" });
+ });
+
+ test("the health score's range tabs switch the window and are remembered", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await resetRange(page, "scoreHealth");
+ const asked = await mockScoreHistory(page);
+
+ await page.goto("/insights/health-score");
+ const chart = page.locator('[data-slot="score-trend-chart"]');
+ await expect(chart).toHaveAttribute("data-score", "HEALTH_SCORE");
+ await expectSharedTabs(page, '[data-slot="score-trend-chart"]', "30");
+ await expect(chart.locator('[data-slot="chart-plot"]')).toBeVisible();
+ await expect(chart.locator('[data-slot="score-trend-band"]')).toBeVisible();
+ expect(asked).toContain(30);
+
+ await chart
+ .locator('[data-slot="chart-range-tab"][data-range="7"]')
+ .click();
+ await expect(chart).toHaveAttribute("data-range", "7");
+ await expect.poll(() => asked.at(-1)).toBe(7);
+ // The table lists exactly the window's days.
+ await chart.locator('[data-slot="chart-data-table-toggle"]').click();
+ await expect(
+ chart.locator('[data-slot="chart-data-table-row"]'),
+ ).toHaveCount(7);
+
+ // Remembered: a reload opens on seven days again.
+ await page.reload();
+ await expect(chart).toHaveAttribute("data-range", "7");
+
+ await chart
+ .locator('[data-slot="chart-range-tab"][data-range="0"]')
+ .click();
+ await expect(chart).toHaveAttribute("data-range", "0");
+ await expect.poll(() => asked.at(-1)).toBe(3650);
+
+ // A point opens its day beside the page.
+ const plot = chart.locator('[data-slot="chart-plot"]');
+ await expect(plot).toHaveAttribute("data-day-links", "true");
+ const box = (await plot.boundingBox())!;
+ await page.mouse.click(box.x + box.width * 0.8, box.y + box.height / 2);
+ await expect(page.locator('[data-slot="day-panel"]')).toHaveAttribute(
+ "data-shell",
+ "docked",
+ );
+ await expect(page).toHaveURL(/[?&]day=\d{4}-\d{2}-\d{2}/);
+
+ await resetRange(page, "scoreHealth");
+ });
+
+ test("the score panel leads to the health score page and back", async ({
+ page,
+ }) => {
+ await mockPopulatedInsights(page);
+ await mockScoreHistory(page);
+ await page.goto("/insights");
+ const link = page.locator('[data-slot="health-score-history-link"]');
+ await expect(link).toHaveAttribute("href", "/insights/health-score");
+ await link.click();
+ await expect(page).toHaveURL(/\/insights\/health-score$/);
+ await expect(
+ page.locator('[data-slot="health-score-back"]'),
+ ).toHaveAttribute("href", "/insights");
+ });
+
+ test("readiness and the sleep score offer the same tabs, and switching asks for the window", async ({
+ page,
+ }) => {
+ await resetRange(page, "scoreReadiness");
+ await mockDerived(page);
+ const asked = await mockScoreHistory(page);
+
+ await page.goto("/insights/scores/readiness");
+ const chart = page.locator('[data-slot="score-trend-chart"]');
+ await expect(chart).toHaveAttribute("data-score", "READINESS");
+ await expectSharedTabs(page, '[data-slot="score-trend-chart"]', "30");
+ await chart
+ .locator('[data-slot="chart-range-tab"][data-range="90"]')
+ .click();
+ await expect(chart).toHaveAttribute("data-range", "90");
+ await expect.poll(() => asked.at(-1)).toBe(90);
+ await resetRange(page, "scoreReadiness");
+
+ await page.goto("/insights/scores/sleep");
+ await expect(
+ page.locator('[data-slot="score-trend-chart"]'),
+ ).toHaveAttribute("data-score", "SLEEP_SCORE");
+ await expectSharedTabs(page, '[data-slot="score-trend-chart"]', "30");
+ });
+
+ test("recovery and strain carry the same tabs on their stored-score chart", async ({
+ page,
+ }) => {
+ await resetRange(page, "scoreRecovery");
+ await mockDerived(page);
+
+ await page.goto("/insights/scores/recovery");
+ const chart = '[data-slot="score-history-chart"]';
+ await expect(page.locator(chart)).toHaveAttribute(
+ "data-type",
+ "RECOVERY_SCORE",
+ );
+ await expectSharedTabs(page, chart, "30");
+ // A score history remembers its range, and mounts no overlay dropdown.
+ await expect(
+ page.locator(`${chart} [data-slot="chart-overlay-controls-trigger"]`),
+ ).toHaveCount(0);
+ await page
+ .locator(`${chart} [data-slot="chart-range-tab"][data-range="7"]`)
+ .click();
+ await expectSharedTabs(page, chart, "7");
+ await page.reload();
+ await expectSharedTabs(page, chart, "7");
+ await resetRange(page, "scoreRecovery");
+
+ await page.goto("/insights/scores/strain");
+ await expectSharedTabs(page, chart, "30");
+ });
+});
+
+const DIGEST: DailyDigest = {
+ generatedAt: new Date().toISOString(),
+ ai: DIGEST_AI_AVAILABLE,
+ phase: "final",
+ sleepPending: false,
+ score: {
+ value: 82,
+ band: "green",
+ delta: null,
+ deltaReason: "first_eligibility_window",
+ steadyWeeks: 2,
+ },
+ topSignal: null,
+ signalLine: null,
+ briefingLead: null,
+ lead: null,
+ today: [],
+ restMode: null,
+ line: "Your health score today is 82.",
+ worthALook: [],
+ justIn: null,
+ reactionLine: null,
+};
+
+test.describe("the dashboard's top card", () => {
+ test.afterEach(async ({ page }) => {
+ // Leave the account's layout at its defaults for the next run.
+ await page.request.delete("/api/dashboard/widgets");
+ });
+
+ test("switches off and on from the layout settings", async ({ page }) => {
+ await page.request.delete("/api/dashboard/widgets");
+ await page.route(/\/api\/daily\/digest(\?|$)/, (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data: DIGEST, error: null }),
+ }),
+ );
+
+ // The dashboard states the switch on its root. What the card shows
+ // depends on the day's digest, which the server renders into the page
+ // before any route mock can answer, so the switch is asserted there and
+ // not on the card's content.
+ const dashboard = page.locator("[data-today-card]");
+ const hero = page.locator('[data-slot="today-hero"]');
+ await page.goto("/");
+ await expect(dashboard).toHaveAttribute("data-today-card", "on");
+
+ async function flip(expected: "checked" | "unchecked") {
+ await page.goto("/settings/layout/dashboard");
+ const toggle = page.locator('[data-slot="today-card-switch"]');
+ await expect(toggle).toBeVisible();
+ await toggle.click();
+ await expect(toggle).toHaveAttribute("data-state", expected);
+ const saved = page.waitForResponse(
+ (res) =>
+ res.url().includes("/api/dashboard/widgets") &&
+ res.request().method() === "PUT",
+ );
+ await page
+ .locator('[data-slot="settings-dashboard-layout-save"]')
+ .click();
+ const response = await saved;
+ expect(response.status()).toBe(200);
+ const body = response.request().postDataJSON() as {
+ todayCardVisible?: boolean;
+ };
+ expect(body.todayCardVisible).toBe(expected === "checked");
+ }
+
+ await flip("unchecked");
+ await page.goto("/");
+ await expect(dashboard).toHaveAttribute("data-today-card", "off");
+ await expect(hero).toHaveCount(0);
+ await expect(page.locator('[data-slot="today-hero-skeleton"]')).toHaveCount(
+ 0,
+ );
+
+ // A phone width shows no card either.
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.reload();
+ await expect(hero).toHaveCount(0);
+ await page.setViewportSize({ width: 1280, height: 720 });
+
+ await flip("checked");
+ await page.goto("/");
+ await expect(dashboard).toHaveAttribute("data-today-card", "on");
+ });
+});
diff --git a/e2e/settings-mobile-consistency.spec.ts b/e2e/settings-mobile-consistency.spec.ts
index b9b67519e..7294d69ba 100644
--- a/e2e/settings-mobile-consistency.spec.ts
+++ b/e2e/settings-mobile-consistency.spec.ts
@@ -149,38 +149,42 @@ test.describe("Settings mobile consistency (Pixel 5)", () => {
}
});
- test("/settings/account: Sprache select shares one grid row with date-of-birth", async ({
+ test("/settings/account: the language select sits in the display card, outside the profile form", async ({
page,
}) => {
await page.goto("/settings/account", { waitUntil: "networkidle" });
- // Gate on the two elements this test is about, not on the network. An idle
- // network says nothing about whether React has rendered the profile form,
- // and the single `evaluate` below does not retry — so without this the
- // test reads an empty document and reports "language + dob fields must
- // exist", which sounds like a missing feature and is really a race.
+ // Gate on the elements this test is about, not on the network: the
+ // single `evaluate` below does not retry.
await expect(page.locator("#language-select")).toBeAttached();
await expect(page.locator("#dob")).toBeAttached();
- // The v1.4.27 R1 settings audit pairs date-of-birth with language
- // in a single `grid sm:grid-cols-2` row so the profile form keeps
- // a uniform two-column rhythm and the language field no longer
- // sits alone at the bottom with a `sm:max-w-xs` clamp.
- const sharedGrid = await page.evaluate(() => {
+ // The language, unit and format selects save the moment they change, so
+ // they live in their own card with no save button. Inside the profile
+ // form they read as covered by its Save. Each card keeps a two-column
+ // grid: the language shares one with the units, the date of birth one
+ // with the timezone.
+ const layout = await page.evaluate(() => {
const lang = document.getElementById("language-select");
const dob = document.getElementById("dob");
- if (!lang || !dob) return { found: false, sharedGrid: false };
- const langGrid = lang.closest('[class*="grid"]');
- const dobGrid = dob.closest('[class*="grid"]');
+ if (!lang || !dob) return null;
+ const displayCard = document.querySelector(
+ '[data-testid="settings-display-card"]',
+ );
return {
- found: true,
- sharedGrid: langGrid !== null && langGrid === dobGrid,
+ langInDisplayCard: !!displayCard && displayCard.contains(lang),
+ langInForm: lang.closest("form") !== null,
+ dobInForm: dob.closest("form") !== null,
+ langHasGrid: lang.closest('[class*="grid"]') !== null,
+ dobHasGrid: dob.closest('[class*="grid"]') !== null,
};
});
- expect(sharedGrid.found, "language + dob fields must exist").toBe(true);
- expect(sharedGrid.sharedGrid, "language + dob must share a grid").toBe(
- true,
- );
+ expect(layout, "language + dob fields must exist").not.toBeNull();
+ expect(layout!.langInDisplayCard).toBe(true);
+ expect(layout!.langInForm).toBe(false);
+ expect(layout!.dobInForm).toBe(true);
+ expect(layout!.langHasGrid).toBe(true);
+ expect(layout!.dobHasGrid).toBe(true);
});
test("/: Compare-to trigger renders at 44 px", async ({ page }) => {
diff --git a/e2e/setup/ai-optional-fixture.ts b/e2e/setup/ai-optional-fixture.ts
index bea502931..4a417d3e1 100644
--- a/e2e/setup/ai-optional-fixture.ts
+++ b/e2e/setup/ai-optional-fixture.ts
@@ -77,6 +77,8 @@ export async function resetAiOptionalAccount(): Promise {
export async function seedStoredBriefing(marker: string): Promise {
const insights = {
dailyBriefing: { paragraph: marker, keyFindings: [] },
+ // Generated now: only text written today is served as today's read.
+ briefingGeneratedAt: new Date().toISOString(),
};
await withAccount(async (pool, userId) => {
await pool.query(
diff --git a/e2e/setup/coach-live-stream.ts b/e2e/setup/coach-live-stream.ts
new file mode 100644
index 000000000..e82e97954
--- /dev/null
+++ b/e2e/setup/coach-live-stream.ts
@@ -0,0 +1,220 @@
+import type { Page, Request, Route } from "@playwright/test";
+
+/**
+ * A Coach chat stream the spec drives frame by frame.
+ *
+ * `route.fulfill` hands the browser the whole event stream at once, so the
+ * live states of a turn (the status line moving from "Thinking…" to
+ * "Fetching…" to "Summarising…", an interim table before the first token)
+ * would flash by unobserved. This replaces `fetch` in the page for the chat
+ * POST only: the response body is a stream the spec feeds with
+ * `feed(page, frames)` and closes with `end(page)`. Every other request goes
+ * through untouched, so `page.route` mocks keep working.
+ *
+ * The POST bodies are recorded in the page (`posts(page)`).
+ */
+export async function installLiveCoachStream(page: Page): Promise {
+ await page.addInitScript(() => {
+ const w = window as unknown as {
+ __coachPosts: unknown[];
+ __coachFeed: (frames: unknown[]) => void;
+ __coachEnd: () => void;
+ };
+ const original = window.fetch.bind(window);
+ let controller: ReadableStreamDefaultController | null = null;
+ const encoder = new TextEncoder();
+ w.__coachPosts = [];
+ w.__coachFeed = (frames) => {
+ for (const frame of frames) {
+ controller?.enqueue(
+ encoder.encode(`data: ${JSON.stringify(frame)}\n\n`),
+ );
+ }
+ };
+ w.__coachEnd = () => {
+ controller?.close();
+ controller = null;
+ };
+ window.fetch = async (input, init) => {
+ const url =
+ typeof input === "string"
+ ? input
+ : input instanceof URL
+ ? input.href
+ : input.url;
+ const method = (
+ init?.method ?? (input instanceof Request ? input.method : "GET")
+ ).toUpperCase();
+ if (
+ method === "POST" &&
+ new URL(url, window.location.href).pathname === "/api/insights/chat"
+ ) {
+ w.__coachPosts.push(JSON.parse(String(init?.body ?? "{}")));
+ const body = new ReadableStream({
+ start(c) {
+ controller = c;
+ },
+ });
+ init?.signal?.addEventListener("abort", () => {
+ try {
+ controller?.error(new DOMException("aborted", "AbortError"));
+ } catch {
+ // already closed
+ }
+ });
+ return new Response(body, {
+ status: 200,
+ headers: { "Content-Type": "text/event-stream" },
+ });
+ }
+ return original(input, init);
+ };
+ });
+}
+
+export async function feed(page: Page, frames: unknown[]): Promise {
+ await page.evaluate((f) => {
+ (window as unknown as { __coachFeed: (x: unknown[]) => void }).__coachFeed(
+ f,
+ );
+ }, frames);
+}
+
+export async function end(page: Page): Promise {
+ await page.evaluate(() => {
+ (window as unknown as { __coachEnd: () => void }).__coachEnd();
+ });
+}
+
+export async function posts(
+ page: Page,
+): Promise>> {
+ return page.evaluate(
+ () =>
+ (window as unknown as { __coachPosts: Array> })
+ .__coachPosts,
+ );
+}
+
+export function tokens(text: string) {
+ return text.split(/(?<= )/).map((token) => ({ type: "token", token }));
+}
+
+function fulfilJson(route: Route, data: unknown) {
+ return route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data, error: null }),
+ });
+}
+
+export interface StoredMessage {
+ id: string;
+ role: "user" | "assistant";
+ content: string;
+ metricSource?: Record | null;
+ providerType?: string | null;
+}
+
+/**
+ * The conversation as the stubbed server holds it. The spec pushes the
+ * persisted turn before it sends `done`, so the refetch after `done` finds
+ * the copy that replaces the streamed reply.
+ */
+export interface StubConversation {
+ id: string;
+ title: string;
+ messages: StoredMessage[];
+ /** `…/messages/{id}/results`, by message id. */
+ results: Record;
+ /** `…/messages/{id}/trail`, by message id. */
+ trails: Record;
+}
+
+export async function serveConversation(
+ page: Page,
+ conversation: StubConversation,
+): Promise {
+ const at = (index: number) =>
+ new Date(Date.UTC(2026, 9, 6, 9, 0, index)).toISOString();
+ const summary = () => ({
+ id: conversation.id,
+ title: conversation.title,
+ createdAt: at(0),
+ updatedAt: at(conversation.messages.length),
+ messageCount: conversation.messages.length,
+ fenced: false,
+ attachments: [],
+ documentTitle: null,
+ });
+ await page.route(
+ /\/api\/insights\/chat(?:\/[^?]+)?(?:\?.*)?$/,
+ async (route: Route, request: Request) => {
+ const url = new URL(request.url());
+ const base = `/api/insights/chat/${conversation.id}`;
+ const message = url.pathname.match(
+ /\/messages\/([^/]+)\/(results|trail)$/,
+ );
+ if (message) {
+ const [, id, kind] = message;
+ return kind === "results"
+ ? fulfilJson(route, { results: conversation.results[id] ?? [] })
+ : fulfilJson(route, {
+ trail: conversation.trails[id] ?? null,
+ ai: { available: true, reason: null },
+ });
+ }
+ if (url.pathname === base) {
+ return fulfilJson(route, {
+ ...summary(),
+ attachmentCount: 0,
+ summary: null,
+ messages: conversation.messages.map((m, index) => ({
+ id: m.id,
+ role: m.role,
+ content: m.content,
+ createdAt: at(index + 1),
+ metricSource: m.metricSource ?? null,
+ providerType:
+ m.role === "assistant" ? (m.providerType ?? "mock") : null,
+ promptVersion: null,
+ tokensUsed: m.role === "assistant" ? 420 : null,
+ model: m.role === "assistant" ? "mock" : null,
+ })),
+ });
+ }
+ if (url.pathname === "/api/insights/chat" && request.method() === "GET") {
+ return fulfilJson(route, {
+ conversations: conversation.messages.length > 0 ? [summary()] : [],
+ nextCursor: null,
+ });
+ }
+ return route.fallback();
+ },
+ );
+ await page.route("**/api/insights/coach/nudge-status*", (route) =>
+ fulfilJson(route, { nudgedAt: null, unread: false }),
+ );
+ await page.route("**/api/coach/about-me/questions*", (route) =>
+ fulfilJson(route, { questions: [] }),
+ );
+}
+
+export function done(conversationId: string, messageId: string) {
+ return {
+ type: "done",
+ conversationId,
+ messageId,
+ usage: { totalTokens: 420 },
+ };
+}
+
+export async function applyTheme(page: Page, theme: "light" | "dark") {
+ await page.emulateMedia({ colorScheme: theme, reducedMotion: "reduce" });
+ await page.addInitScript((value: string) => {
+ window.localStorage.setItem("healthlog-theme", value);
+ }, theme);
+}
+
+/** Midday, so no "today / yesterday" boundary moves a label. */
+export const FIXED_NOW = new Date("2026-10-06T12:00:00+02:00");
diff --git a/e2e/setup/day-doors-fixture.ts b/e2e/setup/day-doors-fixture.ts
new file mode 100644
index 000000000..ddc7b5382
--- /dev/null
+++ b/e2e/setup/day-doors-fixture.ts
@@ -0,0 +1,90 @@
+/**
+ * The record `day-doors.spec.ts` opens days from.
+ *
+ * Every chart the journey clicks needs points to click: ten days of the
+ * autonomic charge (the recovery page), of sleep and of mood (the sleep
+ * chart, the mood line and the mood calendar), weight over the last ten days
+ * and the same ten days a month earlier (the comparison chart draws a prior
+ * period only where one exists), and two readings of one lab marker.
+ *
+ * Written through the app's own routes, once, from `globalSetup`. An account
+ * that already holds the lab marker is taken as seeded, so a local re-run
+ * against a reused database does not double every row.
+ */
+import { request as playwrightRequest } from "@playwright/test";
+
+export const DAY_DOORS_ANALYTE = "Ferritin";
+
+export async function seedDayDoorsRecord(
+ baseURL: string,
+ storageStatePath: string,
+): Promise {
+ const ctx = await playwrightRequest.newContext({
+ baseURL,
+ storageState: storageStatePath,
+ });
+ try {
+ const labs = await ctx.get("/api/labs");
+ const body = (await labs.json()) as { data: unknown };
+ if (JSON.stringify(body.data ?? null).includes(DAY_DOORS_ANALYTE)) return;
+
+ const post = async (path: string, data: unknown): Promise => {
+ const res = await ctx.post(path, { data });
+ if (res.status() === 409 || res.ok()) return;
+ throw new Error(
+ `[day-doors-fixture] ${path} answered ${res.status()}: ${(await res.text()).slice(0, 200)}`,
+ );
+ };
+ const daysAgo = (days: number, hour = 8) => {
+ const at = new Date(Date.now() - days * 86_400_000);
+ at.setUTCHours(hour, 0, 0, 0);
+ return at.toISOString();
+ };
+
+ for (let day = 1; day <= 10; day += 1) {
+ await post("/api/measurements", {
+ type: "ANS_CHARGE",
+ value: 5 + day,
+ measuredAt: daysAgo(day, 5),
+ });
+ await post("/api/measurements", {
+ type: "SLEEP_DURATION",
+ value: 400 + day * 4,
+ measuredAt: daysAgo(day, 6),
+ });
+ await post("/api/measurements", {
+ type: "WEIGHT",
+ value: 72 + day / 10,
+ measuredAt: daysAgo(day, 7),
+ });
+ // The same ten days, a month before: the prior period the comparison
+ // draws beside them.
+ await post("/api/measurements", {
+ type: "WEIGHT",
+ value: 73 + day / 10,
+ measuredAt: daysAgo(day + 30, 7),
+ });
+ await post("/api/mood-entries", {
+ mood: day % 2 ? "GUT" : "OKAY",
+ moodLoggedAt: daysAgo(day, 19),
+ });
+ }
+
+ for (const [days, value] of [
+ [60, 41],
+ [4, 58],
+ ] as const) {
+ await post("/api/labs", {
+ analyte: DAY_DOORS_ANALYTE,
+ panel: "Eisenstoffwechsel",
+ value,
+ unit: "ng/mL",
+ referenceLow: 30,
+ referenceHigh: 300,
+ takenAt: daysAgo(days),
+ });
+ }
+ } finally {
+ await ctx.dispose();
+ }
+}
diff --git a/e2e/setup/day-mock.ts b/e2e/setup/day-mock.ts
new file mode 100644
index 000000000..9a768cce9
--- /dev/null
+++ b/e2e/setup/day-mock.ts
@@ -0,0 +1,125 @@
+/**
+ * The day routes, answered in the contract's shape.
+ *
+ * The day itself (`GET /api/day/{date}`) and the index (`GET /api/day/index`)
+ * are route mocks, so a journey pins the client: which door opens which day,
+ * what the history does, where no door is. The server's own answers are
+ * pinned by the integration suite.
+ */
+import type { Page, Route } from "@playwright/test";
+
+function dayFixture(date: string) {
+ return {
+ date,
+ tz: "Europe/Berlin",
+ counts: { values: 2, entries: 1 },
+ running: [
+ {
+ kind: "illness",
+ section: "illness",
+ id: "ep1",
+ title: "Common cold",
+ sub: null,
+ since: date,
+ until: null,
+ dayIndex: 1,
+ dayCount: null,
+ href: "/illness",
+ },
+ ],
+ values: [
+ {
+ type: "BLOOD_PRESSURE_SYS",
+ value: 124,
+ unit: "mmHg",
+ at: `${date}T07:00:00.000Z`,
+ source: "MANUAL",
+ band: { lo: 118, hi: 128, n: 20 },
+ },
+ {
+ type: "BLOOD_PRESSURE_DIA",
+ value: 79,
+ unit: "mmHg",
+ at: `${date}T07:00:00.000Z`,
+ source: "MANUAL",
+ band: { lo: 76, hi: 82, n: 20 },
+ },
+ ],
+ events: [
+ {
+ at: `${date}T07:00:00.000Z`,
+ kind: "mood",
+ section: "mood",
+ id: "m1",
+ title: "Mood: good",
+ meta: null,
+ note: null,
+ docs: [],
+ href: "/mood",
+ },
+ ],
+ notable: [],
+ scores: [
+ {
+ key: "healthScore",
+ value: 74,
+ max: 100,
+ source: "COMPUTED",
+ band: { lo: 66, hi: 78, n: 30 },
+ },
+ { key: "strain", value: 12.4, max: 21, source: "WHOOP", band: null },
+ ],
+ sections: {},
+ };
+}
+
+/**
+ * Every day from `from` to `to`, each holding values: what the row of day
+ * dots under a chart reads when the whole window has entries.
+ */
+function everyDay(from: string, to: string): Record {
+ const days: Record = {};
+ if (!from || !to) return days;
+ const at = new Date(`${from}T12:00:00.000Z`);
+ const end = new Date(`${to}T12:00:00.000Z`);
+ while (at <= end) {
+ days[at.toISOString().slice(0, 10)] = ["values"];
+ at.setUTCDate(at.getUTCDate() + 1);
+ }
+ return days;
+}
+
+export async function mockDay(
+ page: Page,
+ options: { index?: "empty" | "full" } = {},
+) {
+ await page.route("**/api/day/**", async (route: Route) => {
+ const url = new URL(route.request().url());
+ if (url.pathname === "/api/day/index") {
+ const from = url.searchParams.get("from") ?? "";
+ const to = url.searchParams.get("to") ?? "";
+ const days = options.index === "full" ? everyDay(from, to) : {};
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ from,
+ to,
+ days,
+ // The first day of the window stands out: the rug draws a ring.
+ notable: Object.keys(days).slice(0, 1),
+ },
+ error: null,
+ }),
+ });
+ return;
+ }
+ const date = url.pathname.split("/").pop() ?? "";
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data: dayFixture(date), error: null }),
+ });
+ });
+}
diff --git a/e2e/setup/global-setup.ts b/e2e/setup/global-setup.ts
index 6f28780bb..9eeb1d669 100644
--- a/e2e/setup/global-setup.ts
+++ b/e2e/setup/global-setup.ts
@@ -33,6 +33,7 @@ import {
} from "@playwright/test";
import pg from "pg";
+import { seedDayDoorsRecord } from "./day-doors-fixture";
import { seedMobileRoutesRecord } from "./mobile-routes-fixture";
// Resolve relative to the project root rather than __dirname/import.meta.url —
@@ -229,6 +230,65 @@ export const E2E_AI_OPTIONAL = {
role: "USER",
} as const;
+/**
+ * The timeline journey's own account (`timeline.spec.ts`).
+ *
+ * It switches the opt-in timeline module on and off, which changes the
+ * navigation, the add menus and Settings for the whole account; on the shared
+ * account a parallel spec would see them move underneath it. The module
+ * starts off here, the way every account starts.
+ */
+export const E2E_TIMELINE = {
+ email: "e2e-timeline@healthlog.test",
+ username: "e2e-timeline",
+ password: "Tl6!Wq3zRm8vXn2K",
+ role: "USER",
+} as const;
+
+export const TIMELINE_STORAGE_STATE_PATH = resolve(
+ process.cwd(),
+ "e2e/setup/storageStateTimeline.json",
+);
+
+/**
+ * The day-doors journey's own account (`day-doors.spec.ts`).
+ *
+ * It turns a chart's comparison overlay on, which is a per-account
+ * preference; on a shared account a parallel spec would find its chart
+ * drawing a prior period it never asked for. Its record carries the series
+ * the journey clicks (recovery, sleep, mood, weight, a lab marker).
+ */
+export const E2E_DAY_DOORS = {
+ email: "e2e-day-doors@healthlog.test",
+ username: "e2e-day-doors",
+ password: "Dy4!Rk8wQm2vTz6P",
+ role: "USER",
+} as const;
+
+export const DAY_DOORS_STORAGE_STATE_PATH = resolve(
+ process.cwd(),
+ "e2e/setup/storageStateDayDoors.json",
+);
+
+/**
+ * The score-history journey's own account (`score-history.spec.ts`).
+ *
+ * It switches the dashboard's top card off and back on and remembers a range
+ * tab per score page, both per-account preferences; on a shared account a
+ * parallel spec would find its dashboard without the card it asserts.
+ */
+export const E2E_SCORE_HISTORY = {
+ email: "e2e-score-history@healthlog.test",
+ username: "e2e-score-history",
+ password: "Sh7!Vn2qLp9xWc4R",
+ role: "USER",
+} as const;
+
+export const SCORE_HISTORY_STORAGE_STATE_PATH = resolve(
+ process.cwd(),
+ "e2e/setup/storageStateScoreHistory.json",
+);
+
export const AI_OPTIONAL_STORAGE_STATE_PATH = resolve(
process.cwd(),
"e2e/setup/storageStateAiOptional.json",
@@ -906,6 +966,80 @@ export default async function globalSetup(config: FullConfig): Promise {
],
);
+ // The timeline journey's account. The module starts off; the journey
+ // switches it through the real Settings switch.
+ await pool.query(
+ `INSERT INTO users
+ (id, username, email, password_hash, role, created_at, updated_at,
+ onboarding_completed_at, onboarding_tour_completed,
+ module_preferences_json)
+ VALUES ($1, $2, $3, $4, 'USER', $5, $5, $5, true, NULL)
+ ON CONFLICT (username) DO UPDATE SET
+ email = EXCLUDED.email,
+ password_hash = EXCLUDED.password_hash,
+ updated_at = EXCLUDED.updated_at,
+ onboarding_completed_at = EXCLUDED.onboarding_completed_at,
+ onboarding_tour_completed = EXCLUDED.onboarding_tour_completed,
+ module_preferences_json = NULL`,
+ [
+ cuid(),
+ E2E_TIMELINE.username,
+ E2E_TIMELINE.email,
+ await hashPassword(E2E_TIMELINE.password),
+ now,
+ ],
+ );
+
+ // The day-doors journey's account. Every module the journey reaches is
+ // on; its record is written after the login capture.
+ await pool.query(
+ `INSERT INTO users
+ (id, username, email, password_hash, role, created_at, updated_at,
+ onboarding_completed_at, onboarding_tour_completed,
+ module_preferences_json)
+ VALUES ($1, $2, $3, $4, 'USER', $5, $5, $5, true, NULL)
+ ON CONFLICT (username) DO UPDATE SET
+ email = EXCLUDED.email,
+ password_hash = EXCLUDED.password_hash,
+ updated_at = EXCLUDED.updated_at,
+ onboarding_completed_at = EXCLUDED.onboarding_completed_at,
+ onboarding_tour_completed = EXCLUDED.onboarding_tour_completed,
+ module_preferences_json = NULL`,
+ [
+ cuid(),
+ E2E_DAY_DOORS.username,
+ E2E_DAY_DOORS.email,
+ await hashPassword(E2E_DAY_DOORS.password),
+ now,
+ ],
+ );
+
+ // The score-history journey's account. Every module on, a fresh layout:
+ // the journey moves its top card and range tabs, so a re-run starts from
+ // the defaults.
+ await pool.query(
+ `INSERT INTO users
+ (id, username, email, password_hash, role, created_at, updated_at,
+ onboarding_completed_at, onboarding_tour_completed,
+ module_preferences_json)
+ VALUES ($1, $2, $3, $4, 'USER', $5, $5, $5, true, NULL)
+ ON CONFLICT (username) DO UPDATE SET
+ email = EXCLUDED.email,
+ password_hash = EXCLUDED.password_hash,
+ updated_at = EXCLUDED.updated_at,
+ onboarding_completed_at = EXCLUDED.onboarding_completed_at,
+ onboarding_tour_completed = EXCLUDED.onboarding_tour_completed,
+ module_preferences_json = NULL,
+ dashboard_widgets_json = NULL`,
+ [
+ cuid(),
+ E2E_SCORE_HISTORY.username,
+ E2E_SCORE_HISTORY.email,
+ await hashPassword(E2E_SCORE_HISTORY.password),
+ now,
+ ],
+ );
+
// The notification-dispatch journey's account. Seeded like the others;
// its channels, devices, ledger rows and preferences are reset by
// `e2e/setup/notification-fixture.ts` before every test, because the
@@ -1509,6 +1643,16 @@ export default async function globalSetup(config: FullConfig): Promise {
// The AI-optional journeys' jar — see `E2E_AI_OPTIONAL`.
await capture(E2E_AI_OPTIONAL, AI_OPTIONAL_STORAGE_STATE_PATH);
+ // The timeline journey's jar — see `E2E_TIMELINE`.
+ await capture(E2E_TIMELINE, TIMELINE_STORAGE_STATE_PATH);
+
+ // The day-doors journey's jar, then its record — see `E2E_DAY_DOORS`.
+ await capture(E2E_DAY_DOORS, DAY_DOORS_STORAGE_STATE_PATH);
+ await seedDayDoorsRecord(baseURL, DAY_DOORS_STORAGE_STATE_PATH);
+
+ // The score-history journey's jar — see `E2E_SCORE_HISTORY`.
+ await capture(E2E_SCORE_HISTORY, SCORE_HISTORY_STORAGE_STATE_PATH);
+
await capture(E2E_SCOPE_DELEGATE, SCOPE_DELEGATE_STORAGE_STATE_PATH);
await capture(E2E_SCOPE_DELEGATE, SCOPE_A11Y_STORAGE_STATE_PATH);
await capture(E2E_SCOPE_DELEGATE, SCOPE_CAPABILITIES_STORAGE_STATE_PATH);
diff --git a/e2e/setup/vault-fixture.ts b/e2e/setup/vault-fixture.ts
index 79d2e41a1..60ce0b522 100644
--- a/e2e/setup/vault-fixture.ts
+++ b/e2e/setup/vault-fixture.ts
@@ -550,6 +550,81 @@ export async function seedNamespaceDocs(
}
}
+/** The procedure-filter fixture: two procedures, each with its own files. */
+export const PROCEDURE_KNEE_ID = "e2eprocknee0000000000001";
+export const PROCEDURE_APPENDIX_ID = "e2eprocappx0000000000001";
+export const PROCEDURE_DOC_PREFIX = "procfilter";
+
+/**
+ * Seed the procedure-filter fixture: three namespaced documents and two
+ * procedures — a knee arthroscopy (reason + body site + side) holding two of
+ * them, and an appendectomy holding the third. Dated far from the MRT report
+ * so the visits spec's ±7-day window never sees them. Idempotent: the rows
+ * upsert by fixed id and come back live, so a `resetEncounters` from the visits
+ * spec between runs is repaired by the next call.
+ */
+export async function ensureProcedureFixture(): Promise {
+ const url = process.env.DATABASE_URL;
+ if (!url) throw new Error("[vault-fixture] DATABASE_URL is not set");
+ await seedNamespaceDocs(PROCEDURE_DOC_PREFIX, 3);
+ const pool = new pg.Pool({ connectionString: url });
+ try {
+ const userId = await getUserId(pool);
+ const procedures = [
+ {
+ id: PROCEDURE_KNEE_ID,
+ at: "2024-03-12T08:00:00Z",
+ reason: "Knee arthroscopy",
+ site: "Knee",
+ side: "LEFT",
+ docs: [0, 1],
+ },
+ {
+ id: PROCEDURE_APPENDIX_ID,
+ at: "2023-06-20T08:00:00Z",
+ reason: "Appendectomy",
+ site: null,
+ side: null,
+ docs: [2],
+ },
+ ];
+ for (const p of procedures) {
+ await pool.query(
+ `INSERT INTO encounters
+ (id, user_id, occurred_at, status, kind, reason_encrypted,
+ body_site_encrypted, laterality, created_at, updated_at)
+ VALUES ($1, $2, $3, 'DONE', 'PROCEDURE', $4, $5, $6::laterality, NOW(), NOW())
+ ON CONFLICT (id) DO UPDATE SET
+ occurred_at = EXCLUDED.occurred_at, kind = 'PROCEDURE',
+ status = 'DONE', reason_encrypted = EXCLUDED.reason_encrypted,
+ body_site_encrypted = EXCLUDED.body_site_encrypted,
+ laterality = EXCLUDED.laterality, deleted_at = NULL,
+ updated_at = NOW()`,
+ [
+ p.id,
+ userId,
+ p.at,
+ encryptStringToBytes(p.reason),
+ p.site ? encryptStringToBytes(p.site) : null,
+ p.side,
+ ],
+ );
+ for (const n of p.docs) {
+ const docId = `e2e${PROCEDURE_DOC_PREFIX}${String(n).padStart(8, "0")}`;
+ await pool.query(
+ `INSERT INTO encounter_document_links
+ (id, encounter_id, document_id, user_id, created_at)
+ VALUES ($1, $2, $3, $4, NOW())
+ ON CONFLICT (encounter_id, document_id) DO NOTHING`,
+ [`${p.id.slice(0, 16)}lnk${n}`, p.id, docId, userId],
+ );
+ }
+ }
+ } finally {
+ await pool.end();
+ }
+}
+
/**
* v1.28 — seed the clinician-share document trio (see the `SHARE_*_DOC_ID`
* constants). Enables the vault module for the e2e user and upserts one
diff --git a/e2e/sidebar-expand-no-scrollbar.spec.ts b/e2e/sidebar-expand-no-scrollbar.spec.ts
new file mode 100644
index 000000000..cb136e1df
--- /dev/null
+++ b/e2e/sidebar-expand-no-scrollbar.spec.ts
@@ -0,0 +1,175 @@
+import { expect, test } from "./setup/test";
+
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+
+/**
+ * Expanding the sidebar must never flash a scrollbar on the nav list.
+ *
+ * The rail animates `width` from `w-16` to `w-64`. When the labels laid out
+ * against the growing width, every row overran the 48 px nav and the
+ * multi-word labels wrapped, so the nav's `overflow-y-auto` painted a
+ * vertical and a horizontal scrollbar until the transition ended. The fix
+ * lays the column out at its final width from the first frame and lets the
+ * rail clip it (see `sidebar-column` in `src/components/layout/sidebar-nav.tsx`).
+ *
+ * This samples the nav on every animation frame of a real expand and a real
+ * collapse, so it fails on the overflow itself rather than on a class name.
+ * Desktop only: the sidebar is hidden below `md`.
+ */
+test.describe("sidebar width transition", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ test.beforeEach(({}, testInfo) => {
+ test.skip(
+ testInfo.project.name === "chromium-mobile",
+ "global sidebar is desktop-only (md+)",
+ );
+ });
+
+ test("the nav list never overflows while the rail expands or collapses", async ({
+ page,
+ }) => {
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ const toggle = page.locator('[data-slot="sidebar-collapse-toggle"]');
+ await expect(toggle).toBeVisible();
+ const nav = page.locator("aside nav").first();
+ await expect(nav).toBeVisible();
+
+ async function sampleToggle() {
+ const heightBefore = await nav.evaluate((el) => el.scrollHeight);
+ await page.evaluate(() => {
+ const el = document.querySelector("aside nav") as HTMLElement;
+ const samples: { sw: number; cw: number; sh: number }[] = [];
+ (window as unknown as { __navSamples: typeof samples }).__navSamples =
+ samples;
+ const t0 = performance.now();
+ const tick = () => {
+ samples.push({
+ sw: el.scrollWidth,
+ cw: el.clientWidth,
+ sh: el.scrollHeight,
+ });
+ if (performance.now() - t0 < 600) requestAnimationFrame(tick);
+ };
+ requestAnimationFrame(tick);
+ });
+ await toggle.click();
+ await page.waitForTimeout(700);
+ const samples = await page.evaluate(
+ () =>
+ (
+ window as unknown as {
+ __navSamples: { sw: number; cw: number; sh: number }[];
+ }
+ ).__navSamples,
+ );
+ const heightAfter = await nav.evaluate((el) => el.scrollHeight);
+ // Expanded rows are taller than rail rows, so the list may legitimately
+ // be either rest height while the width moves, never more.
+ return { samples, restHeight: Math.max(heightBefore, heightAfter) };
+ }
+
+ // Two round trips, so the run covers both directions whichever state
+ // the stored preference started in.
+ for (let i = 0; i < 4; i++) {
+ const { samples, restHeight } = await sampleToggle();
+ // A 200 ms transition yields several frames; too few means the
+ // sampler never ran and the assertions below would prove nothing.
+ expect(samples.length).toBeGreaterThan(5);
+ for (const s of samples) {
+ // No horizontal overflow: no row is wider than the nav.
+ expect(s.sw).toBeLessThanOrEqual(s.cw + 1);
+ }
+ // No vertical growth: no label wraps mid-transition, so the list is
+ // never taller than at rest and cannot summon a scrollbar.
+ const tallest = Math.max(...samples.map((s) => s.sh));
+ expect(tallest).toBeLessThanOrEqual(restHeight + 1);
+ }
+ });
+
+ test("on a short window the list scrolls behind a visible edge, never under the footer", async ({
+ page,
+ }) => {
+ // Short enough that the list cannot fit between the logo band and the
+ // footer group. The rail used to cut the last visible entry off right at
+ // the Collapse row with nothing marking the edge, which read as the
+ // footer covering the entry.
+ await page.setViewportSize({ width: 1280, height: 600 });
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ const nav = page.locator("aside nav").first();
+ const footer = page.locator('aside [data-slot="sidebar-footer"]');
+ await expect(nav).toBeVisible();
+ await expect(footer).toHaveAttribute("data-nav-overflows", "true");
+ const borderTop = await footer.evaluate(
+ (el) => getComputedStyle(el).borderTopWidth,
+ );
+ expect(borderTop).toBe("1px");
+
+ // Scrolled to its end, the last entry sits wholly inside the list,
+ // above the footer, and is the element under its own centre.
+ const end = await nav.evaluate((el) => {
+ el.scrollTop = el.scrollHeight;
+ const links = [...el.querySelectorAll("a")];
+ const last = links[links.length - 1] as HTMLElement;
+ const r = last.getBoundingClientRect();
+ const navBox = el.getBoundingClientRect();
+ const footerTop = (
+ document.querySelector('aside [data-slot="sidebar-footer"]') as Element
+ ).getBoundingClientRect().top;
+ const hit = document.elementFromPoint(
+ r.left + r.width / 2,
+ r.top + r.height / 2,
+ );
+ return {
+ insideList: r.bottom <= navBox.bottom + 0.5,
+ aboveFooter: r.bottom <= footerTop + 0.5,
+ hitsItself: last.contains(hit),
+ };
+ });
+ expect(end).toEqual({
+ insideList: true,
+ aboveFooter: true,
+ hitsItself: true,
+ });
+
+ // With room to spare there is no edge: the default rail is unchanged.
+ // No native scrollbar in the rail (a classic one with arrow buttons
+ // read as a foreign body); a fade marks the edge that has more.
+ expect(
+ await nav.evaluate((el) => getComputedStyle(el).scrollbarWidth),
+ ).toBe("none");
+ expect(
+ await nav.evaluate(
+ (el) => (el as HTMLElement).offsetWidth - el.clientWidth,
+ ),
+ ).toBe(0);
+ await expect(nav).toHaveAttribute("data-fade-top", "true");
+ await expect(nav).not.toHaveAttribute("data-fade-bottom", "true");
+ await nav.evaluate((el) => {
+ el.scrollTop = 0;
+ });
+ await expect(nav).toHaveAttribute("data-fade-bottom", "true");
+ await expect(nav).not.toHaveAttribute("data-fade-top", "true");
+
+ await page.setViewportSize({ width: 1280, height: 1400 });
+ await expect(footer).not.toHaveAttribute("data-nav-overflows", "true");
+ await expect(nav).not.toHaveAttribute("data-fade-bottom", "true");
+ });
+
+ test("a 1366 × 768 window fits the rail without scrolling, collapsed and expanded", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1366, height: 768 });
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ const nav = page.locator("aside nav").first();
+ const toggle = page.locator('[data-slot="sidebar-collapse-toggle"]');
+ await expect(nav).toBeVisible();
+ for (let i = 0; i < 2; i++) {
+ await expect
+ .poll(() => nav.evaluate((el) => el.scrollHeight - el.clientHeight))
+ .toBeLessThanOrEqual(0);
+ await toggle.click();
+ await page.waitForTimeout(300);
+ }
+ });
+});
diff --git a/e2e/timeline.spec.ts b/e2e/timeline.spec.ts
new file mode 100644
index 000000000..39b4d40fb
--- /dev/null
+++ b/e2e/timeline.spec.ts
@@ -0,0 +1,1007 @@
+/**
+ * The timeline journey (v1.42, #613), on its own account (`E2E_TIMELINE`).
+ *
+ * 1. Off by default: no navigation entry, and a deep link to /timeline
+ * lands on the module notice, not the page.
+ * 2. Switching it on in Settings opens the readiness inventory; "Open
+ * timeline" goes there and the navigation entry appears.
+ * 3. The timeline hands off to the day: "Open 3 Jan." and Enter on the
+ * chart both put `?day=` in the URL; an incoming `?day=` selects that
+ * day.
+ * 4. A life event is entered through the sheet and posted in the
+ * contract's shape.
+ * 5. The value lines: quarterly means over the years, a missing quarter
+ * left out, a short gap bridged dashed, a thin quarter hollow, and up
+ * to six lines.
+ * 6. On a phone the chronicle replaces the chart, says its empty months
+ * out loud, names each quarter's means once, and a row opens its day.
+ *
+ * The timeline's server routes are mocked with the contract's shapes
+ * (`utils/mock-timeline.ts`); the module switch is the real one. Every
+ * assertion is addressed to a stable `data-*` attribute. The journey moves
+ * its account's module switch, so it runs serially and in one project.
+ */
+import type { Page } from "@playwright/test";
+
+import { TIMELINE_STORAGE_STATE_PATH } from "./setup/global-setup";
+import { expect, test } from "./setup/test";
+import { SYS_MISSING, SYS_THIN, mockTimeline } from "./utils/mock-timeline";
+
+test.use({ storageState: TIMELINE_STORAGE_STATE_PATH });
+test.describe.configure({ mode: "serial" });
+
+/** Today in the zone the browser and the account run in. */
+const TODAY = new Intl.DateTimeFormat("en-CA", {
+ timeZone: "Europe/Berlin",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+}).format(new Date());
+
+async function setTimelineModule(page: Page, on: boolean): Promise {
+ const status = await page.evaluate(async (on) => {
+ const res = await fetch("/api/auth/me/modules", {
+ method: "PATCH",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ timeline: on }),
+ });
+ return res.status;
+ }, on);
+ expect(status).toBe(200);
+}
+
+const navEntry = '[data-tour-id="nav-timeline"]';
+
+test("is off by default: no navigation entry, a deep link meets the notice", async ({
+ page,
+}) => {
+ await page.goto("/settings/modules");
+ await setTimelineModule(page, false);
+ await page.goto("/timeline");
+ await expect(
+ page.locator(
+ '[data-slot="module-disabled-notice"][data-module="timeline"]',
+ ),
+ ).toBeVisible({ timeout: 20_000 });
+ await expect(page.locator('[data-slot="timeline-page"]')).toHaveCount(0);
+ await expect(page.locator(navEntry)).toHaveCount(0);
+});
+
+test("opens on three months when the URL names no zoom", async ({ page }) => {
+ await mockTimeline(page, "ready", TODAY);
+ await page.goto("/settings/modules");
+ await setTimelineModule(page, true);
+ await page.goto("/timeline");
+ await expect(
+ page.locator('[data-slot="timeline-zoom"] [data-value="quarter"]'),
+ ).toHaveAttribute("aria-checked", "true", { timeout: 20_000 });
+ // The next test starts from the module switched off.
+ await setTimelineModule(page, false);
+});
+
+test("only old data opens on everything, and an empty window offers it", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ const log = await mockTimeline(page, "full", TODAY, { onlyOld: true });
+ await page.goto("/timeline");
+ // The default three months hold nothing: the page opens on everything,
+ // without writing it into the URL.
+ await expect(
+ page.locator('[data-slot="timeline-zoom"] [data-value="all"]'),
+ ).toHaveAttribute("aria-checked", "true", { timeout: 20_000 });
+ await expect(page.locator('[data-slot="timeline-chart"]')).toBeVisible();
+ await expect(page).toHaveURL(/\/timeline$/);
+ expect(log.timelineQueries.map((q) => q.get("zoom"))).toEqual(
+ expect.arrayContaining(["quarter", "all"]),
+ );
+
+ // Three months asked for by name stay three months, and offer the rest.
+ await page.goto("/timeline?zoom=quarter");
+ const empty = page.locator('[data-slot="timeline-window-empty"]');
+ await expect(empty).toBeVisible({ timeout: 20_000 });
+ await page.locator('[data-slot="timeline-show-everything"]').click();
+ await expect(page).toHaveURL(/zoom=all/);
+ await expect(page.locator('[data-slot="timeline-chart"]')).toBeVisible();
+
+ // The next test starts from the module switched off.
+ await setTimelineModule(page, false);
+});
+
+test("a young record opens on three months starting at its first entry", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ // The record starts ten days ago: the window spans the four-week minimum.
+ const day = (offset: number) => {
+ const [y, m, d] = TODAY.split("-").map(Number);
+ return new Date(Date.UTC(y, m - 1, d + offset)).toISOString().slice(0, 10);
+ };
+ await mockTimeline(page, "full", TODAY, { dataFrom: day(-10) });
+ await page.goto("/timeline");
+ await expect(
+ page.locator('[data-slot="timeline-zoom"] [data-value="quarter"]'),
+ ).toHaveAttribute("aria-checked", "true", { timeout: 20_000 });
+ await expect(page.locator('[data-slot="timeline-chart"]')).toHaveAttribute(
+ "data-window-from",
+ day(-27),
+ );
+ // The next test starts from the module switched off.
+ await setTimelineModule(page, false);
+});
+
+test("switching it on opens the readiness inventory, which leads to the timeline", async ({
+ page,
+}) => {
+ await mockTimeline(page, "ready", TODAY);
+ await page.goto("/settings/modules");
+ const toggle = page.locator("#module-toggle-timeline");
+ await expect(toggle).toHaveAttribute("aria-checked", "false", {
+ timeout: 20_000,
+ });
+ await toggle.click();
+
+ const sheet = page.locator('[data-slot="timeline-readiness-sheet"]');
+ await expect(sheet).toBeVisible();
+ await expect(
+ page.locator(
+ '[data-slot="timeline-readiness-verdict"][data-verdict="carries"]',
+ ),
+ ).toBeVisible();
+ await expect(
+ page.locator('[data-slot="timeline-readiness-lane"][data-status="thin"]'),
+ ).toHaveCount(1);
+ await expect(
+ page.locator(
+ '[data-slot="timeline-readiness-gap"][data-gap="lifeEventsEmpty"]',
+ ),
+ ).toHaveAttribute("href", "/timeline?add=lifeEvent");
+
+ await page.locator('[data-slot="timeline-readiness-open"]').click();
+ await expect(page).toHaveURL(/\/timeline$/);
+ await expect(page.locator('[data-slot="timeline-page"]')).toBeVisible();
+ await expect(page.locator(navEntry).first()).toBeAttached();
+ // The card carries the open gaps onto the page until it is closed.
+ await expect(
+ page.locator('[data-slot="timeline-readiness-card"]'),
+ ).toBeVisible();
+ await page.locator('[data-slot="timeline-readiness-card-dismiss"]').click();
+ await expect(
+ page.locator('[data-slot="timeline-readiness-card"]'),
+ ).toHaveCount(0);
+});
+
+test("the timeline hands the selected day to the day view through ?day=", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ const log = await mockTimeline(page, "full", TODAY);
+ await page.goto("/timeline?zoom=all");
+
+ const chart = page.locator('[data-slot="timeline-chart"]');
+ await expect(chart).toBeVisible({ timeout: 20_000 });
+ // Empty lanes are not drawn: the record has no labs.
+ await expect(page.locator('svg [data-lane="labs"]')).toHaveCount(0);
+ await expect(page.locator('svg [data-lane="illness"]')).toHaveCount(1);
+ await expect(
+ page.locator('[data-slot="timeline-legend-causality"]'),
+ ).toHaveCount(0);
+ const asked = log.timelineQueries.at(-1)?.get("values")?.split(",") ?? [];
+ expect(asked.length).toBeGreaterThan(0);
+
+ // Before anyone picks a day the bar opens on the newest bucket it has
+ // something for, says so in its head, and says how to pick another.
+ const bar = page.locator('[data-slot="timeline-selection-bar"]');
+ // It sits on the card's own surface under a hairline, not in a grey box,
+ // and the legend under it is set off by a hairline of its own.
+ const surface = await bar.evaluate((el) => {
+ const style = getComputedStyle(el);
+ return {
+ background: style.backgroundColor,
+ borderTop: style.borderTopWidth,
+ };
+ });
+ expect(surface.background).toBe("rgba(0, 0, 0, 0)");
+ expect(surface.borderTop).toBe("1px");
+ await expect(page.locator('[data-slot="timeline-legend"]')).toHaveCSS(
+ "border-top-width",
+ "1px",
+ );
+ await expect(bar).toHaveAttribute("data-bucket", "quarter");
+ await expect(bar).toHaveAttribute(
+ "data-period-from",
+ /^\d{4}-(01|04|07|10)-01$/,
+ );
+ await expect(
+ page.locator('[data-slot="timeline-selection-empty"]'),
+ ).toHaveCount(0);
+ await expect(
+ page.locator('[data-slot="timeline-selection-hint"]'),
+ ).toBeVisible();
+
+ // The keyboard: one step back and Enter opens that day.
+ const before = await page
+ .locator('[data-slot="timeline-selection"]')
+ .getAttribute("data-date");
+ await chart.focus();
+ await page.keyboard.press("ArrowLeft");
+ const after = await page
+ .locator('[data-slot="timeline-selection"]')
+ .getAttribute("data-date");
+ expect(after).not.toBe(before);
+ // A day is picked now: the hint has done its job.
+ await expect(
+ page.locator('[data-slot="timeline-selection-hint"]'),
+ ).toHaveCount(0);
+ await page.keyboard.press("Enter");
+ await expect(page).toHaveURL(new RegExp(`[?&]day=${after}`));
+
+ // An incoming ?day= selects that day; the selection bar has no separate
+ // "Open " link, a chip opens the day it touches.
+ await page.goto("/timeline?day=2026-01-03");
+ await expect(
+ page.locator('[data-slot="timeline-selection"]'),
+ ).toHaveAttribute("data-date", "2026-01-03", { timeout: 20_000 });
+ await expect(
+ page.locator('[data-slot="timeline-zoom"] [data-value="quarter"]'),
+ ).toHaveAttribute("aria-checked", "true");
+ await expect(page.locator('[data-slot="timeline-open-day"]')).toHaveCount(0);
+ const chip = page.locator(
+ '[data-slot="timeline-selection-chip"][data-item="ill-5"]',
+ );
+ await expect(chip).toBeVisible();
+ await chip.click();
+ await expect(page).toHaveURL(/[?&]day=\d{4}-\d{2}-\d{2}/);
+});
+
+test("value lines: quarterly means, gaps kept, short ones bridged, as many as the record has", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ const log = await mockTimeline(page, "full", TODAY);
+ await page.goto("/timeline?zoom=all");
+ const sys = page.locator('svg [data-series="BLOOD_PRESSURE_SYS"]');
+ await expect(sys).toBeVisible({ timeout: 20_000 });
+ await expect(
+ page.locator('[data-slot="timeline-legend-mean"]'),
+ ).toHaveAttribute("data-bucket", "quarter");
+
+ const point = (t: string) =>
+ sys.locator(`[data-slot="timeline-series-point"][data-t="${t}"]`);
+ // A missing quarter stays missing: no point is drawn for it.
+ for (const t of SYS_MISSING) await expect(point(t)).toHaveCount(0);
+ // The quarters around Q2 2023 are joined by a dashed bridge; the three
+ // missing quarters of 2021 are not (one bridge in the whole line).
+ await expect(sys.locator('[data-slot="timeline-series-bridge"]')).toHaveCount(
+ 1,
+ );
+ await expect(point("2023-01-01")).toHaveCount(1);
+ await expect(point("2023-07-01")).toHaveCount(1);
+ // Two readings: drawn hollow; four and more: filled.
+ await expect(point(SYS_THIN)).toHaveAttribute("data-thin", "true");
+ await expect(point("2023-01-01")).toHaveAttribute("data-thin", "false");
+
+ // The value table keeps the missing quarter as a row of its own.
+ await expect(
+ page.locator(
+ '[data-slot="timeline-series-table"] [data-bucket="2023-04-01"]',
+ ),
+ ).toBeAttached();
+
+ // The legend explains the dashed stroke and the hollow point.
+ await expect(
+ page.locator('[data-slot="timeline-legend-gap"] svg'),
+ ).toBeVisible();
+ await expect(
+ page.locator('[data-slot="timeline-legend-thin"] svg'),
+ ).toBeVisible();
+
+ // The picker is one button that counts the lines; the chosen ones are
+ // not listed beside it.
+ const trigger = page.locator('[data-slot="timeline-values-trigger"]');
+ await expect(trigger).toHaveAttribute("data-count", "3");
+ await expect(trigger).toHaveText(/\(3\)/);
+ const toolbarHeight = await trigger.evaluate(
+ (el) => el.parentElement!.getBoundingClientRect().height,
+ );
+
+ // No cap: every series the record has can be chosen, fifteen here.
+ await trigger.click();
+ const options = page.locator('[data-slot="timeline-values-option"]');
+ await expect(options.first()).toBeVisible();
+ const count = await options.count();
+ expect(count).toBe(15);
+ await expect(
+ page.locator('[data-slot="timeline-values-option"][data-disabled]'),
+ ).toHaveCount(0);
+ for (let i = 0; i < count; i++) {
+ const option = options.nth(i);
+ if ((await option.getAttribute("aria-checked")) === "true") continue;
+ await option.click();
+ }
+ await expect(
+ page.locator('[data-slot="timeline-values-option"][aria-checked="true"]'),
+ ).toHaveCount(15);
+ // The menu marks each chosen line with its colour.
+ const dots = await page
+ .locator(
+ '[data-slot="timeline-values-option"][aria-checked="true"] [data-slot="timeline-values-dot"]',
+ )
+ .evaluateAll((els) =>
+ els.map((el) => getComputedStyle(el).backgroundColor),
+ );
+ expect(dots).toHaveLength(15);
+ await page.keyboard.press("Escape");
+ await expect
+ .poll(() => log.timelineQueries.at(-1)?.get("values")?.split(",").length)
+ .toBe(15);
+ await expect(page.locator("svg [data-series]")).toHaveCount(15);
+ await expect(trigger).toHaveAttribute("data-count", "15");
+ // The toolbar did not grow with the choice.
+ expect(
+ await trigger.evaluate(
+ (el) => el.parentElement!.getBoundingClientRect().height,
+ ),
+ ).toBe(toolbarHeight);
+ // Each line in one colour, every mark of a line in that colour, the five
+ // data colours repeating past the fifth line.
+ const colours = await page
+ .locator("svg [data-series]")
+ .evaluateAll((groups) =>
+ groups.map((g) => ({
+ colour: getComputedStyle(g).color,
+ marks: [
+ ...g.querySelectorAll(
+ '[data-slot="timeline-series-line"], [data-slot="timeline-series-swatch"]',
+ ),
+ ].map((m) => getComputedStyle(m).stroke),
+ })),
+ );
+ expect(new Set(colours.map((c) => c.colour)).size).toBe(5);
+ for (const { colour, marks } of colours) {
+ expect(marks.length).toBeGreaterThan(0);
+ for (const stroke of marks) expect(stroke).toBe(colour);
+ }
+ expect(new Set(dots)).toEqual(new Set(colours.map((c) => c.colour)));
+ // The rows run on down the page, which scrolls; the last one is reached.
+ const last = page.locator("svg [data-series]").last();
+ await last.scrollIntoViewIfNeeded();
+ await expect(last).toBeInViewport();
+});
+
+test("each mean's dot sits on the middle of its text and never wraps apart from it", async ({
+ page,
+}, testInfo) => {
+ await setTimelineModuleOn(page);
+ await mockTimeline(page, "full", TODAY);
+ const check = async (slot: string) => {
+ const rows = page.locator(`[data-slot="${slot}"]`);
+ await expect(rows.first()).toBeVisible({ timeout: 20_000 });
+ const units = await rows.evaluateAll((els) =>
+ els.flatMap((row) =>
+ Array.from(
+ row.querySelectorAll('[data-slot="timeline-mean-part"]'),
+ ).map((unit) => {
+ const dot = unit
+ .querySelector('[data-slot="timeline-series-dot"]')!
+ .getBoundingClientRect();
+ const text = unit.querySelector(".truncate")!.getBoundingClientRect();
+ return {
+ dotMid: dot.top + dot.height / 2,
+ textMid: text.top + text.height / 2,
+ dotRight: dot.right,
+ textLeft: text.left,
+ textHeight: text.height,
+ };
+ }),
+ ),
+ );
+ expect(units.length).toBeGreaterThan(0);
+ for (const unit of units) {
+ // Centred on the text line, to the pixel.
+ expect(Math.abs(unit.dotMid - unit.textMid)).toBeLessThanOrEqual(1);
+ // On the same line, the value right after its dot: one line high.
+ expect(unit.textLeft).toBeGreaterThan(unit.dotRight);
+ expect(unit.textLeft - unit.dotRight).toBeLessThanOrEqual(8);
+ expect(unit.textHeight).toBeLessThanOrEqual(20);
+ }
+ };
+
+ // A narrow chart: the selection bar's means wrap between units only.
+ await page.setViewportSize({ width: 800, height: 900 });
+ await page.goto("/timeline?zoom=all");
+ await expect(page.locator('[data-slot="timeline-chart"]')).toBeVisible({
+ timeout: 20_000,
+ });
+ await check("timeline-selection-means");
+ await testInfo.attach("selection-means-800", {
+ body: await page
+ .locator('[data-slot="timeline-selection-bar"]')
+ .screenshot(),
+ contentType: "image/png",
+ });
+
+ // The phone chronicle's means follow the same rule.
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto("/timeline?zoom=all");
+ await check("timeline-chronicle-means");
+ await testInfo.attach("chronicle-means-390", {
+ body: await page.screenshot(),
+ contentType: "image/png",
+ });
+});
+
+test("one medication is one row: its doses cut the bar, its pause is a gap in it", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ await mockTimeline(page, "full", TODAY);
+ await page.goto("/timeline?zoom=all");
+ const lane = page.locator('svg [data-lane="medications"]');
+ await expect(lane).toBeVisible({ timeout: 20_000 });
+ // The medication's own span is not drawn beside its course.
+ await expect(lane.locator('[data-item="mj"]')).toHaveCount(0);
+ const pieces = lane.locator('[data-item="mj-course"]');
+ await expect(pieces).toHaveCount(3);
+ expect(
+ await pieces.evaluateAll((els) => els.map((el) => el.dataset.dose)),
+ ).toEqual(["2,5 mg", "5 mg", "7,5 mg"]);
+ const rowOf = (selector: string) =>
+ lane
+ .locator(selector)
+ .evaluateAll((els) => [...new Set(els.map((el) => el.dataset.row))]);
+ const mounjaroRow = await rowOf('[data-item="mj-course"]');
+ expect(mounjaroRow).toHaveLength(1);
+ expect(await rowOf('[data-kind="pause"][data-item="mj-pause"]')).toEqual(
+ mounjaroRow,
+ );
+ // Three winter courses of one medication share one row of their own.
+ const vitamin = await rowOf('[data-item^="c-"]');
+ expect(vitamin).toHaveLength(1);
+ expect(vitamin).not.toEqual(mounjaroRow);
+ // Four medications, four rows.
+ const rows = await lane
+ .locator("[data-row]")
+ .evaluateAll((els) => new Set(els.map((el) => el.dataset.row)).size);
+ expect(rows).toBe(4);
+ // A zoom change redraws the pieces: none is left behind from the last
+ // window (each piece has a key of its own).
+ const zoom = (value: string) =>
+ page.locator(`[data-slot="timeline-zoom"] [data-value="${value}"]`);
+ await zoom("year").click();
+ await expect(page).toHaveURL(/zoom=year/);
+ await zoom("all").click();
+ await expect(page).toHaveURL(/zoom=all/);
+ await expect(pieces).toHaveCount(3);
+});
+
+/** `days` calendar days before `key`. */
+function daysBefore(key: string, days: number): string {
+ const [y, m, d] = key.split("-").map(Number);
+ return new Date(Date.UTC(y, m - 1, d - days)).toISOString().slice(0, 10);
+}
+
+test("a chosen range lives in the URL: reload keeps it, Back returns to it", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ const log = await mockTimeline(page, "full", TODAY);
+ await page.goto("/timeline?zoom=all");
+ const chart = page.locator('[data-slot="timeline-chart"]');
+ await expect(chart).toBeVisible({ timeout: 20_000 });
+ const desktop = page.locator('[data-slot="timeline-desktop"]');
+ const zoom = (value: string) =>
+ page.locator(`[data-slot="timeline-zoom"] [data-value="${value}"]`);
+
+ // Choosing "Range" opens its fields over the page, anchored to the
+ // segment: the chart does not move, and the URL waits for "Apply".
+ const before = await chart.boundingBox();
+ await zoom("range").click();
+ const popover = page.locator('[data-slot="timeline-range-popover"]');
+ await expect(popover).toBeVisible();
+ const fields = popover.locator('[data-slot="timeline-range"]');
+ await expect(fields).toBeVisible();
+ expect(await chart.boundingBox()).toEqual(before);
+ await expect(desktop.locator('[data-slot="timeline-range"]')).toHaveCount(0);
+ expect(page.url()).not.toContain("zoom=range");
+ const anchor = await zoom("range").boundingBox();
+ const box = await popover.boundingBox();
+ expect(box!.y).toBeGreaterThanOrEqual(anchor!.y + anchor!.height);
+ expect(Math.abs(box!.x - anchor!.x)).toBeLessThan(24);
+ // Starts from what is on screen; "Apply" writes it down.
+ await popover.locator('[data-slot="timeline-range-apply"]').click();
+ await expect(popover).toHaveCount(0);
+ await expect(page).toHaveURL(
+ /[?&]zoom=range&from=\d{4}-\d{2}-\d{2}&to=\d{4}-\d{2}-\d{2}/,
+ );
+
+ // Three weeks: daily means, the bar headed by one day.
+ const from = daysBefore(TODAY, 20);
+ await page.goto(`/timeline?zoom=range&from=${from}&to=${TODAY}`);
+ await expect(desktop).toHaveAttribute("data-from", from, {
+ timeout: 20_000,
+ });
+ await expect(desktop).toHaveAttribute("data-to", TODAY);
+ await expect(zoom("range")).toHaveAttribute("aria-checked", "true");
+ await expect(
+ page.locator('[data-slot="timeline-legend-mean"]'),
+ ).toHaveAttribute("data-bucket", "day");
+ await expect(
+ page.locator('[data-slot="timeline-selection-bar"]'),
+ ).toHaveAttribute("data-bucket", "day");
+ const asked = log.timelineQueries.at(-1)!;
+ expect(asked.get("zoom")).toBe("range");
+ expect(asked.get("from")).toBe(from);
+ expect(asked.get("to")).toBe(TODAY);
+
+ // A reload keeps it.
+ await page.reload();
+ await expect(desktop).toHaveAttribute("data-from", from, {
+ timeout: 20_000,
+ });
+
+ // A fixed zoom replaces it; Back brings it back.
+ await zoom("year").click();
+ await expect(page).toHaveURL(/[?&]zoom=year$/);
+ await expect(desktop).not.toHaveAttribute("data-from", from);
+ await page.goBack();
+ await expect(page).toHaveURL(new RegExp(`zoom=range&from=${from}`));
+ await expect(desktop).toHaveAttribute("data-from", from);
+
+ // Typing a start in the popover moves the range once it is applied.
+ const later = daysBefore(TODAY, 10);
+ await zoom("range").click();
+ const start = popover.locator('[data-testid="timeline-range-from"]');
+ await start.fill(later);
+ await start.press("Enter");
+ expect(page.url()).toContain(`from=${from}`);
+ await popover.locator('[data-slot="timeline-range-apply"]').click();
+ await expect(page).toHaveURL(new RegExp(`from=${later}&to=${TODAY}`));
+
+ // A parameter it cannot read opens the same default as no zoom: this
+ // record has entries in the last three months, so three months.
+ await page.goto("/timeline?zoom=range&from=nonsense&to=2026-01-01");
+ await expect(zoom("quarter")).toHaveAttribute("aria-checked", "true", {
+ timeout: 20_000,
+ });
+ await expect(desktop).toHaveAttribute("data-zoom", "quarter");
+});
+
+async function setTimelineModuleOn(page: Page) {
+ await page.goto("/settings/modules");
+ await setTimelineModule(page, true);
+}
+
+test("a life event is entered through the sheet, in the contract's shape", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ const log = await mockTimeline(page, "full", TODAY);
+ await page.goto("/timeline?add=lifeEvent");
+ const form = page.locator('[data-slot="life-event-form"]');
+ await expect(form).toBeVisible({ timeout: 20_000 });
+
+ // Saving an empty form says what is missing and sends nothing.
+ await page.locator('[data-slot="life-event-save"]').click();
+ await expect(form.locator('[aria-invalid="true"]').first()).toBeVisible();
+ expect(log.lifeEventPosts).toHaveLength(0);
+
+ await page
+ .locator('[data-slot="life-event-title"]')
+ .fill("Umzug nach Bochum");
+ await page
+ .locator('[data-slot="life-event-category"] [data-value="HOME"]')
+ .click();
+ await page
+ .locator('[data-slot="life-event-precision"] [data-value="MONTH"]')
+ .click();
+ await expect(page.locator('[data-slot="life-event-privacy"]')).toBeVisible();
+ await page.locator('[data-slot="life-event-save"]').click();
+
+ await expect(form).toHaveCount(0);
+ expect(log.lifeEventPosts).toHaveLength(1);
+ const posted = log.lifeEventPosts[0] as Record;
+ expect(posted).toMatchObject({
+ title: "Umzug nach Bochum",
+ category: "HOME",
+ precision: "MONTH",
+ endDate: null,
+ note: null,
+ });
+ expect(posted.startDate).toMatch(/^\d{4}-\d{2}-01$/);
+ // The parameter left the URL with the sheet.
+ await expect(page).toHaveURL(/\/timeline$/);
+});
+
+test("on a phone the chronicle replaces the chart and opens a day per row", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ await mockTimeline(page, "full", TODAY);
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto("/timeline?zoom=all");
+
+ const chronicle = page.locator('[data-slot="timeline-chronicle"]');
+ await expect(chronicle).toBeVisible({ timeout: 20_000 });
+ await expect(page.locator('[data-slot="timeline-chart"]')).toBeHidden();
+ await expect(page.locator('[data-slot="timeline-standing"]')).toBeVisible();
+ await expect(
+ page.locator('[data-slot="timeline-chronicle-gap"]').first(),
+ ).toBeVisible();
+ // Quarterly means, each quarter named once, at its newest listed month.
+ const means = page.locator('[data-slot="timeline-chronicle-means"]');
+ await expect(means.first()).toBeVisible();
+ const buckets = await means.evaluateAll((els) =>
+ els.map((el) => el.getAttribute("data-bucket") ?? ""),
+ );
+ expect(buckets.length).toBeGreaterThan(0);
+ expect(new Set(buckets).size).toBe(buckets.length);
+ for (const b of buckets) expect(b).toMatch(/^\d{4}-(01|04|07|10)-01$/);
+ // Each mean carries its line's colour dot.
+ await expect(
+ means.first().locator('[data-slot="timeline-series-dot"]').first(),
+ ).toBeVisible();
+
+ // One medication, one start: its course and its own span are one row,
+ // with the dose taken then.
+ const started = chronicle.locator(
+ '[data-slot="timeline-chronicle-row"][data-date="2025-01-06"]',
+ );
+ await expect(started).toHaveCount(1);
+ await expect(started).toContainText("Mounjaro");
+ await expect(started).toContainText("2,5 mg");
+ await expect(chronicle).not.toContainText("·");
+
+ // A range is chosen in a sheet; the list underneath does not move.
+ const listTop = (await chronicle.boundingBox())!.y;
+ await page.locator('[data-slot="timeline-range-toggle"]').click();
+ const sheet = page.locator('[data-slot="responsive-sheet-content"]');
+ await expect(sheet.locator('[data-slot="timeline-range"]')).toBeVisible();
+ expect((await chronicle.boundingBox())!.y).toBe(listTop);
+ await sheet.locator('[data-slot="timeline-range-apply"]').click();
+ await expect(page).toHaveURL(/[?&]zoom=range&from=\d{4}-\d{2}-\d{2}/);
+ await page.goto("/timeline?zoom=all");
+ await expect(chronicle).toBeVisible({ timeout: 20_000 });
+
+ await page
+ .locator(
+ '[data-slot="timeline-chronicle-row"][data-date="2026-01-03"] button',
+ )
+ .first()
+ .click();
+ await expect(page).toHaveURL(/[?&]day=2026-01-03/);
+});
+
+/** A calendar date `days` before today, in the account's zone. */
+function daysAgo(days: number): string {
+ const at = new Date(`${TODAY}T12:00:00Z`);
+ at.setUTCDate(at.getUTCDate() - days);
+ return at.toISOString().slice(0, 10);
+}
+
+test("every medication still taken is drawn, from the account's real record", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ // Written through the app's own routes, once: a medication with a start
+ // date (it opens a course), one without (its first intake is its start).
+ const list = await page.request.get("/api/medications");
+ const held = JSON.stringify(await list.json());
+ const create = async (body: Record) => {
+ const res = await page.request.post("/api/medications", { data: body });
+ expect(res.status()).toBeLessThan(300);
+ return ((await res.json()) as { data: { id: string } }).data.id;
+ };
+ if (!held.includes("E2E Ramipril")) {
+ await create({
+ name: "E2E Ramipril",
+ dose: "5 mg",
+ startsOn: daysAgo(40),
+ schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
+ });
+ }
+ if (!held.includes("E2E Vitamin D3")) {
+ const id = await create({
+ name: "E2E Vitamin D3",
+ dose: "2000 IU",
+ schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
+ });
+ const intake = await page.request.post(`/api/medications/${id}/intake`, {
+ data: { takenAt: new Date(Date.now() - 20 * 86_400_000).toISOString() },
+ });
+ expect(intake.status()).toBeLessThan(300);
+ }
+
+ await page.goto("/timeline?zoom=quarter");
+ const lane = page.locator('svg [data-lane="medications"]');
+ await expect(lane).toHaveCount(1, { timeout: 20_000 });
+ // One row each, and every one of them reaches today.
+ for (const name of ["E2E Ramipril", "E2E Vitamin D3"]) {
+ const bars = lane.locator("g[data-row]", {
+ has: page.locator(`title:text-matches("^${name}")`),
+ });
+ await expect(bars.first()).toBeAttached();
+ const rows = await bars.evaluateAll((els) =>
+ els.map((el) => el.getAttribute("data-row")),
+ );
+ expect(new Set(rows).size).toBe(1);
+ }
+});
+
+test("an open day stays open from the timeline to another page", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ await mockTimeline(page, "full", TODAY);
+ await page.setViewportSize({ width: 1440, height: 900 });
+ const day = daysAgo(3);
+ await page.goto(`/timeline?day=${day}`);
+ const panel = page.locator('[data-slot="day-panel"]');
+ await expect(panel).toBeVisible({ timeout: 20_000 });
+
+ await page.locator('a[href="/labs"]').first().click();
+ await expect(page).toHaveURL(new RegExp(`/labs\\?(.*&)?day=${day}`));
+ await expect(panel).toBeVisible();
+ await expect(page.locator('[data-slot="day-view"]')).toHaveAttribute(
+ "data-day",
+ day,
+ );
+
+ // Folded by the person (the strip is the docked day's only fold control),
+ // it stays folded on the next page.
+ await expect(page.locator('[data-slot="day-close"]')).toHaveCount(0);
+ await page.locator('[data-slot="day-strip-toggle"]').click();
+ await expect(panel).toHaveCount(0);
+ await page.locator('a[href="/timeline"]').first().click();
+ await expect(page).toHaveURL(/\/timeline$/);
+ await expect(page.locator('[data-slot="day-strip"]')).toHaveAttribute(
+ "data-state",
+ "closed",
+ );
+ await expect(panel).toHaveCount(0);
+});
+
+test("a value point reads on its own, by pointer and by keyboard", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ await mockTimeline(page, "full", TODAY);
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto("/timeline?zoom=all");
+ const targets = page.locator('[data-slot="timeline-point-target"]');
+ await expect(targets.first()).toBeAttached({ timeout: 20_000 });
+
+ const target = targets.last();
+ const series = await target.getAttribute("data-series");
+ const t = await target.getAttribute("data-t");
+ await target.hover();
+ const tip = page.locator('[data-slot="timeline-point-tip"]');
+ await expect(tip).toBeVisible();
+ await expect(tip).toHaveAttribute("data-series", series!);
+ await expect(tip).toHaveAttribute("data-t", t!);
+ // One line only, never the whole bucket.
+ await expect(tip.locator('[data-slot="rich-chart-tooltip-row"]')).toHaveCount(
+ 1,
+ );
+ // Inside the chart, and the page does not scroll sideways for it.
+ const chart = (await page
+ .locator('[data-slot="timeline-chart"]')
+ .boundingBox())!;
+ const box = (await tip.boundingBox())!;
+ expect(box.x).toBeGreaterThanOrEqual(chart.x - 1);
+ expect(box.x + box.width).toBeLessThanOrEqual(chart.x + chart.width + 1);
+
+ // The keyboard: focus shows the card, the arrows walk the line.
+ await page.mouse.move(0, 0);
+ await target.focus();
+ await expect(tip).toHaveAttribute("data-t", t!);
+ await page.keyboard.press("ArrowLeft");
+ await expect(tip).not.toHaveAttribute("data-t", t!);
+ await expect(tip).toHaveAttribute("data-series", series!);
+});
+
+test("the timeline page ends where its content does, at every desktop width", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ await mockTimeline(page, "full", TODAY);
+ for (const width of [1280, 1440, 1920]) {
+ for (const day of [null, daysAgo(3)]) {
+ await page.setViewportSize({ width, height: 900 });
+ await page.goto(day ? `/timeline?day=${day}` : "/timeline");
+ await expect(
+ page.locator('[data-slot="timeline-chart"] > svg'),
+ ).toBeVisible({ timeout: 20_000 });
+ const m = await page.evaluate(() => {
+ const main = document.getElementById("main-content")!;
+ const page = document.querySelector('[data-slot="timeline-page"]')!;
+ const doc = document.scrollingElement!;
+ return {
+ mainSw: main.scrollWidth,
+ mainCw: main.clientWidth,
+ docSw: doc.scrollWidth,
+ docCw: doc.clientWidth,
+ docSh: doc.scrollHeight,
+ docCh: doc.clientHeight,
+ mainSh: main.scrollHeight,
+ contentBottom:
+ page.getBoundingClientRect().bottom -
+ main.getBoundingClientRect().top +
+ main.scrollTop,
+ };
+ });
+ const where = `${width}px ${day ? "with" : "without"} the day`;
+ expect(m.mainSw, where).toBeLessThanOrEqual(m.mainCw + 1);
+ expect(m.docSw, where).toBeLessThanOrEqual(m.docCw + 1);
+ expect(m.docSh, where).toBeLessThanOrEqual(m.docCh + 1);
+ // The shell's own buffer below the last card (pb-20 and the top
+ // padding), nothing more.
+ expect(m.mainSh, where).toBeLessThanOrEqual(m.contentBottom + 104 + 2);
+ }
+ }
+});
+
+test("listing all of a day's values scrolls the panel, never the page", async ({
+ page,
+}) => {
+ await setTimelineModuleOn(page);
+ await mockTimeline(page, "full", TODAY);
+ const day = daysAgo(3);
+ const types = [
+ "WEIGHT",
+ "BODY_FAT",
+ "PULSE",
+ "RESTING_HEART_RATE",
+ "HEART_RATE_VARIABILITY",
+ "RESPIRATORY_RATE",
+ "OXYGEN_SATURATION",
+ "BODY_TEMPERATURE",
+ "MUSCLE_MASS",
+ "BONE_MASS",
+ "FAT_MASS",
+ "VISCERAL_FAT",
+ "BLOOD_GLUCOSE",
+ "ACTIVITY_STEPS",
+ ];
+ await page.route(`**/api/day/${day}`, (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ date: day,
+ tz: "Europe/Berlin",
+ counts: { values: types.length, entries: 0 },
+ running: [],
+ values: types.map((type, i) => ({
+ type,
+ value: 10 + i,
+ unit: "u",
+ at: `${day}T0${i % 10}:00:00.000Z`,
+ source: "MANUAL",
+ band: null,
+ })),
+ events: [],
+ notable: [],
+ scores: [],
+ sections: {},
+ },
+ error: null,
+ }),
+ }),
+ );
+ await page.setViewportSize({ width: 1440, height: 800 });
+ await page.goto(`/timeline?day=${day}`);
+ const all = page.locator('[data-slot="day-values-all"]');
+ await expect(all).toBeVisible({ timeout: 20_000 });
+ const heights = () =>
+ page.evaluate(() => ({
+ doc: document.scrollingElement!.scrollHeight,
+ main: document.getElementById("main-content")!.scrollHeight,
+ }));
+ const before = await heights();
+ await all.click();
+ await expect(all).toHaveAttribute("aria-expanded", "true");
+ const after = await heights();
+ expect(Math.abs(after.doc - before.doc)).toBeLessThanOrEqual(2);
+ expect(Math.abs(after.main - before.main)).toBeLessThanOrEqual(2);
+});
+
+test("the day's strip stays at the right edge, opens and closes the day, and never moves", async ({
+ page,
+}, testInfo) => {
+ await setTimelineModuleOn(page);
+ await mockTimeline(page, "full", TODAY);
+ const strip = page.locator('[data-slot="day-strip"]');
+ const toggle = page.locator('[data-slot="day-strip-toggle"]');
+ const panel = page.locator('[data-slot="day-panel"]');
+ const where = async () => {
+ const box = (await strip.boundingBox())!;
+ return [box.x, box.y, box.width, box.height].map(Math.round);
+ };
+ const measure = () =>
+ page.evaluate(() => {
+ const main = document.getElementById("main-content")!;
+ const doc = document.scrollingElement!;
+ return {
+ docSw: doc.scrollWidth - doc.clientWidth,
+ docSh: doc.scrollHeight - doc.clientHeight,
+ mainSw: main.scrollWidth - main.clientWidth,
+ };
+ });
+
+ for (const width of [1280, 1440, 1920]) {
+ await page.setViewportSize({ width, height: 900 });
+ await page.goto("/timeline?zoom=all");
+ await expect(
+ page.locator('[data-slot="timeline-chart"] > svg'),
+ ).toBeVisible({ timeout: 20_000 });
+
+ // Closed: the strip is there, at the right edge, the full height of the
+ // window from its top edge, holding today.
+ await expect(strip).toHaveAttribute("data-state", "closed");
+ await expect(toggle).toHaveAttribute("aria-expanded", "false");
+ const closed = await where();
+ expect(closed[0] + closed[2], `${width}px right edge`).toBe(width);
+ const before = await measure();
+ expect(closed[1], `${width}px from the top edge`).toBe(0);
+ expect(closed[3], `${width}px full height`).toBe(900);
+ expect(
+ Math.round((await toggle.boundingBox())!.y),
+ `${width}px toggle from the top edge`,
+ ).toBe(0);
+ expect(before.docSw).toBeLessThanOrEqual(0);
+ expect(before.mainSw).toBeLessThanOrEqual(0);
+
+ // A click opens the day left of the strip; the strip stays put.
+ await toggle.click();
+ await expect(panel).toHaveAttribute("data-shell", "docked");
+ await expect(strip).toHaveAttribute("data-state", "open");
+ await expect(page.locator("#day-docked-panel")).toHaveAttribute(
+ "data-state",
+ "open",
+ );
+ await expect
+ .poll(async () => {
+ const box = (await panel.boundingBox())!;
+ return Math.round(box.x + box.width);
+ })
+ .toBe(closed[0]);
+ expect(await where()).toEqual(closed);
+ await expect(panel.locator("h2").first()).toBeFocused();
+ const open = await measure();
+ expect(open.docSw, `${width}px open, sideways`).toBeLessThanOrEqual(0);
+ expect(open.mainSw, `${width}px open, sideways`).toBeLessThanOrEqual(0);
+ expect(open.docSh, `${width}px open, page height`).toBeLessThanOrEqual(
+ before.docSh,
+ );
+ await testInfo.attach(`day-strip-${width}-open`, {
+ body: await page.screenshot(),
+ contentType: "image/png",
+ });
+
+ // A second click closes it; focus stays on the strip, which stays put.
+ await toggle.click();
+ await expect(panel).toHaveCount(0);
+ await expect(strip).toHaveAttribute("data-state", "closed");
+ await expect(toggle).toBeFocused();
+ expect(await where()).toEqual(closed);
+ // Closing the day takes `?day=` out and leaves the zoom it opened with.
+ await expect(page).toHaveURL(/\/timeline\?zoom=all$/);
+ }
+
+ // Both themes, open at 1440.
+ await page.setViewportSize({ width: 1440, height: 900 });
+ for (const colorScheme of ["light", "dark"] as const) {
+ await page.emulateMedia({ colorScheme });
+ await page.goto("/timeline?zoom=all");
+ await toggle.click();
+ await expect(panel).toHaveAttribute("data-shell", "docked");
+ await testInfo.attach(`day-strip-1440-${colorScheme}`, {
+ body: await page.screenshot(),
+ contentType: "image/png",
+ });
+ await toggle.click();
+ await expect(panel).toHaveCount(0);
+ }
+});
diff --git a/e2e/today-just-in.spec.ts b/e2e/today-just-in.spec.ts
index 2583b460d..6c8853330 100644
--- a/e2e/today-just-in.spec.ts
+++ b/e2e/today-just-in.spec.ts
@@ -162,8 +162,17 @@ test("a fresh arrival flips the day in place", async ({ page }) => {
// swept by the same `e2e-just-in-` purge on the next run.
const priorNight = new Date(Date.now() - 3 * 24 * 60 * 60_000);
const priorWoke = new Date(priorNight.getTime() + 7 * 60 * 60_000);
+ //
+ // Tagged `foreground`, the trigger an open app sends. Since v1.42 a batch
+ // without it only marks the dashboard snapshot stale, and `cachedSwr`
+ // serves the stale cell once while it rebuilds: a snapshot an earlier
+ // spec left for this account (no sleep at all, so no sleep expected)
+ // would then paint the BEFORE state as `final`, depending on which spec
+ // happened to run first in the shard. `foreground` evicts, so the first
+ // read below is built from the rows this spec just wrote.
const priorBatch = await page.request.post("/api/measurements/batch", {
data: {
+ syncTrigger: "foreground",
entries: [
{
hkIdentifier: "HKCategoryTypeIdentifierSleepAnalysis",
@@ -229,8 +238,11 @@ test("a fresh arrival flips the day in place", async ({ page }) => {
const wokeAt = new Date(now.getTime() - 60 * 60_000);
const fellAsleepAt = new Date(wokeAt.getTime() - 7 * 60 * 60_000);
+ // `foreground` for the same reason as the prior night above: the page
+ // is open while it lands.
const batch = await page.request.post("/api/measurements/batch", {
data: {
+ syncTrigger: "foreground",
entries: [
{
hkIdentifier: "HKCategoryTypeIdentifierSleepAnalysis",
diff --git a/e2e/today-overview.spec.ts b/e2e/today-overview.spec.ts
new file mode 100644
index 000000000..1b7782963
--- /dev/null
+++ b/e2e/today-overview.spec.ts
@@ -0,0 +1,591 @@
+import { mkdirSync } from "node:fs";
+import { join } from "node:path";
+
+import type { Page } from "@playwright/test";
+
+import { expect, test } from "./setup/test";
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import {
+ mockDashboardSnapshot,
+ POPULATED_SUMMARIES,
+} from "./utils/mock-dashboard-snapshot";
+import type { DailyDigest } from "@/lib/daily/digest";
+import { DIGEST_AI_AVAILABLE } from "@/__tests__/helpers/ai-capability-fixtures";
+
+/**
+ * The Today overview on the dashboard hero, at a desktop and a phone width.
+ *
+ * What it proves is geometry, because that is what the unit suites cannot
+ * see: the facts sit beside the ring on a wide screen; on a phone the ring is
+ * a compact dial at the top right beside the lead and the facts run the full
+ * card width under both; a phone shows four facts and a wide screen five,
+ * nothing pushes the page sideways, the hero keeps the width of the cards
+ * around it, and the ring keeps its size per breakpoint. Each state is also
+ * captured as an element screenshot, in both themes, under
+ * `test-results/today-overview/` for a visual check.
+ *
+ * The digests are what the server would publish for each state. They carry
+ * the pre-overview fields too (`briefingLead`, `line`), with the values the
+ * old server sent, so the same fixtures render the previous hero on an older
+ * build for a before/after comparison.
+ *
+ * Assertions anchor on `data-slot` / `data-kind`, never on visible text.
+ */
+test.use({ storageState: STORAGE_STATE_PATH });
+
+const SHOTS = join(process.cwd(), "test-results", "today-overview");
+
+const VIEWPORTS = [
+ { name: "desktop", width: 1440, height: 900 },
+ { name: "mobile", width: 390, height: 844 },
+ { name: "narrow", width: 360, height: 780 },
+] as const;
+
+const THEMES = ["dark", "light"] as const;
+
+/** The dial's edge per breakpoint: compact on a phone, md from 768 px. */
+const RING_PX = { phone: 80, wide: 168 } as const;
+
+/** Below this the lead beside the ring would read as a column of words. */
+const MIN_LEAD_WIDTH = 180;
+
+const BASE: DailyDigest = {
+ generatedAt: new Date().toISOString(),
+ ai: DIGEST_AI_AVAILABLE,
+ phase: "final",
+ sleepPending: false,
+ score: {
+ value: 94,
+ band: "green",
+ delta: null,
+ deltaReason: "first_eligibility_window",
+ steadyWeeks: 4,
+ },
+ topSignal: null,
+ signalLine: null,
+ briefingLead: null,
+ lead: null,
+ today: [],
+ restMode: null,
+ line: "Your health score today is 94.",
+ worthALook: [],
+ justIn: null,
+ reactionLine: null,
+};
+
+const FACTS: DailyDigest["today"] = [
+ {
+ kind: "medications",
+ label: "Medications",
+ value: "1 of 3 taken",
+ href: "/medications",
+ moduleKey: "medications",
+ },
+ {
+ kind: "appointment",
+ label: "Appointment",
+ value: "Tomorrow 09:30, Routine visit",
+ href: "/checkups",
+ },
+ {
+ kind: "sleep",
+ label: "Last night",
+ value: "7h 20m, close to your usual",
+ href: "/insights/sleep",
+ moduleKey: "sleep",
+ },
+ {
+ kind: "vitals",
+ label: "Vitals",
+ value: "6 checked, all in your range",
+ href: "/insights",
+ },
+ {
+ kind: "cycle",
+ label: "Cycle",
+ value: "Follicular, day 12",
+ href: "/cycle",
+ moduleKey: "cycle",
+ },
+];
+
+const STATES: Record = {
+ // AI on. The old server led with the briefing's first sentence, which was
+ // the greeting.
+ "ai-on": {
+ ...BASE,
+ briefingLead: "Good morning.",
+ line: "Good morning.",
+ topSignal: {
+ sourceMetric: "bp",
+ tone: "good",
+ headline: "Blood pressure is sitting in the optimal band",
+ nudge: "",
+ delta: "↓ ~10 mmHg systolic vs the start of the window",
+ },
+ lead: {
+ text: "Your latest blood pressure is sitting in the optimal band.",
+ source: "briefing",
+ },
+ // The lead already talks about blood pressure, so the server keeps only
+ // the delta under it.
+ signalLine: {
+ headline: null,
+ delta: "↓ ~10 mmHg systolic vs the start of the window",
+ },
+ today: FACTS,
+ worthALook: [
+ {
+ kind: "dose_window",
+ title: "Medication due",
+ body: "Ramipril is due soon.",
+ status: "info",
+ actions: [
+ {
+ labelKey: "daily.action.logDose",
+ intent: "dose.log",
+ href: "/medications",
+ },
+ ],
+ moduleKey: "medications",
+ },
+ ],
+ },
+ // AI off. The old server had only the score sentence, which the hero then
+ // removed again, leaving the ring and "Nothing needs your attention".
+ "ai-off": {
+ ...BASE,
+ ai: {
+ ...DIGEST_AI_AVAILABLE,
+ briefing: {
+ available: false,
+ reason: "no_provider",
+ onDeviceAllowed: false,
+ },
+ },
+ lead: {
+ text: "Resting heart rate is at 61 bpm, above your usual range of 50 to 58 bpm.",
+ source: "signal",
+ },
+ today: [FACTS[0], FACTS[2], FACTS[4]],
+ },
+ // AI on with a long lead and a long sleep value: the phone case the ring
+ // used to fall under the facts for.
+ "ai-on-long": {
+ ...BASE,
+ score: {
+ value: 63,
+ band: "yellow",
+ delta: -6,
+ deltaReason: null,
+ steadyWeeks: null,
+ },
+ lead: {
+ text: "Your pulse sits well above its usual level this evening, after a shorter night than you normally get; a quieter evening would suit it.",
+ source: "briefing",
+ },
+ signalLine: { headline: null, delta: "+34 bpm vs your 30-day average" },
+ today: [
+ FACTS[0],
+ {
+ kind: "sleep",
+ label: "Last night",
+ value: "5h 45m, 42m less than usual",
+ href: "/insights/sleep",
+ moduleKey: "sleep",
+ },
+ {
+ kind: "vitals",
+ label: "Vitals",
+ value: "Pulse above your range",
+ href: "/insights",
+ },
+ ],
+ },
+ // A quiet day: one calm sentence and no facts beyond it.
+ quiet: {
+ ...BASE,
+ lead: {
+ text: "All 6 of your latest vitals sit inside their usual range.",
+ source: "signal",
+ },
+ },
+};
+
+async function openDashboard(page: Page, digest: DailyDigest) {
+ await mockDashboardSnapshot(page, { summaries: POPULATED_SUMMARIES });
+ await page.route(/\/api\/daily\/digest(\?|$)/, (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ data: digest, error: null }),
+ }),
+ );
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ const hero = page.locator('[data-slot="today-hero"]');
+ await expect(hero).toBeVisible({ timeout: 15_000 });
+ await expect(page.locator('[data-slot="dashboard-tile-strip"]')).toBeVisible({
+ timeout: 15_000,
+ });
+ return hero;
+}
+
+test.describe("Today overview geometry", () => {
+ test.beforeEach(({}, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "viewport-driven spec; desktop project only",
+ );
+ });
+
+ for (const viewport of VIEWPORTS) {
+ for (const theme of THEMES) {
+ for (const [state, digest] of Object.entries(STATES)) {
+ test(`${state} at ${viewport.width}px, ${theme}`, async ({ page }) => {
+ // The app paints from its own stored preference, not the media
+ // query, so both are set.
+ await page.emulateMedia({
+ colorScheme: theme,
+ reducedMotion: "reduce",
+ });
+ await page.addInitScript((value: string) => {
+ window.localStorage.setItem("healthlog-theme", value);
+ }, theme);
+ await page.setViewportSize(viewport);
+ const hero = await openDashboard(page, digest);
+ const phone = viewport.width < 768;
+
+ // Late banners (the key-backup nudge) insert above the hero; let
+ // the page settle so the capture shows one stable layout.
+ await page.waitForLoadState("networkidle");
+ mkdirSync(SHOTS, { recursive: true });
+ await hero.screenshot({
+ path: join(SHOTS, `${viewport.name}-${theme}-${state}.png`),
+ });
+ // The viewport as a reader sees it, bottom navigation included.
+ await page.evaluate(() => window.scrollTo(0, 0));
+ await page.screenshot({
+ path: join(
+ SHOTS,
+ `${viewport.name}-${theme}-${state}-viewport.png`,
+ ),
+ });
+
+ // Nothing pushes the page sideways.
+ const overflow = await page.evaluate(
+ () => document.documentElement.scrollWidth - window.innerWidth,
+ );
+ expect(overflow).toBeLessThanOrEqual(0);
+
+ // The hero spans the same column as the tile strip below it.
+ const heroBox = (await hero.boundingBox())!;
+ const stripBox = (await page
+ .locator('[data-slot="dashboard-tile-strip"]')
+ .boundingBox())!;
+ expect(Math.abs(heroBox.x - stripBox.x)).toBeLessThanOrEqual(1);
+ expect(Math.abs(heroBox.width - stripBox.width)).toBeLessThanOrEqual(
+ 1,
+ );
+
+ // The ring keeps its size: a compact dial on a phone, the same
+ // 168 px dial as before from md up.
+ const ring = hero.locator('[data-slot="score-ring"]').first();
+ const ringBox = (await ring.boundingBox())!;
+ const edge = phone ? RING_PX.phone : RING_PX.wide;
+ expect(Math.round(ringBox.width)).toBe(edge);
+ expect(Math.round(ringBox.height)).toBe(edge);
+
+ const lead = hero.locator('[data-slot="today-hero-lead"]');
+ await expect(lead).toBeVisible();
+ await expect(lead).toHaveAttribute(
+ "data-source",
+ digest.lead!.source,
+ );
+ const leadBox = (await lead.boundingBox())!;
+
+ // The ring sits at the top right, beside the lead, and the lead
+ // keeps a readable width next to it.
+ expect(ringBox.x).toBeGreaterThanOrEqual(leadBox.x + leadBox.width);
+ expect(ringBox.y).toBeLessThan(leadBox.y + leadBox.height);
+ expect(Math.abs(ringBox.y - leadBox.y)).toBeLessThanOrEqual(8);
+ expect(leadBox.width).toBeGreaterThanOrEqual(MIN_LEAD_WIDTH);
+ // Symmetric insets: the ring's right edge mirrors the lead's left.
+ const leftInset = leadBox.x - heroBox.x;
+ const rightInset =
+ heroBox.x + heroBox.width - (ringBox.x + ringBox.width);
+ expect(Math.abs(leftInset - rightInset)).toBeLessThanOrEqual(1);
+
+ const facts = hero.locator('[data-slot="today-hero-fact"]');
+ const expected = digest.today.length;
+ await expect(facts).toHaveCount(expected);
+ const visible = await facts.evaluateAll(
+ (items) =>
+ items.filter((el) => (el as HTMLElement).offsetParent !== null)
+ .length,
+ );
+ expect(visible).toBe(phone ? Math.min(expected, 4) : expected);
+
+ // The all-clear sentence never sits under a lead or facts.
+ await expect(
+ hero.locator('[data-slot="today-hero-all-clear"]'),
+ ).toHaveCount(0);
+
+ if (expected === 0) {
+ await expect(
+ hero.locator('[data-slot="today-hero-today"]'),
+ ).toHaveCount(0);
+ // A quiet day keeps the full composition: lead beside the ring.
+ await expect(hero).toHaveAttribute("data-layout", "narrative");
+ return;
+ }
+
+ const block = hero.locator('[data-slot="today-hero-today"]');
+ const blockBox = (await block.boundingBox())!;
+ // The facts follow the lead.
+ expect(blockBox.y).toBeGreaterThanOrEqual(
+ leadBox.y + leadBox.height - 1,
+ );
+ if (!phone) {
+ // Beside the ring on a wide screen, never under it.
+ expect(blockBox.x + blockBox.width).toBeLessThanOrEqual(
+ ringBox.x + 1,
+ );
+ expect(blockBox.y).toBeLessThan(ringBox.y + ringBox.height);
+ } else {
+ // Under the dial on a phone, across the full card width.
+ expect(blockBox.y).toBeGreaterThanOrEqual(
+ ringBox.y + ringBox.height - 1,
+ );
+ expect(
+ Math.abs(
+ blockBox.x + blockBox.width - (ringBox.x + ringBox.width),
+ ),
+ ).toBeLessThanOrEqual(1);
+ // Each tappable fact keeps the 44 px floor, label over value,
+ // both on the reading edge.
+ const rows = await facts.evaluateAll((items) =>
+ items
+ .filter((el) => (el as HTMLElement).offsetParent !== null)
+ .map((el) => {
+ const link = el.querySelector("a")!;
+ const [label, value] = Array.from(link.children).map((c) =>
+ c.getBoundingClientRect(),
+ );
+ return {
+ height: link.getBoundingClientRect().height,
+ stacked: value.top >= label.bottom - 1,
+ sameEdge: Math.abs(value.left - label.left) <= 1,
+ };
+ }),
+ );
+ for (const row of rows) {
+ expect(row.height).toBeGreaterThanOrEqual(44);
+ expect(row.stacked).toBe(true);
+ expect(row.sameEdge).toBe(true);
+ }
+ }
+
+ // No fact line overflows its own box.
+ const clipped = await facts.evaluateAll(
+ (items) =>
+ items.filter((el) => {
+ const link = el.querySelector("a");
+ return link ? link.scrollWidth > link.clientWidth + 1 : false;
+ }).length,
+ );
+ expect(clipped).toBe(0);
+ });
+ }
+ }
+ }
+
+ test("the hero width does not move when the overview arrives", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ let calls = 0;
+ await mockDashboardSnapshot(page, { summaries: POPULATED_SUMMARIES });
+ await page.route(/\/api\/daily\/digest(\?|$)/, async (route) => {
+ calls += 1;
+ // First paint: a quiet digest. Every later read carries the overview.
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: calls > 1 ? STATES["ai-on"] : STATES.quiet,
+ error: null,
+ }),
+ });
+ });
+ await page.goto("/", { waitUntil: "domcontentloaded" });
+ const hero = page.locator('[data-slot="today-hero"]');
+ await expect(hero).toBeVisible({ timeout: 15_000 });
+ const before = (await hero.boundingBox())!;
+
+ // The tab becomes visible again: the signal TanStack Query's focus
+ // manager refetches on, as `today-just-in.spec.ts` uses.
+ await page.evaluate(() =>
+ window.dispatchEvent(new Event("visibilitychange")),
+ );
+ await expect(hero.locator('[data-slot="today-hero-today"]')).toBeVisible({
+ timeout: 15_000,
+ });
+ const after = (await hero.boundingBox())!;
+ expect(Math.abs(after.x - before.x)).toBeLessThanOrEqual(1);
+ expect(Math.abs(after.width - before.width)).toBeLessThanOrEqual(1);
+ });
+});
+
+/**
+ * The phone in German, where the score's meta lines are longest. In the
+ * dial's 80 px column "−6 vs. dein Mittel" and the basis sentence broke into
+ * three to five rows each; a short lead then left a gap above the facts as
+ * tall as that column. The lines now run on their own row under the dial.
+ */
+const GERMAN_SCORE: DailyDigest["score"] = {
+ value: 63,
+ band: "yellow",
+ delta: -6,
+ deltaReason: null,
+ steadyWeeks: null,
+ scoreBasis: {
+ domains: 2,
+ recommended: 3,
+ tier: "partial",
+ physiological: true,
+ },
+};
+
+const GERMAN_FACTS: DailyDigest["today"] = [
+ {
+ kind: "medications",
+ label: "Medikamente",
+ value: "1 von 2 genommen",
+ href: "/medications",
+ moduleKey: "medications",
+ },
+ {
+ kind: "sleep",
+ label: "Letzte Nacht",
+ value: "5 Std. 45 Min., 42 Min. weniger als sonst",
+ href: "/insights/sleep",
+ moduleKey: "sleep",
+ },
+ {
+ kind: "vitals",
+ label: "Vitalwerte",
+ value: "Puls über deinem Bereich",
+ href: "/insights",
+ },
+];
+
+const GERMAN_STATES: Record = {
+ "de-short": {
+ ...BASE,
+ score: GERMAN_SCORE,
+ lead: { text: "Dein Puls liegt heute erhöht.", source: "signal" },
+ today: GERMAN_FACTS,
+ },
+ "de-long": {
+ ...BASE,
+ score: GERMAN_SCORE,
+ lead: {
+ text: "Dein Puls liegt heute Abend deutlich über seinem üblichen Niveau, nach einer kürzeren Nacht als sonst; ein ruhiger Abend täte ihm gut.",
+ source: "briefing",
+ },
+ signalLine: { headline: null, delta: "+34 bpm vs. dein 30-Tage-Mittel" },
+ today: GERMAN_FACTS,
+ },
+};
+
+test.describe("Today overview on a German phone", () => {
+ test.beforeEach(async ({ context }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "viewport-driven spec; desktop project only",
+ );
+ const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
+ await context.addCookies([
+ { name: "healthlog-locale", value: "de", url: baseURL },
+ ]);
+ });
+
+ for (const viewport of VIEWPORTS.filter((v) => v.width < 768)) {
+ for (const [state, digest] of Object.entries(GERMAN_STATES)) {
+ test(`${state} at ${viewport.width}px`, async ({ page }) => {
+ await page.emulateMedia({
+ colorScheme: "dark",
+ reducedMotion: "reduce",
+ });
+ await page.addInitScript(() => {
+ window.localStorage.setItem("healthlog-theme", "dark");
+ });
+ await page.setViewportSize(viewport);
+ const hero = await openDashboard(page, digest);
+ await page.waitForLoadState("networkidle");
+ mkdirSync(SHOTS, { recursive: true });
+ await hero.screenshot({
+ path: join(SHOTS, `${viewport.name}-dark-${state}.png`),
+ });
+
+ const box = async (slot: string) =>
+ (await hero.locator(`[data-slot="${slot}"]`).first().boundingBox())!;
+ const ring = await box("score-ring");
+ const lead = await box("today-hero-lead");
+ const meta = await box("today-hero-score-meta");
+ const block = await box("today-hero-today");
+
+ // The dial keeps its size and its place beside the lead.
+ expect(Math.round(ring.width)).toBe(RING_PX.phone);
+ expect(lead.width).toBeGreaterThanOrEqual(MIN_LEAD_WIDTH);
+
+ // Each meta line reads on one line, flush with the dial's edge.
+ const lines = await hero
+ .locator('[data-slot="today-hero-score-meta"] > span')
+ .evaluateAll((spans) =>
+ spans.map((span) => {
+ const rect = span.getBoundingClientRect();
+ const lineHeight = parseFloat(getComputedStyle(span).lineHeight);
+ return {
+ rows: Math.round(rect.height / lineHeight),
+ right: rect.right,
+ };
+ }),
+ );
+ expect(lines.length).toBe(2);
+ for (const line of lines) {
+ expect(line.rows).toBe(1);
+ expect(
+ Math.abs(line.right - (ring.x + ring.width)),
+ ).toBeLessThanOrEqual(1);
+ }
+
+ // The meta row follows the read row at once: right under the dial
+ // beside a short lead, right under a long lead's last line.
+ // The lead's cell ends with its signal line when there is one.
+ const signalLine = hero.locator('[data-slot="today-hero-signal"]');
+ const signal =
+ (await signalLine.count()) > 0
+ ? await signalLine.boundingBox()
+ : null;
+ const leadBottom = signal
+ ? signal.y + signal.height
+ : lead.y + lead.height;
+ const readBottom = Math.max(ring.y + ring.height, leadBottom);
+ expect(meta.y - readBottom).toBeGreaterThanOrEqual(0);
+ expect(meta.y - readBottom).toBeLessThanOrEqual(6);
+
+ // No gap above the facts beyond one section step: they start one
+ // row gap (16 px) under whatever ends lowest, the lead or the meta.
+ const above = Math.max(leadBottom, meta.y + meta.height);
+ expect(block.y - above).toBeGreaterThanOrEqual(15);
+ expect(block.y - above).toBeLessThanOrEqual(17);
+
+ const overflow = await page.evaluate(
+ () => document.documentElement.scrollWidth - window.innerWidth,
+ );
+ expect(overflow).toBeLessThanOrEqual(0);
+ });
+ }
+ }
+});
diff --git a/e2e/utils/deferred-sections.ts b/e2e/utils/deferred-sections.ts
new file mode 100644
index 000000000..247ab9d80
--- /dev/null
+++ b/e2e/utils/deferred-sections.ts
@@ -0,0 +1,75 @@
+import { expect, type Page } from "@playwright/test";
+
+/**
+ * Mount every overview section that waits for the viewport.
+ *
+ * Sections below the first screens of `/insights` render as a 1 px
+ * `[data-deferred-section]` sentinel until they come near the visible area
+ * (`src/components/insights/defer-until-near.tsx`). A spec that asserts on
+ * one of them, or measures or scans the whole page, scrolls each sentinel
+ * into view first, the way a reader would, until none is left. Polled, since
+ * the observers only exist once the page has hydrated.
+ */
+export async function revealDeferredSections(page: Page): Promise {
+ // The sentinels render with the overview itself; until the hero is there,
+ // "no sentinel" only means the page has not rendered yet.
+ await expect(page.locator('[data-slot="insights-hero-strip"]')).toBeAttached({
+ timeout: 20_000,
+ });
+ const sentinels = page.locator("[data-deferred-section]");
+ await expect
+ .poll(
+ async () => {
+ const remaining = await sentinels.count();
+ if (remaining > 0) {
+ await sentinels
+ .first()
+ .scrollIntoViewIfNeeded({ timeout: 1_000 })
+ .catch(() => {
+ // Mounted between the count and the scroll.
+ });
+ }
+ return remaining;
+ },
+ { timeout: 20_000 },
+ )
+ .toBe(0);
+ await page.evaluate(() => document.querySelector("main")?.scrollTo(0, 0));
+}
+
+/**
+ * `revealDeferredSections`, then wait until the reads the newly mounted
+ * sections fired have landed: no request in flight for 750 ms. A
+ * `waitForLoadState("networkidle")` cannot do this: the page reached that
+ * state once already, so it resolves at once, and a count of zero taken then
+ * passes before a deferred section has painted anything.
+ */
+export async function revealDeferredSectionsSettled(page: Page): Promise {
+ let inFlight = 0;
+ const started = () => {
+ inFlight += 1;
+ };
+ const ended = () => {
+ inFlight -= 1;
+ };
+ page.on("request", started);
+ page.on("requestfinished", ended);
+ page.on("requestfailed", ended);
+ try {
+ await revealDeferredSections(page);
+ let quietSince = Date.now();
+ await expect
+ .poll(
+ () => {
+ if (inFlight > 0) quietSince = Date.now();
+ return Date.now() - quietSince >= 750;
+ },
+ { timeout: 20_000, intervals: [100] },
+ )
+ .toBe(true);
+ } finally {
+ page.off("request", started);
+ page.off("requestfinished", ended);
+ page.off("requestfailed", ended);
+ }
+}
diff --git a/e2e/utils/mock-timeline.ts b/e2e/utils/mock-timeline.ts
new file mode 100644
index 000000000..512b90c8f
--- /dev/null
+++ b/e2e/utils/mock-timeline.ts
@@ -0,0 +1,913 @@
+import type { Page, Route } from "@playwright/test";
+
+/**
+ * Route mocks for the timeline (v1.42, #613): `GET /api/timeline`,
+ * `GET /api/timeline/readiness` and `/api/life-events`, answered in the
+ * shapes of `src/lib/day/contract.ts` from an invented record. Two records:
+ * `full` (years of history in every lane) and `ready` (a thin record that
+ * starts in May 2024, a medication without a start date, no life events, no
+ * vaccinations), matching the two states the design was approved in.
+ *
+ * Every value here is made up.
+ */
+
+type Item = {
+ id: string;
+ kind: string;
+ start: string;
+ end: string | null;
+ open: boolean;
+ precision: "DAY" | "MONTH" | "YEAR";
+ startKnown: boolean;
+ label: string;
+ sub: string | null;
+ href: string | null;
+ /** The medication an item belongs to; one row per group. */
+ group: string | null;
+};
+
+function item(over: Partial & Pick): Item {
+ return {
+ end: null,
+ open: false,
+ precision: "DAY",
+ startKnown: true,
+ label: over.id,
+ sub: null,
+ href: null,
+ group: null,
+ ...over,
+ };
+}
+
+/** A small deterministic generator, so every run draws the same lines. */
+function rng(seed: number) {
+ let s = seed;
+ return () => {
+ s = (s * 16807) % 2147483647;
+ return (s - 1) / 2147483646;
+ };
+}
+
+type Bucket = "quarter" | "month" | "week" | "day";
+
+/** The bucket the server picks for a zoom (`timelineBucket`). */
+export function bucketFor(
+ zoom: string,
+ from?: string | null,
+ to?: string | null,
+): Bucket {
+ if (zoom === "range" && from && to) {
+ // `rangeBucket`: by the range's length in days.
+ const days = dayNum(to) - dayNum(from) + 1;
+ return days >= 730
+ ? "quarter"
+ : days >= 120
+ ? "month"
+ : days >= 42
+ ? "week"
+ : "day";
+ }
+ return zoom === "quarter" ? "week" : zoom === "year" ? "month" : "quarter";
+}
+
+function dayNum(key: string): number {
+ const [y, m, d] = key.split("-").map(Number);
+ return Math.round(Date.UTC(y, m - 1, d) / 86_400_000);
+}
+
+function dayStr(n: number): string {
+ return new Date(n * 86_400_000).toISOString().slice(0, 10);
+}
+
+/** Bucket starts from the one holding `from` to the one holding `to`. */
+function bucketStarts(from: string, to: string, bucket: Bucket): string[] {
+ const out: string[] = [];
+ if (bucket === "day") {
+ for (let d = dayNum(from); d <= dayNum(to); d++) out.push(dayStr(d));
+ return out;
+ }
+ if (bucket === "week") {
+ const n = dayNum(from);
+ // 1970-01-01 was a Thursday: day 4 is the first Monday.
+ for (let d = n - ((((n - 4) % 7) + 7) % 7); d <= dayNum(to); d += 7) {
+ out.push(dayStr(d));
+ }
+ return out;
+ }
+ const step = bucket === "quarter" ? 3 : 1;
+ let [y, m] = from.split("-").map(Number);
+ m -= (m - 1) % step;
+ const [ty, tm] = to.split("-").map(Number);
+ while (y < ty || (y === ty && m <= tm)) {
+ out.push(`${y}-${String(m).padStart(2, "0")}-01`);
+ m += step;
+ if (m > 12) {
+ m -= 12;
+ y++;
+ }
+ }
+ return out;
+}
+
+/**
+ * Systolic pressure leaves gaps the chart has to show honestly: in the
+ * quarterly view one missing quarter (Q2 2023, bridged dashed) and three
+ * (2021, the line breaks). Q1 2022 rests on two readings in every series.
+ */
+export const SYS_MISSING = new Set([
+ "2021-01-01",
+ "2021-04-01",
+ "2021-07-01",
+ "2023-04-01",
+]);
+export const SYS_THIN = "2022-01-01";
+
+function points(
+ from: string,
+ to: string,
+ bucket: Bucket,
+ value: (i: number, key: string) => number,
+ skip: (key: string) => boolean = () => false,
+) {
+ const starts = bucketStarts(from, to, bucket);
+ return starts
+ .map((key, i) => ({
+ t: key,
+ mean: Math.round(value(i / starts.length, key) * 10) / 10,
+ count: key === SYS_THIN ? 2 : 4 + (i % 9),
+ }))
+ .filter((p) => !skip(p.t));
+}
+
+function series(dataFrom: string, today: string, bucket: Bucket) {
+ const r = rng(11);
+ return [
+ {
+ key: "BLOOD_PRESSURE_SYS",
+ unit: "mmHg",
+ points: points(
+ dataFrom,
+ today,
+ bucket,
+ (f) => 147 - 18 * f + (r() - 0.5) * 4,
+ (t) => bucket === "quarter" && SYS_MISSING.has(t),
+ ),
+ },
+ {
+ key: "BLOOD_PRESSURE_DIA",
+ unit: "mmHg",
+ points: points(
+ dataFrom,
+ today,
+ bucket,
+ (f) => 92 - 10 * f + (r() - 0.5) * 3,
+ ),
+ },
+ {
+ key: "WEIGHT",
+ unit: "kg",
+ points: points(
+ dataFrom,
+ today,
+ bucket,
+ (f) => 89 - 6.5 * f + (r() - 0.5) * 0.6,
+ ),
+ },
+ {
+ key: "RESTING_HEART_RATE",
+ unit: "bpm",
+ points: points(dataFrom, today, bucket, (f, k) => {
+ const month = Number(k.slice(5, 7));
+ return (
+ 68 - 5 * f + Math.sin((month / 12) * 6.28) * 1.2 + (r() - 0.5) * 1.2
+ );
+ }),
+ },
+ {
+ key: "PULSE",
+ unit: "bpm",
+ points: points(dataFrom, today, bucket, (f) => 74 - 4 * f + r()),
+ },
+ {
+ key: "BODY_FAT",
+ unit: "%",
+ points: points(dataFrom, today, bucket, (f) => 24 - 3 * f + r() * 0.4),
+ },
+ {
+ key: "ACTIVITY_STEPS",
+ unit: "steps",
+ points: points(
+ dataFrom,
+ today,
+ bucket,
+ (f) => 7200 + 900 * f + r() * 400,
+ ),
+ },
+ ];
+}
+
+/**
+ * Every series the `full` record can show: fifteen, past the five colours
+ * and past the six lines the chart used to stop at.
+ */
+export const FULL_AVAILABLE = [
+ "WEIGHT",
+ "BLOOD_PRESSURE_SYS",
+ "BLOOD_PRESSURE_DIA",
+ "PULSE",
+ "BODY_FAT",
+ "SLEEP_DURATION",
+ "ACTIVITY_STEPS",
+ "BLOOD_GLUCOSE",
+ "OXYGEN_SATURATION",
+ "HEART_RATE_VARIABILITY",
+ "RESTING_HEART_RATE",
+ "VO2_MAX",
+ "BODY_TEMPERATURE",
+ "RESPIRATORY_RATE",
+ "MOOD",
+];
+
+export function fullTimeline(today: string) {
+ const vaccinations = [
+ "2020-06-10",
+ "2021-05-20",
+ "2021-07-01",
+ "2021-12-10",
+ "2022-10-15",
+ "2023-10-20",
+ "2024-03-20",
+ "2024-04-25",
+ "2024-10-18",
+ "2025-10-10",
+ ];
+ const visits = [
+ "2019-03-10",
+ "2020-03-05",
+ "2021-09-14",
+ "2022-09-20",
+ "2023-09-12",
+ "2024-09-17",
+ "2025-09-18",
+ "2026-01-03",
+ "2026-06-22",
+ ];
+ const documents = [
+ "2019-03-12",
+ "2020-03-06",
+ "2021-09-15",
+ "2022-09-21",
+ "2022-11-09",
+ "2023-02-20",
+ "2023-09-13",
+ "2024-09-18",
+ "2025-03-14",
+ "2025-09-19",
+ "2026-01-03",
+ "2026-06-23",
+ ];
+ return {
+ zoom: "all",
+ range: {
+ from: "2019-01-01",
+ to: `${today.slice(0, 4)}-12-31`,
+ dataFrom: "2019-01-01",
+ },
+ lanes: [
+ {
+ key: "life",
+ items: [
+ item({
+ id: "le-1",
+ kind: "lifeEvent",
+ start: "2021-05-14",
+ label: "Geburt Tochter",
+ }),
+ item({
+ id: "le-2",
+ kind: "lifeEvent",
+ start: "2023-09-01",
+ precision: "MONTH",
+ label: "Umzug",
+ }),
+ item({
+ id: "le-3",
+ kind: "lifeEvent",
+ start: "2024-10-01",
+ label: "Neue Stelle",
+ }),
+ ],
+ },
+ {
+ key: "illness",
+ items: [
+ item({
+ id: "ill-c",
+ kind: "chronic",
+ start: "2019-03-10",
+ open: true,
+ label: "Bluthochdruck",
+ sub: "chronisch",
+ href: "/illness",
+ }),
+ item({
+ id: "ill-1",
+ kind: "episode",
+ start: "2020-09-01",
+ end: "2020-10-20",
+ label: "Schulter",
+ }),
+ item({
+ id: "ill-2",
+ kind: "episode",
+ start: "2022-03-08",
+ end: "2022-03-24",
+ label: "COVID-19",
+ }),
+ item({
+ id: "ill-3",
+ kind: "episode",
+ start: "2022-11-02",
+ end: "2023-02-15",
+ label: "Bandscheibe L4/L5",
+ }),
+ item({
+ id: "ill-4",
+ kind: "episode",
+ start: "2024-08-10",
+ end: "2024-08-16",
+ label: "Magen-Darm",
+ }),
+ item({
+ id: "ill-5",
+ kind: "episode",
+ start: "2025-12-31",
+ end: "2026-01-08",
+ label: "Erkältung",
+ }),
+ ],
+ },
+ {
+ key: "allergies",
+ items: [
+ item({
+ id: "al-1",
+ kind: "allergy",
+ start: "2018-01-01",
+ open: true,
+ label: "Gräserpollen",
+ sub: "seit 2008",
+ }),
+ item({
+ id: "al-2",
+ kind: "allergy",
+ start: "2020-07-15",
+ open: true,
+ label: "Penicillin",
+ }),
+ ],
+ },
+ {
+ key: "medications",
+ items: [
+ item({
+ id: "med-1",
+ group: "med-1",
+ kind: "medication",
+ start: "2019-04-02",
+ open: true,
+ label: "Ramipril",
+ sub: "2,5 mg",
+ }),
+ item({
+ id: "dose-1",
+ group: "med-1",
+ kind: "doseChange",
+ start: "2020-08-12",
+ label: "Ramipril",
+ sub: "5 mg",
+ }),
+ item({
+ id: "med-2",
+ group: "med-2",
+ kind: "medication",
+ start: "2022-11-03",
+ end: "2022-12-10",
+ label: "Ibuprofen",
+ }),
+ // One medication sent as its own span, a course, three dose
+ // changes and a pause: the case the beta drew as two medications.
+ item({
+ id: "mj",
+ group: "mj",
+ kind: "medication",
+ start: "2025-01-06",
+ open: true,
+ label: "Mounjaro",
+ sub: "7,5 mg",
+ }),
+ item({
+ id: "mj-course",
+ group: "mj",
+ kind: "course",
+ start: "2025-01-06",
+ open: true,
+ label: "Mounjaro",
+ }),
+ ...(
+ [
+ ["mj-25", "2025-01-06", "2,5 mg"],
+ ["mj-5", "2025-03-03", "5 mg"],
+ ["mj-75", "2025-06-02", "7,5 mg"],
+ ] as const
+ ).map(([id, start, sub]) =>
+ item({
+ id,
+ group: "mj",
+ kind: "doseChange",
+ start,
+ label: "Mounjaro",
+ sub,
+ }),
+ ),
+ item({
+ id: "mj-pause",
+ group: "mj",
+ kind: "pause",
+ start: "2025-08-01",
+ end: "2025-08-20",
+ label: "Mounjaro",
+ }),
+ item({
+ id: "c-1",
+ group: "vit-d",
+ kind: "course",
+ start: "2023-11-01",
+ end: "2024-03-31",
+ label: "Vitamin D (Winter)",
+ }),
+ item({
+ id: "c-2",
+ group: "vit-d",
+ kind: "course",
+ start: "2024-11-01",
+ end: "2025-03-31",
+ label: "Vitamin D (Winter)",
+ }),
+ item({
+ id: "c-3",
+ group: "vit-d",
+ kind: "course",
+ start: "2025-11-01",
+ end: "2026-03-31",
+ label: "Vitamin D (Winter)",
+ }),
+ ],
+ },
+ {
+ key: "vaccinations",
+ items: vaccinations.map((d, i) =>
+ item({
+ id: `vac-${i}`,
+ kind: "vaccination",
+ start: d,
+ label: "Impfung",
+ }),
+ ),
+ },
+ {
+ key: "visits",
+ items: [
+ ...visits.map((d, i) =>
+ item({
+ id: `v-${i}`,
+ kind: "visit",
+ start: d,
+ label: "Hausarztpraxis",
+ }),
+ ),
+ item({
+ id: "p-1",
+ kind: "procedure",
+ start: "2022-11-08",
+ label: "MRT Lendenwirbelsäule",
+ }),
+ item({
+ id: "p-2",
+ kind: "procedure",
+ start: "2025-03-12",
+ label: "Darmspiegelung",
+ }),
+ ],
+ },
+ {
+ key: "documents",
+ items: documents.map((d, i) =>
+ item({ id: `doc-${i}`, kind: "document", start: d, label: "Befund" }),
+ ),
+ },
+ ],
+ standing: [],
+ bucket: "quarter" as Bucket,
+ series: series("2019-01-01", today, "quarter"),
+ availableSeries: FULL_AVAILABLE,
+ notable: [{ date: "2026-01-04", kind: "extremeHigh" }],
+ };
+}
+
+export function readyTimeline(today: string) {
+ return {
+ zoom: "all",
+ range: {
+ from: "2024-01-01",
+ to: `${today.slice(0, 4)}-12-31`,
+ dataFrom: "2024-05-01",
+ },
+ lanes: [
+ {
+ key: "illness",
+ items: [
+ item({
+ id: "ill-c",
+ kind: "chronic",
+ start: "2019-03-10",
+ open: true,
+ label: "Bluthochdruck",
+ sub: "chronisch",
+ }),
+ item({
+ id: "ill-4",
+ kind: "episode",
+ start: "2024-08-10",
+ end: "2024-08-16",
+ label: "Magen-Darm",
+ }),
+ item({
+ id: "ill-5",
+ kind: "episode",
+ start: "2025-12-31",
+ end: "2026-01-08",
+ label: "Erkältung",
+ }),
+ ],
+ },
+ {
+ key: "medications",
+ items: [
+ item({
+ id: "med-1",
+ group: "med-1",
+ kind: "medication",
+ start: "2024-05-01",
+ open: true,
+ startKnown: false,
+ label: "Ramipril",
+ sub: "5 mg",
+ }),
+ item({
+ id: "c-2",
+ group: "vit-d",
+ kind: "course",
+ start: "2024-11-01",
+ end: "2025-03-31",
+ label: "Vitamin D (Winter)",
+ }),
+ item({
+ id: "c-3",
+ group: "vit-d",
+ kind: "course",
+ start: "2025-11-01",
+ end: "2026-03-31",
+ label: "Vitamin D (Winter)",
+ }),
+ ],
+ },
+ {
+ key: "visits",
+ items: [
+ item({
+ id: "v-1",
+ kind: "visit",
+ start: "2024-09-17",
+ label: "Hausarztpraxis",
+ }),
+ item({
+ id: "p-2",
+ kind: "procedure",
+ start: "2025-03-12",
+ label: "Darmspiegelung",
+ }),
+ item({
+ id: "v-2",
+ kind: "visit",
+ start: "2025-09-18",
+ label: "Hausarztpraxis",
+ }),
+ item({
+ id: "v-3",
+ kind: "visit",
+ start: "2026-01-03",
+ label: "Hausarztpraxis",
+ }),
+ item({
+ id: "v-4",
+ kind: "visit",
+ start: "2026-06-22",
+ label: "Hausarztpraxis",
+ }),
+ ],
+ },
+ {
+ key: "documents",
+ items: [
+ "2024-09-18",
+ "2025-03-14",
+ "2025-09-19",
+ "2026-01-03",
+ "2026-06-23",
+ ].map((d, i) =>
+ item({ id: `doc-${i}`, kind: "document", start: d, label: "Befund" }),
+ ),
+ },
+ ],
+ standing: [],
+ bucket: "quarter" as Bucket,
+ series: series("2024-05-01", today, "quarter").filter(
+ (s) => s.key !== "RESTING_HEART_RATE",
+ ),
+ availableSeries: ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "WEIGHT"],
+ notable: [],
+ };
+}
+
+export function fullReadiness() {
+ return {
+ verdict: "carries",
+ since: "2019-03-01",
+ lanes: [
+ {
+ key: "values",
+ status: "carries",
+ count: 3,
+ detail: { key: "values", params: { count: 3, since: "2019-03-01" } },
+ gaps: [],
+ },
+ {
+ key: "illness",
+ status: "carries",
+ count: 6,
+ detail: { key: "illness", params: { count: 6, chronic: 1 } },
+ gaps: [],
+ },
+ {
+ key: "visits",
+ status: "carries",
+ count: 11,
+ detail: { key: "visits", params: { count: 11, procedures: 2 } },
+ gaps: [],
+ },
+ {
+ key: "documents",
+ status: "carries",
+ count: 14,
+ detail: { key: "documents", params: { count: 14 } },
+ gaps: [],
+ },
+ {
+ key: "labs",
+ status: "carries",
+ count: 5,
+ detail: { key: "labs", params: { count: 5 } },
+ gaps: [],
+ },
+ {
+ key: "medications",
+ status: "thin",
+ count: 5,
+ detail: { key: "medications", params: { count: 5, missingStart: 3 } },
+ gaps: [
+ { key: "medicationsWithoutStart", count: 3, href: "/medications" },
+ ],
+ },
+ {
+ key: "vaccinations",
+ status: "empty",
+ count: 0,
+ detail: { key: "empty", params: {} },
+ gaps: [{ key: "vaccinationsEmpty", count: 0, href: "/vaccinations" }],
+ },
+ {
+ key: "life",
+ status: "empty",
+ count: 0,
+ detail: null,
+ gaps: [
+ { key: "lifeEventsEmpty", count: 0, href: "/timeline?add=lifeEvent" },
+ ],
+ },
+ ],
+ };
+}
+
+export function readyReadiness() {
+ return {
+ verdict: "carries",
+ since: "2024-05-01",
+ lanes: [
+ {
+ key: "values",
+ status: "carries",
+ count: 2,
+ detail: { key: "values", params: { count: 2, since: "2024-05-01" } },
+ gaps: [],
+ },
+ {
+ key: "illness",
+ status: "carries",
+ count: 3,
+ detail: { key: "illness", params: { count: 3, chronic: 1 } },
+ gaps: [],
+ },
+ {
+ key: "visits",
+ status: "carries",
+ count: 5,
+ detail: { key: "visits", params: { count: 5, procedures: 1 } },
+ gaps: [],
+ },
+ {
+ key: "documents",
+ status: "carries",
+ count: 5,
+ detail: { key: "documents", params: { count: 5 } },
+ gaps: [],
+ },
+ {
+ key: "medications",
+ status: "thin",
+ count: 3,
+ detail: { key: "medications", params: { count: 3, missingStart: 3 } },
+ gaps: [
+ { key: "medicationsWithoutStart", count: 3, href: "/medications" },
+ ],
+ },
+ {
+ key: "life",
+ status: "empty",
+ count: 0,
+ detail: null,
+ gaps: [
+ { key: "lifeEventsEmpty", count: 0, href: "/timeline?add=lifeEvent" },
+ ],
+ },
+ {
+ key: "vaccinations",
+ status: "empty",
+ count: 0,
+ detail: { key: "empty", params: {} },
+ gaps: [{ key: "vaccinationsEmpty", count: 0, href: "/vaccinations" }],
+ },
+ ],
+ };
+}
+
+const LIFE_EVENTS = [
+ {
+ id: "le-1",
+ category: "FAMILY",
+ startDate: "2021-05-14",
+ endDate: null,
+ precision: "DAY",
+ title: "Geburt Tochter",
+ note: null,
+ },
+ {
+ id: "le-2",
+ category: "HOME",
+ startDate: "2023-09-01",
+ endDate: null,
+ precision: "MONTH",
+ title: "Umzug",
+ note: null,
+ },
+ {
+ id: "le-3",
+ category: "WORK",
+ startDate: "2024-10-01",
+ endDate: null,
+ precision: "DAY",
+ title: "Neue Stelle",
+ note: null,
+ },
+].map((e) => ({
+ ...e,
+ createdAt: "2026-01-01T00:00:00.000Z",
+ updatedAt: "2026-01-01T00:00:00.000Z",
+}));
+
+function json(route: Route, status: number, data: unknown) {
+ return route.fulfill({
+ status,
+ contentType: "application/json",
+ body: JSON.stringify({ data, error: null }),
+ });
+}
+
+export interface TimelineMockLog {
+ timelineQueries: URLSearchParams[];
+ lifeEventPosts: unknown[];
+}
+
+/**
+ * Answer the timeline routes from the `full` or `ready` record. Returns a
+ * log of what the page asked for and posted, for assertions.
+ *
+ * `onlyOld`: the record's entries all lie before the last year, so a
+ * quarter or year window answers empty (with the record's `dataFrom`) and
+ * only everything, or a chosen range, carries them.
+ *
+ * `dataFrom`: the record's first entry as the answers name it, to stand in
+ * for a young record.
+ */
+export async function mockTimeline(
+ page: Page,
+ state: "full" | "ready",
+ today: string,
+ options: { onlyOld?: boolean; dataFrom?: string } = {},
+): Promise {
+ const log: TimelineMockLog = { timelineQueries: [], lifeEventPosts: [] };
+ await page.route(/\/api\/timeline\/readiness(\?|$)/, (route) =>
+ json(route, 200, state === "full" ? fullReadiness() : readyReadiness()),
+ );
+ await page.route(/\/api\/timeline(\?|$)/, (route) => {
+ const url = new URL(route.request().url());
+ log.timelineQueries.push(url.searchParams);
+ const body = state === "full" ? fullTimeline(today) : readyTimeline(today);
+ const values = (url.searchParams.get("values") ?? "")
+ .split(",")
+ .filter(Boolean);
+ const zoom = url.searchParams.get("zoom") ?? "all";
+ const from = url.searchParams.get("from");
+ const to = url.searchParams.get("to");
+ const bucket = bucketFor(zoom, from, to);
+ const dataFrom =
+ options.dataFrom ?? (state === "full" ? "2019-01-01" : "2024-05-01");
+ if (options.onlyOld && (zoom === "quarter" || zoom === "year")) {
+ return json(route, 200, {
+ ...body,
+ zoom,
+ bucket,
+ range: { from, to, dataFrom },
+ lanes: body.lanes.map((lane) => ({ ...lane, items: [] })),
+ standing: [],
+ series: values.map((key) => ({ key, unit: null, points: [] })),
+ });
+ }
+ // A chosen range answers its own window, as the server does.
+ const chosen = zoom === "range" && from && to;
+ const all =
+ !chosen && bucket === body.bucket
+ ? body.series
+ : series(chosen ? from : dataFrom, chosen ? to : today, bucket);
+ return json(route, 200, {
+ ...body,
+ zoom,
+ bucket,
+ ...(chosen
+ ? { range: { from, to, dataFrom } }
+ : options.dataFrom
+ ? { range: { ...body.range, dataFrom } }
+ : {}),
+ // Any other value the menu offers answers a flat line of its own.
+ series: values.map(
+ (key) =>
+ all.find((s) => s.key === key) ?? {
+ key,
+ unit: null,
+ points: points(dataFrom, today, bucket, (f) => 50 + 10 * f),
+ },
+ ),
+ });
+ });
+ await page.route(/\/api\/life-events(\/[^/?]+)?(\?|$)/, async (route) => {
+ const method = route.request().method();
+ if (method === "GET") {
+ return json(route, 200, { events: state === "full" ? LIFE_EVENTS : [] });
+ }
+ if (method === "POST") {
+ const posted = route.request().postDataJSON() as Record;
+ log.lifeEventPosts.push(posted);
+ return json(route, 201, {
+ id: "le-new",
+ endDate: null,
+ note: null,
+ ...posted,
+ createdAt: "2026-10-01T00:00:00.000Z",
+ updatedAt: "2026-10-01T00:00:00.000Z",
+ });
+ }
+ return json(route, 200, { deleted: true });
+ });
+ return log;
+}
diff --git a/e2e/v1341-ui-evidence.spec.ts b/e2e/v1341-ui-evidence.spec.ts
index ef9e150f9..8d128c71e 100644
--- a/e2e/v1341-ui-evidence.spec.ts
+++ b/e2e/v1341-ui-evidence.spec.ts
@@ -30,7 +30,12 @@ const NARRATIVE_DIGEST: DailyDigest = {
composition: ["BLOOD_PRESSURE", "ACTIVITY", "SLEEP"],
},
topSignal: LONG_HEADLINE_BRIEFING.signalsOfDay?.[0] ?? null,
+ // The lead talks about blood pressure, so the server keeps only the delta.
+ signalLine: { headline: null, delta: "−12 mmHg vs. 30-Tage-Mittel" },
briefingLead: LONG_HEADLINE_BRIEFING.paragraph,
+ lead: { text: LONG_HEADLINE_BRIEFING.paragraph, source: "briefing" },
+ today: [],
+ restMode: null,
line: LONG_HEADLINE_BRIEFING.paragraph,
worthALook: [],
justIn: null,
@@ -104,6 +109,7 @@ test.describe("v1.34.1 deterministic UI evidence", () => {
...NARRATIVE_DIGEST,
topSignal: null,
briefingLead: null,
+ lead: null,
line: "Dein Gesundheitsscore liegt heute bei 84.",
reactionLine: null,
};
diff --git a/e2e/v137-sharing-managed-profiles.spec.ts b/e2e/v137-sharing-managed-profiles.spec.ts
index 6cf1ffbd0..f4e225f74 100644
--- a/e2e/v137-sharing-managed-profiles.spec.ts
+++ b/e2e/v137-sharing-managed-profiles.spec.ts
@@ -503,13 +503,20 @@ test.describe.serial("scoped sharing browser journeys", () => {
await expect(banner).toHaveAttribute("data-record-kind", "shared");
await page.goto("/measurements");
- await expect(page.locator('[data-slot="measurement-add"]')).toBeVisible();
+ // An empty list carries its add action in the empty state rather than
+ // the header, so the write grant shows whichever of the two applies.
+ const addControl = page
+ .locator(
+ '[data-slot="measurement-add"], [data-slot="measurement-add-first"]',
+ )
+ .first();
+ await expect(addControl).toBeVisible();
const post = page.waitForResponse(
(response) =>
response.request().method() === "POST" &&
new URL(response.url()).pathname === "/api/measurements",
);
- await page.locator('[data-slot="measurement-add"]').click();
+ await addControl.click();
await page.locator("#sys").fill("124");
await page.locator("#dia").fill("78");
await page.getByRole("button", { name: /^save$/i }).click();
diff --git a/e2e/v138-managed-record-capabilities.spec.ts b/e2e/v138-managed-record-capabilities.spec.ts
index adedc711f..2a7207059 100644
--- a/e2e/v138-managed-record-capabilities.spec.ts
+++ b/e2e/v138-managed-record-capabilities.spec.ts
@@ -26,6 +26,7 @@
* the same ones, for the same reasons written there.
*/
import type { Page } from "@playwright/test";
+import pg from "pg";
import { expect, test } from "./setup/test";
import {
@@ -111,15 +112,38 @@ test.describe.serial("a guardian's controls inside a managed profile", () => {
if (!managed) throw new Error("managed record fixture is missing");
test("offers the mood add control and lands the entry", async ({ page }) => {
+ // The profile starts every run without mood entries. The form stamps an
+ // entry to the minute and `(user, date, moodLoggedAt)` is unique, so a
+ // second run inside the same minute (a retry, a repeat) met a 409 for the
+ // entry the first one wrote. An empty record also fixes which control the
+ // page offers: the empty state's add, not the header's.
+ const dbUrl = process.env.DATABASE_URL;
+ test.skip(!dbUrl, "DATABASE_URL is required to reset the profile's mood");
+ const pool = new pg.Pool({ connectionString: dbUrl });
+ try {
+ await pool.query(
+ `DELETE FROM mood_entries
+ WHERE user_id = (SELECT id FROM users WHERE username = $1)`,
+ [managed.username],
+ );
+ } finally {
+ await pool.end();
+ }
+
await openRecord(page, managed.username);
const banner = page.locator('[data-slot="shared-record-banner"]');
await expect(banner).toHaveAttribute("data-record-kind", "managed");
await expect(banner).toHaveAttribute("data-access-level", "manage");
await page.goto("/mood");
- const add = page.locator('[data-slot="mood-add-entry"]');
- await expect(add).toBeVisible({ timeout: 30_000 });
- await add.click();
+ // Since v1.42 an empty list carries the one add control in its empty
+ // state and the header drops its own; the header's returns once there is
+ // an entry to list. Both are asserted on their data-slots.
+ const addFirst = page.locator('[data-slot="mood-add-first"]');
+ const addHeader = page.locator('[data-slot="mood-add-entry"]');
+ await expect(addFirst).toBeVisible({ timeout: 30_000 });
+ await expect(addHeader).toHaveCount(0);
+ await addFirst.click();
const face = page.locator('[data-slot="mood-face"][data-mood="GUT"]');
await expect(face).toBeVisible();
@@ -139,6 +163,8 @@ test.describe.serial("a guardian's controls inside a managed profile", () => {
await expect(page.locator('[data-slot="mood-rows"]').first()).toBeVisible({
timeout: 30_000,
});
+ await expect(addHeader).toBeVisible();
+ await expect(addFirst).toHaveCount(0);
await leaveRecord(page);
});
diff --git a/e2e/v142-managed-profile-handover.spec.ts b/e2e/v142-managed-profile-handover.spec.ts
new file mode 100644
index 000000000..fc722b35e
--- /dev/null
+++ b/e2e/v142-managed-profile-handover.spec.ts
@@ -0,0 +1,258 @@
+/**
+ * v1.42 (#959) — a managed profile handed over to the person it describes,
+ * end to end in a browser: the Guardian mints the link, the person opens it in
+ * a browser that has never signed in, chooses a sign-in, lands on the decision
+ * about their former Guardian, and decides.
+ *
+ * The link is never mailed (by design: it is shown once and handed over in
+ * person or scanned). This journey "mails" it by reading it off the panel's
+ * `data-handover-url` and opening it in a second, cookie-less browser context,
+ * which is exactly the hop a person makes with a phone.
+ *
+ * Its own Guardian account, per project, seeded here: the shared
+ * `e2e-guardian` belongs to the managed-profile journey, which clears every
+ * profile that account looks after, and the two would delete each other's
+ * records mid-run. Every assertion addresses a stable `data-*` attribute or
+ * the database, never rendered copy.
+ */
+import AxeBuilder from "@axe-core/playwright";
+import { hash } from "@node-rs/argon2";
+import type { Page } from "@playwright/test";
+import pg from "pg";
+
+import { expect, test } from "./setup/test";
+
+const PASSWORD = "Handover!Journey-2042";
+
+function connect(): pg.Pool {
+ const url = process.env.DATABASE_URL;
+ if (!url) throw new Error("[handover-e2e] DATABASE_URL is not set");
+ return new pg.Pool({ connectionString: url });
+}
+
+/** WCAG 2.1 AA on the screen as it stands. */
+async function expectNoAxeViolations(page: Page): Promise {
+ const result = await new AxeBuilder({ page })
+ .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
+ .analyze();
+ expect(result.violations).toEqual([]);
+}
+
+function names(project: string) {
+ const slug = project
+ .replace(/[^a-z0-9]/gi, "")
+ .toLowerCase()
+ .slice(0, 12);
+ return {
+ guardian: `e2e-hg-${slug}`,
+ claimant: `e2e-hc-${slug}`,
+ };
+}
+
+/**
+ * A Guardian with a password, signed in through the real login route, then
+ * given a second factor and a session that has just proved it — the state
+ * `requireFreshMfa` needs and the only state in which a handover can be made.
+ */
+async function seedGuardian(
+ pool: pg.Pool,
+ username: string,
+ claimant: string,
+): Promise {
+ // Whatever a previous run left: the claimed account (no longer a managed
+ // profile, so it does not cascade with the guardian) and the guardian.
+ await pool.query(`DELETE FROM users WHERE username = $1 OR email = $2`, [
+ claimant,
+ `${claimant}@healthlog.test`,
+ ]);
+ await pool.query(
+ `DELETE FROM users WHERE managed_profile_at IS NOT NULL AND id IN (
+ SELECT g.grantor_id FROM account_grants g JOIN users u ON u.id = g.grantee_id
+ WHERE u.username = $1)`,
+ [username],
+ );
+ await pool.query(`DELETE FROM users WHERE username = $1`, [username]);
+ const passwordHash = await hash(PASSWORD, {
+ memoryCost: 19456,
+ timeCost: 2,
+ outputLen: 32,
+ parallelism: 1,
+ });
+ await pool.query(
+ `INSERT INTO users
+ (id, username, email, password_hash, role, created_at, updated_at,
+ onboarding_completed_at, onboarding_tour_completed,
+ disclaimer_acknowledged_at, disclaimer_acknowledged_version)
+ VALUES ($1, $2, $3, $4, 'USER', NOW(), NOW(), NOW(), true, NOW(), '1')`,
+ [
+ `c${Date.now().toString(36)}${username}`,
+ username,
+ `${username}@healthlog.test`,
+ passwordHash,
+ ],
+ );
+ // The claim and login buckets for this address, so a local re-run inside the
+ // window meets the product rather than its rate limit.
+ await pool.query(
+ `DELETE FROM rate_limits WHERE key LIKE 'auth:claim%' OR key LIKE 'managed-profile:%'`,
+ );
+}
+
+async function stampSecondFactor(pool: pg.Pool, username: string) {
+ await pool.query(
+ `UPDATE users SET totp_confirmed_at = NOW() WHERE username = $1`,
+ [username],
+ );
+ await pool.query(
+ `UPDATE sessions SET mfa_verified_at = NOW()
+ WHERE user_id = (SELECT id FROM users WHERE username = $1)`,
+ [username],
+ );
+}
+
+test.describe("managed profile handover", () => {
+ test("guardian hands over, the person claims and decides", async ({
+ page,
+ browser,
+ baseURL,
+ }, testInfo) => {
+ test.setTimeout(120_000);
+ const { guardian, claimant } = names(testInfo.project.name);
+ const pool = connect();
+ try {
+ await seedGuardian(pool, guardian, claimant);
+
+ // ── The Guardian ─────────────────────────────────────────────────
+ const login = await page.request.post("/api/auth/login", {
+ data: { email: guardian, password: PASSWORD },
+ });
+ expect(login.status()).toBe(200);
+ await stampSecondFactor(pool, guardian);
+
+ const created = await page.request.post("/api/managed-profiles", {
+ data: {
+ displayName: "Robin",
+ dateOfBirth: "2012-03-04",
+ locale: "en",
+ timezone: "Europe/Berlin",
+ gender: null,
+ },
+ });
+ expect(created.status()).toBe(201);
+ const profileId = ((await created.json()) as { data: { id: string } })
+ .data.id;
+
+ await page.goto("/settings/access");
+ const row = page.locator(
+ `[data-slot="managed-profile-row"][data-managed-profile-id="${profileId}"]`,
+ );
+ await expect(row).toBeVisible();
+ await row
+ .locator('[data-slot="managed-profile-handover-toggle"]')
+ .click();
+
+ const form = row.locator('[data-slot="managed-profile-handover-form"]');
+ await expect(form).toBeVisible();
+ // Born 2012: the minor hint is shown, the choice stays the Guardian's.
+ await expect(
+ row.locator('[data-slot="managed-profile-handover-minor"]'),
+ ).toBeVisible();
+ // The Guardian proposes to keep managing.
+ await form
+ .locator('[data-slot="managed-profile-handover-proposal"]')
+ .first()
+ .selectOption("manage");
+ await form
+ .locator('[data-slot="managed-profile-handover-create"]')
+ .click();
+
+ const linkPanel = row.locator(
+ '[data-slot="managed-profile-handover-link"]',
+ );
+ await expect(linkPanel).toBeVisible();
+ await expect(
+ row.locator('[data-slot="managed-profile-handover-qr"]'),
+ ).toBeVisible();
+ const url = await linkPanel.getAttribute("data-handover-url");
+ expect(url).toMatch(/\/claim\/hlp_[0-9a-f]{64}$/);
+ await expectNoAxeViolations(page);
+
+ // ── The person, on a browser that never signed in ────────────────
+ const person = await browser.newContext({ baseURL });
+ try {
+ const claimPage = await person.newPage();
+ const path = new URL(url as string).pathname;
+ await claimPage.goto(path);
+ await expect(claimPage).toHaveURL(/\/auth\/claim\?token=hlp_/);
+ await expect(
+ claimPage.locator('[data-testid="claim-form-card"]'),
+ ).toBeVisible();
+ await expect(
+ claimPage.locator(
+ '[data-slot="claim-guardian"][data-proposal="manage"]',
+ ),
+ ).toHaveCount(1);
+
+ await expectNoAxeViolations(claimPage);
+ await claimPage.locator("#claim-username").fill(claimant);
+ await claimPage
+ .locator("#claim-email")
+ .fill(`${claimant}@healthlog.test`);
+ await claimPage.locator("#claim-password").fill(PASSWORD);
+ await claimPage.locator('[data-slot="claim-submit"]').click();
+
+ await expect(claimPage).toHaveURL(/\/onboarding\/handover$/, {
+ timeout: 30_000,
+ });
+ const decision = claimPage.locator(
+ '[data-slot="handover-decision-guardian"][data-decidable="true"]',
+ );
+ await expect(decision).toHaveCount(1);
+ await expect(
+ claimPage.locator('[data-slot="handover-notification-hint"]'),
+ ).toBeVisible();
+ await expectNoAxeViolations(claimPage);
+ // The person decides: view only, not managing.
+ await decision
+ .locator('[data-slot="handover-decision-choice"]')
+ .selectOption("read");
+ await claimPage
+ .locator('[data-slot="handover-decision-submit"]')
+ .click();
+ await expect(claimPage).toHaveURL(/\/onboarding(\?|$)/, {
+ timeout: 30_000,
+ });
+
+ // The used link is dead for anyone after.
+ const reused = await person.request.post("/api/auth/claim/preview", {
+ data: { token: (url as string).split("/claim/")[1] },
+ });
+ // A signed-in browser is refused before the token is read; a fresh one
+ // would get the uniform 404. Either way, nothing is handed over twice.
+ expect([404, 409]).toContain(reused.status());
+ } finally {
+ await person.close();
+ }
+
+ // ── What the database says ───────────────────────────────────────
+ const account = await pool.query(
+ `SELECT managed_profile_at, username, email, password_hash IS NOT NULL AS has_password
+ FROM users WHERE id = $1`,
+ [profileId],
+ );
+ expect(account.rows[0]).toMatchObject({
+ managed_profile_at: null,
+ username: claimant,
+ has_password: true,
+ });
+ const live = await pool.query(
+ `SELECT g.access FROM account_grants g JOIN users u ON u.id = g.grantee_id
+ WHERE g.grantor_id = $1 AND u.username = $2 AND g.revoked_at IS NULL`,
+ [profileId, guardian],
+ );
+ expect(live.rows).toEqual([{ access: "READ" }]);
+ } finally {
+ await pool.end();
+ }
+ });
+});
diff --git a/e2e/v142-qol.spec.ts b/e2e/v142-qol.spec.ts
new file mode 100644
index 000000000..a252891b7
--- /dev/null
+++ b/e2e/v142-qol.spec.ts
@@ -0,0 +1,384 @@
+/**
+ * v1.42 — three surfaces made easier to live with.
+ *
+ * - Documents: the display choice behind the wrench (cards or list, months
+ * stacked or flowing) persists per account; every delete asks first; the
+ * selection is cleared with an X on the count line.
+ * - Wellness scores: every score tile shows its course; a score's detail
+ * page carries its history right under the score card and no longer the
+ * warning-coloured "not clinical" line.
+ * - Mood insights: the calendar and the line chart appear together without
+ * moving the page, and logging is the header plus every Insights page
+ * uses.
+ *
+ * Everything is addressed through stable data attributes, never copy.
+ */
+import type { Page } from "@playwright/test";
+import { expect, test } from "./setup/test";
+
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+import { ensureVaultFixture, seedNamespaceDocs } from "./setup/vault-fixture";
+import { mockPopulatedInsights } from "./utils/mock-populated-insights";
+
+test.use({ storageState: STORAGE_STATE_PATH });
+
+async function resetDocumentsLayout(page: Page): Promise {
+ const res = await page.request.put("/api/documents/inbound/layout", {
+ data: { version: 1, view: "cards", arrangement: "stacked" },
+ });
+ expect(res.status()).toBe(200);
+}
+
+test.describe("documents", () => {
+ test.beforeAll(async () => {
+ await ensureVaultFixture();
+ });
+
+ test("the display choice behind the wrench persists across visits", async ({
+ page,
+ isMobile,
+ }) => {
+ // Both projects share one account; the layout is account state, so one
+ // project owns this journey.
+ test.skip(isMobile, "account-level preference; one project owns it");
+ await resetDocumentsLayout(page);
+ try {
+ await page.goto("/documents");
+ const timeline = page.locator('[data-slot="document-timeline"]');
+ await expect(timeline).toHaveAttribute("data-view", "cards");
+ await expect(timeline).toHaveAttribute("data-arrangement", "stacked");
+
+ await page.locator('[data-slot="documents-customize"]').click();
+ await expect(page).toHaveURL(/\/settings\/layout\/documents$/);
+ const settings = page.locator('[data-slot="documents-layout-settings"]');
+ await settings.locator('[data-slot="module-view-list"]').click();
+ await expect(
+ settings.locator('[data-slot="module-view-list"]'),
+ ).toHaveAttribute("aria-pressed", "true");
+ const flow = settings.locator('[data-slot="documents-arrangement-flow"]');
+ // Wait for the write to land, so the reload below reads it back.
+ const saved = page.waitForResponse(
+ (r) =>
+ r.url().includes("/api/documents/inbound/layout") &&
+ r.request().method() === "PUT",
+ );
+ await flow.click();
+ expect((await saved).status()).toBe(200);
+
+ await page.goto("/documents");
+ await expect(timeline).toHaveAttribute("data-view", "list");
+ await expect(timeline).toHaveAttribute("data-arrangement", "flow");
+ await expect(
+ timeline
+ .locator('[data-slot="document-card"][data-variant="row"]')
+ .first(),
+ ).toBeVisible();
+ // Flowing: no month gets a row of its own, the name rides inline.
+ await expect(
+ timeline.locator('[data-slot="document-flow-month"]').first(),
+ ).toBeVisible();
+
+ // A fresh visit in a fresh page — the choice is the account's, not
+ // this tab's.
+ const second = await page.context().newPage();
+ await second.goto("/documents");
+ await expect(
+ second.locator('[data-slot="document-timeline"]'),
+ ).toHaveAttribute("data-arrangement", "flow");
+ await second.close();
+ } finally {
+ await resetDocumentsLayout(page);
+ }
+ });
+
+ test("bulk delete asks first, and cancelling deletes nothing", async ({
+ page,
+ isMobile,
+ }) => {
+ test.skip(isMobile, "selection by checkbox is a pointer flow");
+ await seedNamespaceDocs("qoldel", 2);
+ await page.goto("/documents?q=qoldel");
+ const box = page.getByRole("checkbox", { name: "Select qoldel 002" });
+ await expect(box).toBeVisible();
+ await box.click();
+
+ let deletes = 0;
+ page.on("request", (req) => {
+ if (
+ req.method() === "POST" &&
+ req.url().includes("/api/documents/inbound/bulk") &&
+ (req.postData() ?? "").includes('"delete"')
+ ) {
+ deletes += 1;
+ }
+ });
+
+ const bar = page.locator('[data-slot="document-bulk-bar"]');
+ await bar.locator('[data-slot="document-bulk-delete"]').click();
+ const confirm = page.locator('[data-slot="documents-delete-confirm"]');
+ await expect(confirm).toBeVisible();
+ await page
+ .getByRole("alertdialog")
+ .getByRole("button", { name: "Cancel" })
+ .click();
+ await expect(confirm).toBeHidden();
+ expect(deletes).toBe(0);
+ await expect(
+ page.getByRole("button", { name: "Open qoldel 002" }),
+ ).toBeVisible();
+
+ await bar.locator('[data-slot="document-bulk-delete"]').click();
+ await confirm.click();
+ await expect(
+ page.getByRole("button", { name: "Open qoldel 002" }),
+ ).toBeHidden();
+ expect(deletes).toBe(1);
+ });
+
+ test("the Delete key on a focused card asks first too", async ({
+ page,
+ isMobile,
+ }) => {
+ test.skip(isMobile, "keyboard flow");
+ await seedNamespaceDocs("qolkey", 1);
+ await page.goto("/documents?q=qolkey");
+ const open = page.getByRole("button", { name: "Open qolkey 001" });
+ await expect(open).toBeVisible();
+ await open.focus();
+ await page.keyboard.press("Delete");
+ await expect(
+ page.locator('[data-slot="documents-delete-confirm"]'),
+ ).toBeVisible();
+ await page.keyboard.press("Escape");
+ await expect(open).toBeVisible();
+ });
+
+ test("the selection clears with the X on the count line", async ({
+ page,
+ isMobile,
+ }) => {
+ test.skip(isMobile, "selection by checkbox is a pointer flow");
+ await seedNamespaceDocs("qolclear", 2);
+ await page.goto("/documents?q=qolclear");
+ await page.getByRole("checkbox", { name: "Select qolclear 001" }).click();
+
+ const bar = page.locator('[data-slot="document-bulk-bar"]');
+ await expect(bar).toBeVisible();
+ const count = bar.locator('[data-slot="document-bulk-count"]');
+ const clear = bar.locator('[data-slot="document-bulk-clear"]');
+ const actions = bar.locator('[data-slot="document-bulk-actions"]');
+
+ // The X sits on the count line, at its right edge, above the action row,
+ // and the action row follows the count line without a gap of its own.
+ const [countBox, clearBox, actionsBox, barBox] = await Promise.all([
+ count.boundingBox(),
+ clear.boundingBox(),
+ actions.boundingBox(),
+ bar.boundingBox(),
+ ]);
+ expect(countBox && clearBox && actionsBox && barBox).toBeTruthy();
+ const mid = (b: { y: number; height: number }) => b.y + b.height / 2;
+ expect(Math.abs(mid(clearBox!) - mid(countBox!))).toBeLessThan(6);
+ expect(
+ barBox!.x + barBox!.width - (clearBox!.x + clearBox!.width),
+ ).toBeLessThan(20);
+ expect(
+ actionsBox!.y - (clearBox!.y + clearBox!.height),
+ ).toBeLessThanOrEqual(10);
+ // One action row: every verb shares a top edge.
+ const tops = await actions
+ .locator("button")
+ .evaluateAll((els) =>
+ els.map((el) => Math.round(el.getBoundingClientRect().top)),
+ );
+ expect(new Set(tops).size).toBe(1);
+
+ await clear.click();
+ await expect(bar).toBeHidden();
+ });
+});
+
+test.describe("wellness scores", () => {
+ test("every score tile shows its course", async ({ page }) => {
+ await mockPopulatedInsights(page);
+ await page.goto("/insights");
+ for (const metric of ["RECOVERY_SCORE", "STRESS_SCORE"]) {
+ const tile = page.locator(
+ `[data-slot="wellness-score-tile"][data-metric="${metric}"]`,
+ );
+ await tile.scrollIntoViewIfNeeded();
+ await expect(
+ tile.locator('[data-slot="wellness-score-history"]'),
+ ).toBeVisible();
+ }
+ });
+
+ test("a score's detail page carries its history under the card, without the caveat line", async ({
+ page,
+ }) => {
+ // The e2e account has no nightly score; serve one so the page has a
+ // score to draw the history under.
+ await page.route(
+ /\/api\/insights\/derived\?metric=RECOVERY_SCORE/,
+ (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ metric: "RECOVERY_SCORE",
+ status: "ok",
+ value: {
+ score: 66,
+ band: "yellow",
+ trendDelta: 2,
+ daysInWindow: 5,
+ asOf: new Date().toISOString(),
+ series: [58, 61, 64, 60, 66],
+ anchor: null,
+ components: null,
+ },
+ coverage: {
+ requiredInputs: 1,
+ presentInputs: 1,
+ historyDays: 5,
+ missing: [],
+ },
+ confidence: { score: 80, band: "high" },
+ provenance: {
+ inputs: ["RECOVERY_SCORE"],
+ source: "live",
+ windowDays: 14,
+ computedAt: new Date().toISOString(),
+ },
+ reason: null,
+ assessment: null,
+ },
+ error: null,
+ }),
+ }),
+ );
+ await page.goto("/insights/scores/recovery");
+ const anatomy = page.locator('[data-slot="score-anatomy-view"]');
+ await expect(anatomy).toBeVisible();
+ const history = page.locator('[data-slot="score-history-chart"]');
+ await expect(history).toBeAttached();
+ const [cardBox, historyBox] = await Promise.all([
+ anatomy.boundingBox(),
+ history.boundingBox(),
+ ]);
+ expect(historyBox!.y).toBeGreaterThan(cardBox!.y + cardBox!.height - 1);
+ await expect(anatomy.locator(".text-warning")).toHaveCount(0);
+ });
+});
+
+test.describe("mood insights", () => {
+ test.beforeAll(async ({ browser }) => {
+ // A short, real history, so the calendar and the chart both have days.
+ const context = await browser.newContext({
+ storageState: STORAGE_STATE_PATH,
+ });
+ const now = Date.now();
+ for (let day = 1; day <= 12; day++) {
+ const res = await context.request.post("/api/mood-entries", {
+ data: {
+ mood: day % 3 === 0 ? "OKAY" : "GUT",
+ moodLoggedAt: new Date(now - day * 86_400_000).toISOString(),
+ source: "MANUAL",
+ externalId: `e2e-qol-mood-${day}`,
+ },
+ });
+ expect([200, 201]).toContain(res.status());
+ }
+ await context.close();
+ });
+
+ test("the calendar and the chart appear together without moving the page", async ({
+ page,
+ }) => {
+ // The calendar's read is the slower one in production; slow it down here
+ // so the old order (chart first, calendar pushing it down) would show.
+ await page.route(/\/api\/mood\/insights(\?|$)/, async (route) => {
+ await new Promise((resolve) => setTimeout(resolve, 1_200));
+ await route.continue();
+ });
+ await page.addInitScript(() => {
+ const w = window as unknown as {
+ __cls: number;
+ __seen: Record;
+ };
+ w.__cls = 0;
+ w.__seen = {};
+ new PerformanceObserver((list) => {
+ for (const entry of list.getEntries() as Array<
+ PerformanceEntry & { value: number; hadRecentInput: boolean }
+ >) {
+ if (!entry.hadRecentInput) w.__cls += entry.value;
+ }
+ }).observe({ type: "layout-shift", buffered: true });
+ const watch = () => {
+ if (
+ !w.__seen.heatmap &&
+ document.querySelector(
+ '[data-slot="mood-heatmap-body"] svg[role="img"]',
+ )
+ )
+ w.__seen.heatmap = performance.now();
+ if (
+ !w.__seen.chart &&
+ document.querySelector(
+ '[data-slot="insights-mood-chart"] .recharts-surface',
+ ) &&
+ !document.querySelector(
+ '[data-slot="insights-mood-chart"] .invisible',
+ )
+ )
+ w.__seen.chart = performance.now();
+ requestAnimationFrame(watch);
+ };
+ requestAnimationFrame(watch);
+ });
+
+ await page.goto("/insights/mood");
+ await expect(
+ page.locator('[data-slot="mood-heatmap-body"] svg[role="img"]'),
+ ).toBeAttached({
+ timeout: 15_000,
+ });
+ await expect(
+ page
+ .locator('[data-slot="insights-mood-chart"] .recharts-surface')
+ .first(),
+ ).toBeVisible();
+ await page.waitForTimeout(1_500);
+
+ const { cls, seen } = await page.evaluate(() => {
+ const w = window as unknown as {
+ __cls: number;
+ __seen: Record;
+ };
+ return { cls: w.__cls, seen: w.__seen };
+ });
+ expect(cls).toBeLessThan(0.02);
+ expect(seen.heatmap).toBeDefined();
+ expect(seen.chart).toBeDefined();
+ // Together: within a couple of frames of each other.
+ expect(Math.abs(seen.heatmap - seen.chart)).toBeLessThan(100);
+ });
+
+ test("logging is the header plus, not a text link", async ({ page }) => {
+ await page.goto("/insights/mood");
+ const add = page.locator(
+ '[data-slot="insights-mood-add"][data-header-add]',
+ );
+ await expect(add).toBeVisible();
+ await expect(
+ page.locator('[data-slot="insights-mood-log-link"]'),
+ ).toHaveCount(0);
+ await expect(
+ page.locator('[data-slot="insights-mood-mental-wellbeing-link"]'),
+ ).toHaveCount(0);
+ await add.click();
+ await expect(page.getByRole("dialog")).toBeVisible();
+ });
+});
diff --git a/e2e/vaccinations.spec.ts b/e2e/vaccinations.spec.ts
index fbf2f2199..40b43f27c 100644
--- a/e2e/vaccinations.spec.ts
+++ b/e2e/vaccinations.spec.ts
@@ -84,11 +84,19 @@ async function captureDose(
booster: "confirm" | "decline" | "none";
},
): Promise {
- // The header Add renders whether or not the list is empty, so clicking it
- // works in both states. click() auto-waits for the button to be actionable,
- // which avoids racing an instant visibility read against the client render
- // (an empty-state-only branch times out once a record already exists).
- await page.locator('[data-slot="vaccination-add"]').first().click();
+ // While the Impfpass is empty its empty state carries the add action and
+ // the header drops its own, so wait for the list to settle (a record or
+ // the empty state) and click whichever add control is then on screen.
+ await expect(
+ page.locator('[data-vaccination-id], [data-slot="empty-state"]').first(),
+ ).toBeVisible();
+ await page
+ .locator(
+ '[data-slot="vaccination-add"], [data-slot="vaccination-add-empty"]',
+ )
+ .filter({ visible: true })
+ .first()
+ .click();
await expect(page.locator('[data-slot="vaccination-form"]')).toBeVisible();
@@ -321,7 +329,12 @@ test.describe("vaccinations", () => {
).toBeVisible({ timeout: 15_000 });
await expectNoSeriousAxe(page);
- await page.locator('[data-slot="vaccination-add"]').first().click();
+ await page
+ .locator(
+ '[data-slot="vaccination-add"], [data-slot="vaccination-add-empty"]',
+ )
+ .last()
+ .click();
await expect(page.locator('[data-slot="vaccination-form"]')).toBeVisible();
await expectNoSeriousAxe(page);
});
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 897750959..d1d6373ee 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -153,6 +153,7 @@ const eslintConfig = defineConfig([
"src/components/charts/workout-hr-chart.tsx",
"src/components/charts/workout-elevation-chart.tsx",
"src/components/insights/coach-panel/result-chart.tsx",
+ "src/components/insights/derived/score-trend-chart.tsx",
],
rules: {
"no-restricted-imports": [
diff --git a/knip.jsonc b/knip.jsonc
index 560b3914b..50a7b35a4 100644
--- a/knip.jsonc
+++ b/knip.jsonc
@@ -121,6 +121,14 @@
"**/__mocks__/**",
"e2e/setup/test-helpers.ts",
"src/lib/logging/index.ts",
+ // v1.42 release-branch contract stubs: each is created ahead of its
+ // consumers so the packages built in parallel share one signature. Drop
+ // a line the moment its first consumer lands; the release integration
+ // checklist removes whatever is left before the tag.
+ "src/lib/environment/air-quality-contract.ts",
+ // The day view and timeline contract (#613): constants and wire types the
+ // server, the two views and the Coach/MCP tool build against.
+ "src/lib/day/contract.ts",
],
"ignoreDependencies": [
"@types/node",
diff --git a/messages/de.json b/messages/de.json
index 0c7173452..118df8494 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -49,7 +49,9 @@
"dismiss": "Schließen",
"moreInfo": "Mehr Informationen",
"durationHoursMinutes": "{hours} Std. {minutes} Min.",
- "durationMinutes": "{minutes} Min."
+ "durationMinutes": "{minutes} Min.",
+ "durationClockMinutes": "{time} Min.",
+ "durationClockHours": "{time} Std."
},
"apiErrors": {
"measurement": {
@@ -165,7 +167,7 @@
"anamnesisShiftSchedule": "Arbeitszeitmodell",
"anamnesisColFact": "Profilangabe",
"anamnesisColValue": "Erfasster Wert",
- "subtitle": "HealthLog — Persönlicher Gesundheitsbericht",
+ "subtitle": "HealthLog: Persönlicher Gesundheitsbericht",
"patient": "Patient",
"dateOfBirth": "Geburtsdatum",
"gender": "Geschlecht",
@@ -175,18 +177,18 @@
"height": "Körpergröße",
"period": "Berichtszeitraum",
"createdOn": "Erstellt am",
- "vitalsTitle": "Vitalparameter — Übersicht",
+ "vitalsTitle": "Vitalparameter: Übersicht",
"colParameter": "Parameter",
"colCurrent": "Aktuell",
"colAvgPeriod": "Ø Zeitraum",
"colMin": "Min",
"colMax": "Max",
"colN": "n",
- "bpClassificationTitle": "Blutdruck — ESH-Klassifikation (2023)",
+ "bpClassificationTitle": "Blutdruck: ESH-Klassifikation (2023)",
"avgBp": "Durchschnittlicher Blutdruck",
"classification": "Klassifikation",
"bmiTitle": "Body-Mass-Index (BMI)",
- "bmiRow": "BMI: {bmi} kg/m² — Klassifikation: {class} (WHO)",
+ "bmiRow": "BMI: {bmi} kg/m², Klassifikation: {class} (WHO)",
"complianceTitle": "Therapietreue",
"glp1Title": "GLP-1-Therapie",
"glp1WeightSummary": "Gewicht im Berichtszeitraum: {start} kg → {end} kg ({delta} kg)",
@@ -204,7 +206,7 @@
"colExpected": "Erwartet",
"colComplianceRate": "Therapietreue",
"moodTitle": "Stimmungsverlauf",
- "moodSummary": "Durchschnitt: {avg} / 5,0 · Einträge: {count} · Bereich: {min}–{max}",
+ "moodSummary": "Durchschnitt {avg} / 5,0, {count} Einträge, Bereich {min}–{max}",
"colMood": "Stimmung",
"colCount": "Anzahl",
"colShare": "Anteil",
@@ -234,8 +236,8 @@
"typeGlucoseRandom": "Glukose (beliebig)",
"typeGlucoseBedtime": "Glukose (vor dem Schlafen)",
"typeGlucoseUnspecified": "Glukose (ohne Angabe)",
- "glucoseClassificationTitle": "Blutzucker — Referenzbereiche (ADA 2024 / DDG)",
- "glucoseRow": "{label}: Ø {avg} {unit} (Ziel {rangeMin}–{rangeMax}) — {class}",
+ "glucoseClassificationTitle": "Blutzucker: Referenzbereiche (ADA 2024 / DDG)",
+ "glucoseRow": "{label}: Ø {avg} {unit} (Ziel {rangeMin}–{rangeMax}): {class}",
"glucoseInTarget": "im Ziel",
"glucoseBelowTarget": "unter Ziel",
"glucoseAboveTarget": "über Ziel",
@@ -287,7 +289,7 @@
"gmi": "Glukose-Management-Indikator (GMI)",
"eA1c": "Geschätzter HbA1c",
"cv": "Variabilität (VK%)",
- "spotCaveat": "Dies sind Schätzwerte aus Einzelmessungen, kein Profil eines kontinuierlichen Messgeräts — als Tendenz und nicht als klinisches Urteil zu lesen."
+ "spotCaveat": "Dies sind Schätzwerte aus Einzelmessungen, kein Profil eines kontinuierlichen Messgeräts. Sie sind als Tendenz und nicht als klinisches Urteil zu lesen."
},
"illnessTitle": "Erkrankungen",
"illnessOngoing": "Andauernd",
@@ -376,12 +378,12 @@
"backToDashboard": "Zurück zum Dashboard"
},
"offlineBanner": {
- "message": "Keine Verbindung — Änderungen können erst gespeichert werden, wenn du wieder online bist.",
- "saveFailed": "Speichern fehlgeschlagen — du bist offline. Bitte versuche es erneut, sobald du wieder online bist.",
+ "message": "Keine Verbindung: Änderungen können erst gespeichert werden, wenn du wieder online bist.",
+ "saveFailed": "Speichern fehlgeschlagen, du bist offline. Bitte versuche es erneut, sobald du wieder online bist.",
"showingCached": "Zeigt deine zuletzt synchronisierten Daten."
},
"demoBanner": {
- "message": "Demo-Modus — Änderungen werden nicht gespeichert."
+ "message": "Demo-Modus: Änderungen werden nicht gespeichert."
},
"nav": {
"dashboard": "Dashboard",
@@ -407,6 +409,7 @@
"mood": "Stimmung",
"coach": "Coach",
"skipToContent": "Zum Inhalt springen",
+ "breadcrumb": "Du bist hier",
"sidebar": "Sidebar",
"mainNavigation": "Hauptnavigation",
"mobileNavigation": "Mobile Navigation",
@@ -426,7 +429,9 @@
"workout": "Workout",
"workoutDescription": "Eine Einheit, die kein Gerät aufgezeichnet hat",
"symptom": "Symptom",
- "symptomDescription": "Eines deiner eigenen Symptome, mit Stärke"
+ "symptomDescription": "Eines deiner eigenen Symptome, mit Stärke",
+ "lifeEventDescription": "Umzug, Geburt, neue Stelle",
+ "lifeEvent": "Lebensereignis"
},
"cycle": "Zyklus",
"labs": "Laborwerte",
@@ -435,7 +440,8 @@
"illness": "Krankheit",
"documents": "Dokumente",
"vaccinations": "Impfungen",
- "mentalWellbeing": "Seelisches Wohlbefinden"
+ "mentalWellbeing": "Seelisches Wohlbefinden",
+ "timeline": "Zeitleiste"
},
"auth": {
"login": "Anmelden",
@@ -452,14 +458,13 @@
"hasAccount": "Bereits ein Konto?",
"loginFailed": "Anmeldung fehlgeschlagen",
"registerFailed": "Registrierung fehlgeschlagen",
- "invitedBanner": "Du wurdest eingeladen — erstelle dein Konto, auch wenn die Registrierung sonst geschlossen ist.",
+ "invitedBanner": "Du wurdest eingeladen. Erstelle dein Konto, auch wenn die Registrierung sonst geschlossen ist.",
"alreadySignedIn": {
"title": "Du bist bereits angemeldet",
"body": "Du bist als {username} angemeldet. Eine Einladung legt ein neues Konto an. Melde dich ab, um sie anzunehmen.",
"signOut": "Abmelden und Einladung annehmen"
},
"createAccount": "Konto erstellen",
- "newAccount": "Neues Konto erstellen",
"backToPasskey": "Zurück zu Passkey",
"passkeySetupLater": "Passkeys kannst du jederzeit in den Einstellungen einrichten.",
"passwordMinLength": "Min. 12 Zeichen",
@@ -512,6 +517,37 @@
"fixLan": "Server im Heimnetz ohne TLS: Setze SESSION_COOKIE_SECURE=false in seinen Einstellungen und starte ihn neu.",
"fixHttps": "Server hinter einem HTTPS-Proxy: Öffne stattdessen die https://-Adresse und sorge dafür, dass der Proxy X-Forwarded-Proto: https weitergibt.",
"localhostNote": "Diese Seite läuft auf http://localhost, der Server setzt sein Anmelde-Cookie aber nur für https://. Die meisten Browser akzeptieren das auf localhost; wenn du nach der Anmeldung wieder hier landest, setze SESSION_COOKIE_SECURE=false."
+ },
+ "claim": {
+ "title": "Übernimm deine Gesundheitsakte",
+ "titleNamed": "Willkommen, {name}",
+ "description": "Deine Gesundheitsakte wurde bisher für dich geführt. Wähle, wie du dich anmeldest, und sie wird dein eigenes Konto. Nichts wird kopiert oder verschoben.",
+ "loading": "Dein Link wird geprüft…",
+ "invalid": "Dieser Link funktioniert nicht mehr",
+ "invalidHint": "Er ist unbekannt, abgelaufen, zurückgezogen oder schon benutzt. Bitte die Person, die ihn dir gegeben hat, um einen neuen.",
+ "oidcOnly": "Dieser Server erlaubt nur Single Sign-on, deshalb lässt sich ein Profil hier noch nicht übernehmen.",
+ "signedInTitle": "Melde dich zuerst ab",
+ "signedInBody": "Du bist als {username} angemeldet. Dieser Link richtet eine Anmeldung für jemand anderen ein, melde dich also ab, bevor du ihn benutzt.",
+ "signOut": "Abmelden und weiter",
+ "guardiansTitle": "Wer vorerst Zugriff behält",
+ "guardiansHint": "Vorgeschlagen von den Personen, die deine Akte geführt haben. Du entscheidest direkt nach der Anmeldung.",
+ "guardianUnnamed": "Eine betreuende Person",
+ "usernameHint": "3 bis 30 Buchstaben, Ziffern, _ oder -.",
+ "usernameReserved": "Benutzernamen, die mit managed- beginnen, sind reserviert.",
+ "emailHint": "Pflicht, damit du wieder hineinkommst, wenn du dein Passwort vergisst.",
+ "consentNote": "Den Hinweis und jede KI-Nutzung bestätigst du selbst. Bis dahin geht nichts aus deiner Akte an einen KI-Anbieter.",
+ "expires": "Dieser Link gilt bis {date}.",
+ "submit": "Übernehmen",
+ "submitting": "Wird übernommen…",
+ "errorTaken": "Dieser Benutzername oder diese E-Mail-Adresse ist schon vergeben.",
+ "errorRateLimit": "Zu viele Versuche. Warte ein paar Minuten und versuche es erneut.",
+ "errorFailed": "Das hat nicht geklappt. Versuche es gleich noch einmal.",
+ "errorOffline": "Das hat den Server nicht erreicht. Prüfe deine Verbindung und versuche es erneut."
+ },
+ "inviteInvalid": {
+ "title": "Einladung ungültig",
+ "description": "Dieser Einladungslink ist unvollständig oder falsch kopiert. Bitte die Person, die dich eingeladen hat, um einen neuen Link.",
+ "toLogin": "Zur Anmeldung"
}
},
"dashboard": {
@@ -543,17 +579,17 @@
"sleepShort": "Schlaf",
"stepsShort": "Schritte",
"vo2MaxShort": "VO₂ max",
- "bpInTargetWindow90": "BD-Ziel · 90 T",
- "bpInTargetWindowDynamic": "BD-Ziel · {days} T",
+ "bpInTargetWindow90": "BD-Ziel (90 T)",
+ "bpInTargetWindowDynamic": "BD-Ziel ({days} T)",
"bpInTargetCollecting": "sammelt noch Daten",
"medications": "Medikamente",
"quickAddMeasurement": "Messung erfassen",
"quickAddMood": "Stimmung erfassen",
"quickAddMedicationIntake": "Einnahme erfassen",
"quickAddWorkout": "Workout erfassen",
+ "quickAddLifeEvent": "Lebensereignis erfassen",
"customizeDashboard": "Dashboard anpassen",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Medikamenteneinnahme erfassen",
"sheetDescription": "Dosis, Zeitpunkt und Medikament in wenigen Tipps protokollieren.",
"medicationLabel": "Medikament",
"medicationPlaceholder": "Medikament auswählen",
@@ -610,7 +646,7 @@
"layoutTileColumn": "Kachel",
"layoutChartColumn": "Chart",
"layoutNativeOnlyTitle": "In der mobilen App sichtbar",
- "layoutNativeOnlyHint": "Diese Widgets zeichnet die mobile App, nicht diese Seite. Sie liegen im selben gespeicherten Layout — hier wird ihre Sichtbarkeit gesetzt.",
+ "layoutNativeOnlyHint": "Diese Widgets zeichnet die mobile App, nicht diese Seite. Sie liegen im selben gespeicherten Layout, hier wird ihre Sichtbarkeit gesetzt.",
"dragHandle": "Zum Verschieben ziehen",
"dragHandleHint": "Pfeiltasten Hoch/Runter, oder Leertaste plus Pfeiltasten am fokussierten Griff.",
"emptyTitle": "Dein Dashboard ist leer",
@@ -666,7 +702,8 @@
"body": "Ohne eine Kopie des Schlüssels lassen sich deine Daten nach einer Neuinstallation nicht wiederherstellen. Es dauert eine Minute.",
"action": "Zum Schritt"
},
- "quickAddSymptom": "Symptom erfassen"
+ "quickAddSymptom": "Symptom erfassen",
+ "todayCardVisible": "Obere Karte anzeigen"
},
"measurements": {
"title": "Messungen",
@@ -734,11 +771,11 @@
"valueWithUnit": "Wert ({unit})",
"timestamp": "Zeitpunkt",
"timestampBackdateHint": "Datum und Uhrzeit anpassen, um einen vergangenen Wert zu erfassen.",
- "notesPlaceholder": "z.B. nach dem Essen",
+ "notesPlaceholder": "z. B. nach dem Essen",
"formReset": "Formular zurücksetzen",
"saveError": "Fehler beim Speichern",
"duplicateTimestamp": "Zum gewählten Zeitpunkt existiert bereits ein Messwert dieses Typs.",
- "serverOwnedSource": "Werte aus einer verbundenen Quelle lassen sich nicht bearbeiten — Zeitpunkt und Notiz bleiben änderbar.",
+ "serverOwnedSource": "Werte aus einer verbundenen Quelle lassen sich nicht bearbeiten. Zeitpunkt und Notiz bleiben änderbar.",
"deleteError": "Fehler beim Löschen",
"deletedToast": "Messwert gelöscht",
"restoredToast": "Messwert wiederhergestellt",
@@ -833,11 +870,12 @@
"durationHoursOnly": "{hours} Std.",
"durationMinutesOnly": "{minutes} Min.",
"durationInvalid": "Gib eine Dauer ein, zum Beispiel 7,5, 7:30 oder 7h 30m.",
- "durationTooLong": "Ein Eintrag kann nicht länger als 24 Stunden sein."
+ "durationTooLong": "Ein Eintrag kann nicht länger als 24 Stunden sein.",
+ "sourceHealthConnect": "Health Connect"
},
"metricCatalog": {
"title": "Was HealthLog erfassen kann",
- "description": "Alle Messwert-Bereiche, die HealthLog unterstützt — auch die, für die noch keine Daten vorliegen — und was sie liefert.",
+ "description": "Alle Messwert-Bereiche, die HealthLog unterstützt (auch die, für die noch keine Daten vorliegen), und was sie liefert.",
"statusPresent": "Erfasst",
"statusAbsent": "Noch nicht erfasst",
"statusModuleOff": "Aus",
@@ -845,11 +883,11 @@
"connectCta": "Quelle verbinden",
"viewCta": "Ansehen",
"ecg": {
- "source": "Einkanal-EKG-Streifen von einer kompatiblen Uhr — aktuell Withings ScanWatch — werden automatisch synchronisiert."
+ "source": "Einkanal-EKG-Streifen von einer kompatiblen Uhr (aktuell Withings ScanWatch) werden automatisch synchronisiert."
},
"watchdog": {
"title": "Automatische Überwachung",
- "description": "HealthLog beobachtet außerdem still im Hintergrund — Benachrichtigungen zu unregelmäßigem Rhythmus und Atemstörungen von einer verbundenen Uhr, Warnungen bei Abweichungen von deiner Baseline sowie auffällige Veränderungen zwischen deinen letzten beiden Laborwerten — und zeigt eine Karte nur, wenn etwas auffällt. Ist mindestens eine dieser Quellen verbunden, ist keine Karte ein gutes Zeichen, kein fehlendes Feature; ist noch keine verbunden, wird schlicht noch nichts überwacht."
+ "description": "HealthLog beobachtet außerdem still im Hintergrund: Benachrichtigungen zu unregelmäßigem Rhythmus und Atemstörungen von einer verbundenen Uhr, Warnungen bei Abweichungen von deiner Baseline sowie auffällige Veränderungen zwischen deinen letzten beiden Laborwerten. Eine Karte erscheint nur, wenn etwas auffällt. Ist mindestens eine dieser Quellen verbunden, ist keine Karte ein gutes Zeichen, kein fehlendes Feature; ist noch keine verbunden, wird schlicht noch nichts überwacht."
}
},
"mood": {
@@ -864,7 +902,7 @@
"factorNo": "Nein",
"factorRatingOption": "{label}: {value} von {max}",
"tags": "Tags",
- "tagsPlaceholder": "z.B. Sport, Arbeit, Familie",
+ "tagsPlaceholder": "z. B. Sport, Arbeit, Familie",
"tagsHelp": "Kommagetrennt eingeben",
"timestamp": "Zeitpunkt",
"date": "Datum",
@@ -1006,7 +1044,7 @@
"archive": "Archivieren",
"restore": "Wiederherstellen",
"archivedTitle": "Archivierte Tags",
- "archivedDescription": "Archivierte Tags bleiben in deinen bisherigen Einträgen erhalten — sie verschwinden nur aus der Auswahl.",
+ "archivedDescription": "Archivierte Tags bleiben in deinen bisherigen Einträgen erhalten. Sie verschwinden nur aus der Auswahl.",
"archivedEmpty": "Nichts archiviert.",
"purgeTitle": "Tag endgültig löschen?",
"purgeBody": "Der Tag wird aus {count} bisherigen Einträgen entfernt. Das lässt sich nicht rückgängig machen.",
@@ -1031,7 +1069,7 @@
},
"sectionMoreTags": "Weitere Tags",
"sectionNoteDetails": "Notiz & Details",
- "quickTagsHelp": "Häufig genutzte Tags — ein Tipp genügt",
+ "quickTagsHelp": "Häufig genutzte Tags: ein Tipp genügt",
"quickTagsExpand": "Weitere",
"loadError": "Stimmungseinträge konnten nicht geladen werden.",
"sectionLevelA": "Gefühlslage",
@@ -1191,7 +1229,8 @@
"bodyLogged": "Im Krankheitstagebuch erfasst: {count} Symptome",
"bodyNotLogged": "Für diesen Tag ist im Krankheitstagebuch nichts erfasst",
"openIllness": "Öffnen"
- }
+ },
+ "sourceNotification": "Benachrichtigung"
},
"medications": {
"title": "Medikamente",
@@ -1209,8 +1248,8 @@
"dialogDescription": "Diese Medikamente sind jetzt fällig. Bestätige, um sie alle als genommen zu erfassen.",
"confirm": "Alle einnehmen ({count})",
"successToast": "{count} Einnahmen erfasst",
- "partialToast": "{taken} erfasst, {failed} fehlgeschlagen — bitte erneut versuchen",
- "failedToast": "Keine Einnahme erfasst — bitte erneut versuchen"
+ "partialToast": "{taken} erfasst, {failed} fehlgeschlagen. Bitte erneut versuchen",
+ "failedToast": "Keine Einnahme erfasst. Bitte erneut versuchen"
},
"logIntake": {
"dialogTitle": "Einnahme erfassen",
@@ -1284,6 +1323,14 @@
"disabledToast": "Einnahmen werden nicht erfasst",
"toggleFailed": "Einnahme-Erfassung konnte nicht geändert werden."
},
+ "autoMarkTaken": {
+ "label": "Einnahmen automatisch erfassen",
+ "helper": "HealthLog erfasst jede geplante Dosis zu ihrer Uhrzeit als eingenommen. Du kannst eine Dosis weiterhin selbst ändern oder überspringen. Erinnerungen entfallen.",
+ "cardHint": "Wird automatisch erfasst",
+ "enabledToast": "Einnahmen werden jetzt automatisch erfasst",
+ "disabledToast": "Einnahmen werden nicht mehr automatisch erfasst",
+ "toggleFailed": "Automatische Erfassung konnte nicht geändert werden."
+ },
"withoutNotification": "Ohne Benachrichtigung",
"pausedSince": "Pausiert seit",
"takeNow": "Jetzt einnehmen",
@@ -1299,7 +1346,7 @@
"complianceInfoLabel": "Was bedeutet Therapietreue?",
"lastIntake": "Letzte Einnahme:",
"nextIntake": "Nächste Einnahme:",
- "nextIntakeOverdue": "Überfällig · {time} — noch nachholbar",
+ "nextIntakeOverdue": "Überfällig seit {time}, noch nachholbar",
"dayStreak": "Tage Serie",
"dayStreakOne": "Tag Serie",
"cycleDueToday": "Heute fällig",
@@ -1308,10 +1355,10 @@
"skipped": "Übersprungen",
"intakeToastTaken": "{name} als genommen erfasst",
"intakeToastSkipped": "{name} übersprungen",
- "intakeToastFailed": "{name} konnte nicht erfasst werden — bitte erneut versuchen",
+ "intakeToastFailed": "{name} konnte nicht erfasst werden. Bitte erneut versuchen",
"intakeUndo": "Rückgängig",
"intakeUndone": "Eintrag zurückgenommen",
- "intakeUndoFailed": "Rückgängig fehlgeschlagen — im Verlauf korrigieren",
+ "intakeUndoFailed": "Rückgängig fehlgeschlagen. Im Verlauf korrigieren",
"glp1LastInjection": "{label}",
"glp1NextInjectionDays": "{label} (in {days} Tagen)",
"glp1NextInjectionToday": "heute",
@@ -1379,7 +1426,7 @@
"scheduling": {
"naturalLanguage": {
"title": "Medikament beschreiben",
- "description": "Beschreibe das Medikament in eigenen Worten — das Formular wird automatisch vorausgefüllt.",
+ "description": "Beschreibe das Medikament in eigenen Worten, das Formular wird automatisch vorausgefüllt.",
"placeholder": "z. B. Mounjaro 5 mg wöchentlich am Mittwochmorgen ab nächstem Montag",
"submit": "Extrahieren",
"submitBusy": "Wird verarbeitet…",
@@ -1393,10 +1440,10 @@
"error": {
"empty": "Gib zuerst eine Beschreibung ein.",
"tooLong": "Beschreibung muss unter 2000 Zeichen bleiben.",
- "rateLimit": "Zu viele Anfragen — bitte kurz warten und erneut versuchen.",
- "noProvider": "Kein KI-Anbieter konfiguriert — bitte in den Einstellungen einrichten.",
- "budget": "Tägliches KI-Budget erreicht — bitte morgen erneut versuchen.",
- "network": "KI-Anbieter nicht erreichbar — bitte erneut versuchen.",
+ "rateLimit": "Zu viele Anfragen. Bitte kurz warten und erneut versuchen.",
+ "noProvider": "Kein KI-Anbieter konfiguriert. Bitte in den Einstellungen einrichten.",
+ "budget": "Tägliches KI-Budget erreicht. Bitte morgen erneut versuchen.",
+ "network": "KI-Anbieter nicht erreichbar. Bitte erneut versuchen.",
"consent": "Für das Lesen deiner Beschreibung mit KI brauchst du zuerst deine Einwilligung.",
"unavailable": "Das Lesen von Beschreibungen mit KI ist hier gerade nicht verfügbar."
}
@@ -1412,7 +1459,7 @@
"everyNMonths": "Alle N Monate an einem bestimmten Tag",
"yearly": "Einmal im Jahr",
"rolling": "Alle N Tage seit der letzten Einnahme (flexibel)",
- "rollingExplainer": "Zählt ab der letzten erfassten Einnahme — pausiert, wenn du eine Dosis auslässt.",
+ "rollingExplainer": "Zählt ab der letzten erfassten Einnahme und pausiert, wenn du eine Dosis auslässt.",
"oneShot": "Einmaldosis"
},
"weekdays": {
@@ -1470,7 +1517,7 @@
"night": "Nachts"
},
"max": {
- "reached": "Maximum erreicht — bitte zuerst eine Uhrzeit entfernen."
+ "reached": "Maximum erreicht. Bitte zuerst eine Uhrzeit entfernen."
}
},
"courseWindow": {
@@ -1520,7 +1567,7 @@
},
"step3": {
"title": "Wie hoch ist die Dosis?",
- "subline": "Eine Tablette, ein Hub, ein Tropfen — je nach Form.",
+ "subline": "Eine Tablette, ein Hub, ein Tropfen, je nach Form.",
"amountLabel": "Menge",
"amountPlaceholder": "z. B. 5",
"unitLabel": "Einheit",
@@ -1546,10 +1593,10 @@
},
"dosesPerUnitLabel": "Dosen pro Einheit",
"dosesPerUnitPlaceholder": "z. B. 4",
- "dosesPerUnitHint": "Dosen pro Pen / Ampulle — leer lassen, wenn kein Bestand getrackt wird.",
+ "dosesPerUnitHint": "Dosen pro Pen / Ampulle. Leer lassen, wenn kein Bestand getrackt wird.",
"short": "Dosis",
"unitsPerDoseLabel": "Einheiten pro Dosis",
- "unitsPerDoseHint": "z. B. 2 Tabletten à 2 mg für eine 4-mg-Dosis — oder ½ Tablette bei geteilter Dosis"
+ "unitsPerDoseHint": "z. B. 2 Tabletten à 2 mg für eine 4-mg-Dosis oder ½ Tablette bei geteilter Dosis"
},
"step4": {
"title": "Über welchen Zeitraum nimmst du es?",
@@ -1558,7 +1605,7 @@
},
"step5": {
"title": "Wie oft nimmst du es?",
- "subline": "Wähle die Regelmäßigkeit — die Details kommen im nächsten Schritt.",
+ "subline": "Wähle die Regelmäßigkeit, die Details kommen im nächsten Schritt.",
"short": "Takt"
},
"step6": {
@@ -1580,7 +1627,7 @@
},
"step7": {
"title": "Wann nimmst du es?",
- "subline": "Eine oder mehrere Uhrzeiten — die Erinnerungen richten sich danach.",
+ "subline": "Eine oder mehrere Uhrzeiten. Die Erinnerungen richten sich danach.",
"presetsLabel": "Tagespresets",
"presets": {
"morning": "Morgens",
@@ -1588,7 +1635,7 @@
"evening": "Abends",
"night": "Nachts"
},
- "overrideHint": "Optional — für diese Uhrzeit eine abweichende Dosis oder Stückzahl festlegen. Leer lassen, um den Standard des Medikaments zu verwenden.",
+ "overrideHint": "Optional: für diese Uhrzeit eine abweichende Dosis oder Stückzahl festlegen. Leer lassen, um den Standard des Medikaments zu verwenden.",
"doseOverrideLabel": "Dosis für diese Uhrzeit (optional)",
"doseOverridePlaceholder": "z. B. eine halbe Tablette",
"unitsOverrideLabel": "Verbrauchte Einheiten zu dieser Uhrzeit",
@@ -1596,11 +1643,11 @@
"short": "Zeiten"
},
"step8": {
- "title": "Fertig — soll HealthLog dich erinnern?",
+ "title": "Fertig! Soll HealthLog dich erinnern?",
"subline": "Du kannst alles unten nochmal prüfen, bevor du speicherst.",
"remindersLabel": "Erinnerungen aktiv",
- "remindersDescription": "Push, Telegram oder ntfy — je nach deinen Einstellungen.",
- "noChannelHint": "Erinnerungen brauchen einen Benachrichtigungskanal — aktuell ist keiner aktiv.",
+ "remindersDescription": "Push, Telegram oder ntfy, je nach deinen Einstellungen.",
+ "noChannelHint": "Erinnerungen brauchen einen Benachrichtigungskanal, aktuell ist keiner aktiv.",
"noChannelHintLink": "Benachrichtigungen einrichten",
"short": "Fertig"
}
@@ -1641,7 +1688,7 @@
},
"rolling": {
"label": "Flexibel ab letzter Einnahme",
- "description": "Du lässt den nächsten Termin offen, drückst 'genommen' wenn du sie genommen hast — ab dem Zeitpunkt zählt der Counter wieder."
+ "description": "Du lässt den nächsten Termin offen, drückst 'genommen', wenn du sie genommen hast. Ab dem Zeitpunkt zählt der Counter wieder."
},
"oneShot": {
"label": "Einmalig",
@@ -1649,7 +1696,7 @@
},
"asNeeded": {
"label": "Bei Bedarf",
- "description": "Kein fester Plan — du erfasst eine Einnahme, wenn du sie brauchst (z. B. Schmerzmittel)."
+ "description": "Kein fester Plan: Du erfasst eine Einnahme, wenn du sie brauchst (z. B. Schmerzmittel)."
}
},
"summary": {
@@ -1684,7 +1731,7 @@
}
},
"errors": {
- "submitFailed": "Medikament konnte nicht gespeichert werden — bitte erneut versuchen."
+ "submitFailed": "Medikament konnte nicht gespeichert werden. Bitte erneut versuchen."
}
},
"doseStrength": {
@@ -1700,11 +1747,11 @@
"empty": "Noch keine Dosis-Steigerungsschritte erfasst. Datierte Dosisänderungen hinzufügen, um den Plan abzubilden.",
"youAreHere": "Du bist hier",
"doseStep": "{dose} {unit}",
- "plannedFor": "Geplant · {date}"
+ "plannedFor": "Geplant für {date}"
},
"injectionSiteBodyOutlineAriaLabel": "Körperumriss",
"injectionSitePickerAriaLabel": "Injektionsstelle auswählen",
- "injectionSiteRecommendedAriaLabel": "{site} — empfohlene nächste Stelle",
+ "injectionSiteRecommendedAriaLabel": "{site}: empfohlene nächste Stelle",
"injectionSiteLegendAriaLabel": "Legende der Markierungen",
"injectionSiteLegendRecommended": "Empfohlene nächste Stelle",
"injectionSiteLegendLastUsed": "Zuletzt verwendete Stelle",
@@ -1740,6 +1787,7 @@
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "über {origin}",
+ "intakeRecordedAutomatically": "Automatisch erfasst",
"intakeHistoryEmptyTitle": "Noch keine Einnahmen erfasst",
"intakeHistoryEmptyDescription": "Trage eine Einnahme auf der Tagesübersicht ein, um den Verlauf aufzubauen.",
"intakeHistoryEmptyAction": "Zur Tagesübersicht",
@@ -1751,7 +1799,7 @@
"intakeHistoryStatusSkipped": "Übersprungen",
"intakeHistorySortByTaken": "Nach Einnahme-Zeitpunkt sortieren",
"intakeHistorySortByScheduled": "Nach geplantem Zeitpunkt sortieren",
- "intakeHistoryPageInfo": "Seite {page} von {total} · {count} Einträge",
+ "intakeHistoryPageInfo": "Seite {page} von {total}, {count} Einträge",
"intakeHistoryPrev": "Zurück",
"intakeHistoryNext": "Weiter",
"glp1": {
@@ -1792,7 +1840,7 @@
"cadence": {
"oneShotOn": "Einmalige Dosis am {date}.",
"oneShotPending": "Einmalige Dosis ausstehend.",
- "asNeeded": "Bei Bedarf — kein fester Plan."
+ "asNeeded": "Bei Bedarf, kein fester Plan."
},
"intake": {
"title": "Einnahmeverlauf",
@@ -1819,7 +1867,7 @@
"dialogTitle": "Einnahme bearbeiten",
"takenAtLabel": "Eingenommen um",
"scheduledForHint": "Geplant: {dateTime}",
- "farFromScheduledWarning": "Liegt weit vom geplanten Zeitpunkt entfernt — Datum prüfen.",
+ "farFromScheduledWarning": "Liegt weit vom geplanten Zeitpunkt entfernt. Datum prüfen.",
"skippedLabel": "Übersprungen",
"save": "Speichern",
"cancel": "Abbrechen",
@@ -1851,7 +1899,7 @@
"markAsTaken": "Als genommen markieren",
"markAsSkipped": "Als übersprungen markieren",
"adHocTag": "außerplanmäßig",
- "takenDetail": "geplant {planned} · genommen {taken}",
+ "takenDetail": "geplant {planned}, genommen {taken}",
"doseTaken": "Dosis: {dose}",
"status": {
"taken_on_time": "pünktlich",
@@ -1863,10 +1911,10 @@
},
"addDialog": {
"title": "Einnahme hinzufügen",
- "description": "Dosis nachtragen — auch rückwirkend oder außerplanmäßig.",
+ "description": "Dosis nachtragen, auch rückwirkend oder außerplanmäßig.",
"takenAtLabel": "Genommen um",
"skippedLabel": "Übersprungen",
- "nudgeHint": "Liegt nah am Slot {slot} — beim Speichern fragen wir, ob zugeordnet werden soll.",
+ "nudgeHint": "Liegt nah am Slot {slot}. Beim Speichern fragen wir, ob zugeordnet werden soll.",
"cancel": "Abbrechen",
"submit": "Speichern"
},
@@ -1915,7 +1963,7 @@
},
"codes": {
"label": "Klinische Codes (ATC / RxNorm)",
- "note": "Optional. Werden beim FHIR-Export der Gesundheitsakte verwendet. Codes werden genau wie eingegeben übernommen — nie geraten.",
+ "note": "Optional. Werden beim FHIR-Export der Gesundheitsakte verwendet. Codes werden genau wie eingegeben übernommen, nie geraten.",
"atcLabel": "ATC",
"rxNormLabel": "RxNorm",
"saved": "Klinische Codes gespeichert",
@@ -1935,7 +1983,7 @@
"title": "Medikament beenden",
"helper": "Erinnerungen stoppen und das Medikament als beendet markieren. Verlauf bleibt sichtbar.",
"button": "Beenden",
- "dialogTitle": "{name} beenden — Erinnerungen stoppen, Verlauf bleibt sichtbar",
+ "dialogTitle": "{name} beenden: Erinnerungen stoppen, Verlauf bleibt sichtbar",
"dialogBody": "Keine weiteren Erinnerungen. Alte Einträge bleiben in der Übersicht.",
"toast": "Medikament beendet",
"failed": "Medikament konnte nicht beendet werden."
@@ -1973,7 +2021,7 @@
"copyFailed": "Kopieren fehlgeschlagen.",
"tokenCopied": "Token kopiert",
"copyToken": "Token kopieren",
- "mintedHint": "Token jetzt kopieren — es wird nur einmal angezeigt."
+ "mintedHint": "Token jetzt kopieren, es wird nur einmal angezeigt."
},
"advanced": {
"dataPortability": {
@@ -2002,7 +2050,6 @@
"bestand": "Bestand",
"verlauf": "Verlauf",
"injektion": "Injektion",
- "api": "API",
"erweitert": "Erweitert",
"wirkung": "Wirkung"
},
@@ -2014,9 +2061,9 @@
"statusTitle": "Status",
"nextDoseLabel": "Nächste Dosis",
"nextDoseNone": "Keine anstehende Dosis.",
- "nextDoseAsNeeded": "Bei Bedarf — keine geplante Dosis.",
+ "nextDoseAsNeeded": "Bei Bedarf, keine geplante Dosis.",
"nextDoseNotTracked": "Nicht erfasst, nur dokumentiert.",
- "pausedHint": "Pausiert — keine Erinnerungen.",
+ "pausedHint": "Pausiert, keine Erinnerungen.",
"reminderLabel": "Erinnerung",
"reminderOn": "Aktiv",
"reminderOff": "Aus",
@@ -2087,7 +2134,7 @@
"packagingDefaultPack": "Standard-Packungsgröße: {units} Einheiten",
"packagingNoDefaultPack": "Keine Standard-Packungsgröße hinterlegt",
"packagingEdit": "Bearbeiten",
- "packagingDescription": "Lege fest, wie viele Einheiten eine Dosis verbraucht und wie groß eine neue Packung üblicherweise ist — z. B. nach einem Herstellerwechsel mit anderer Blistergröße.",
+ "packagingDescription": "Lege fest, wie viele Einheiten eine Dosis verbraucht und wie groß eine neue Packung üblicherweise ist, z. B. nach einem Herstellerwechsel mit anderer Blistergröße.",
"packagingUnitsPerDoseLabel": "Einheiten pro Dosis",
"packagingUnitsPerDoseHelper": "Z. B. 2 Tabletten à 2 mg für eine 4-mg-Dosis oder 1,5 für anderthalb (über 0, bis 100). Bereits erfasste Einnahmen bleiben unverändert.",
"packagingDefaultPackLabel": "Standard-Packungsgröße (Einheiten)",
@@ -2138,11 +2185,11 @@
"hideEras": "Frühere Ären ausblenden",
"addEra": "Frühere Ära ergänzen",
"addEraTitle": "Frühere Ära ergänzen",
- "addEraDescription": "Halte fest, zu welchen Uhrzeiten dieses Medikament früher eingenommen wurde — etwa bevor du es hier angepasst hast.",
+ "addEraDescription": "Halte fest, zu welchen Uhrzeiten dieses Medikament früher eingenommen wurde, etwa bevor du es hier angepasst hast.",
"editAria": "Ära bearbeiten",
"editEraTitle": "Ära bearbeiten",
"editEraDescription": "Passe die Einnahmezeiten und den Zeitraum dieser Ära an.",
- "archivedEditHint": "Diese Ära wurde automatisch aufgezeichnet — deine Änderung korrigiert die aufgezeichnete Historie; das Original bleibt intern als Beleg erhalten.",
+ "archivedEditHint": "Diese Ära wurde automatisch aufgezeichnet. Deine Änderung korrigiert die aufgezeichnete Historie; das Original bleibt intern als Beleg erhalten.",
"updatedToast": "Ära aktualisiert",
"updateError": "Die Ära konnte nicht aktualisiert werden. Sie darf sich nicht mit anderen Ären überschneiden und muss vor dem aktuellen Plan enden.",
"timesLabel": "Einnahmezeiten",
@@ -2168,7 +2215,7 @@
"saved": "Zeitplan gespeichert",
"failed": "Zeitplan konnte nicht gespeichert werden",
"window": {
- "intro": "Lege je Dosis fest, wann eine Einnahme als pünktlich gilt — ein genauer Zeitpunkt (±1 Std.) oder ein eigenes Fenster wie 07:00–09:00.",
+ "intro": "Lege je Dosis fest, wann eine Einnahme als pünktlich gilt: ein genauer Zeitpunkt (±1 Std.) oder ein eigenes Fenster wie 07:00–09:00.",
"explicitToggle": "Eigenes Fenster",
"startLabel": "Fensterbeginn",
"endLabel": "Fensterende",
@@ -2181,10 +2228,10 @@
}
},
"logInjectionSiteTitle": "Injektionsstelle erfassen",
- "logInjectionSiteDescription": "Wo hast du {name} gespritzt? Optional — du kannst dies überspringen.",
+ "logInjectionSiteDescription": "Wo hast du {name} gespritzt? Optional, du kannst dies überspringen.",
"logInjectionSiteSkip": "Überspringen",
"logInjectionSiteConfirm": "Stelle speichern",
- "logInjectionSiteNoneAvailable": "Keine Injektionsstelle verfügbar — alle Stellen sind in deinen Einstellungen ausgeschlossen.",
+ "logInjectionSiteNoneAvailable": "Keine Injektionsstelle verfügbar. Alle Stellen sind in deinen Einstellungen ausgeschlossen.",
"logInjectionSiteSaveFailed": "Injektionsstelle konnte nicht gespeichert werden. Bitte versuche es erneut.",
"perSlotUnits": "{units} pro Dosis",
"trackInjectionSitesToggle": "Injektionsstellen erfassen",
@@ -2209,16 +2256,15 @@
"tableStockDoses": "{count} Dosen",
"tableStockRunway": "≈ {days} Tage übrig",
"cardLowStockRunway": "Vorrat: ≈ {days} Tage",
- "takeActionFor": "Einnehmen — {name}",
- "skipActionFor": "Überspringen — {name}",
- "reorderTitle": "Reihenfolge anpassen",
- "reorderDescription": "Bestimme die Reihenfolge deiner Medikamente — sie gilt für die Karten- und die Tabellenansicht.",
+ "takeActionFor": "Einnehmen: {name}",
+ "skipActionFor": "Überspringen: {name}",
+ "reorderDescription": "Bestimme die Reihenfolge deiner Medikamente für die Karten- und die Tabellenansicht.",
"reorderSaved": "Reihenfolge gespeichert",
"reorderSaveFailed": "Reihenfolge konnte nicht gespeichert werden",
"customize": "Medikamente anpassen",
"efficacy": {
"title": "Wirkung",
- "intro": "Wie sich die Messwerte, die dieses Medikament beeinflussen soll, rund um den Beginn der Einnahme entwickelt haben. Es zeigt den zeitlichen Zusammenhang — eine Assoziation, keine Ursache — und keine Empfehlung zu Behandlung oder Dosis.",
+ "intro": "Wie sich die Messwerte, die dieses Medikament beeinflussen soll, rund um den Beginn der Einnahme entwickelt haben. Es zeigt den zeitlichen Zusammenhang (eine Assoziation, keine Ursache) und keine Empfehlung zu Behandlung oder Dosis.",
"notEligible": "Für dieses Medikament gibt es keinen zugehörigen Messwert.",
"startFallback": "Es ist kein Startdatum hinterlegt, daher dient der erste erfasste Wert als Bezugspunkt.",
"noStart": "Hinterlege ein Startdatum, um die Werte vor und nach Beginn zu vergleichen.",
@@ -2231,7 +2277,7 @@
"levelShift": "Um {date} wurde eine Verschiebung des Niveaus erkannt.",
"plateau": {
"title": "Gewichts-Plateau auf aktueller Dosis",
- "evidence": "Gewichtsveränderung von {delta} {unit} in den letzten {window} Tagen ({readings} Messungen) — seit {days} Tagen auf {dose}."
+ "evidence": "Gewichtsveränderung von {delta} {unit} in den letzten {window} Tagen ({readings} Messungen), seit {days} Tagen auf {dose}."
},
"retarget": {
"label": "Nicht der richtige Messwert?",
@@ -2241,16 +2287,16 @@
"error": "Diese Änderung konnte nicht gespeichert werden. Bitte erneut versuchen."
},
"beforeAfter": {
- "summary": "Ø {before} davor → Ø {after} seitdem — eine Veränderung von {delta}.",
+ "summary": "Ø {before} davor → Ø {after} seitdem, eine Veränderung von {delta}.",
"counts": "Basierend auf {beforeCount} Werten davor und {afterCount} seitdem.",
- "needBefore": "Noch zu wenige Werte vor dem Beginn — für einen Vergleich werden etwa {weeks} Wochen davor und danach benötigt.",
- "needAfter": "Noch zu wenige Werte seit dem Beginn — für einen Vergleich werden etwa {weeks} Wochen davor und danach benötigt.",
+ "needBefore": "Noch zu wenige Werte vor dem Beginn. Für einen Vergleich werden etwa {weeks} Wochen davor und danach benötigt.",
+ "needAfter": "Noch zu wenige Werte seit dem Beginn. Für einen Vergleich werden etwa {weeks} Wochen davor und danach benötigt.",
"needStart": "Hinterlege ein Startdatum, um die Werte vor und nach Beginn zu vergleichen.",
- "needData": "Noch zu wenige Werte — für einen Vergleich werden etwa {weeks} Wochen vor und nach dem Beginn benötigt."
+ "needData": "Noch zu wenige Werte. Für einen Vergleich werden etwa {weeks} Wochen vor und nach dem Beginn benötigt."
},
"insights": {
"title": "Medikamenten-Wirkung",
- "subtitle": "Was mit den Zielwerten deiner Medikamente zeitlich zusammenfällt — nur ein Zusammenhang, kein Urteil.",
+ "subtitle": "Was mit den Zielwerten deiner Medikamente zeitlich zusammenfällt: nur ein Zusammenhang, kein Urteil.",
"link": "Wirkung öffnen",
"empty": "Noch nicht genug Daten, um deine Medikamente mit ihren Zielwerten in Beziehung zu setzen."
},
@@ -2284,9 +2330,9 @@
"reasonMissingMedication": "die Zeile nennt kein Medikament",
"reasonUnreadableDosage": "die Dosis ist keine Zahl",
"reasonUnreadableRow": "die Zeile hat weniger Spalten als die Kopfzeile",
- "reasonStatusNoDoseInformation": "die Datei hält zu dieser Dosis nichts fest — niemand hat reagiert und nichts wurde eingetragen",
+ "reasonStatusNoDoseInformation": "die Datei hält zu dieser Dosis nichts fest; niemand hat reagiert und nichts wurde eingetragen",
"reasonStatusReminderEvent": "die Zeile betrifft eine verschobene Erinnerung und sagt nichts über die Dosis",
- "reasonStatusNotificationNotSent": "die Zeile hält fest, dass keine Benachrichtigung gesendet wurde — das betrifft die App, nicht die Dosis"
+ "reasonStatusNotificationNotSent": "die Zeile hält fest, dass keine Benachrichtigung gesendet wurde; das betrifft die App, nicht die Dosis"
},
"unitsPerDoseField": {
"other": "Andere…",
@@ -2356,7 +2402,7 @@
},
"charts": {
"loadingLabel": "Diagramm wird geladen…",
- "loadingSlowHint": "Auswertungen werden berechnet — das kann einen Moment dauern.",
+ "loadingSlowHint": "Auswertungen werden berechnet. Das kann einen Moment dauern.",
"weight": "Gewicht",
"bloodPressure": "Blutdruck",
"pulse": "Puls",
@@ -2378,6 +2424,7 @@
"days7Label": "7T",
"days7Title": "Letzte 7 Tage",
"days30Label": "30T",
+ "daysNLabel": "{days}T",
"days30Title": "Letzte 30 Tage",
"days90Label": "90T",
"days90Title": "Letzte 90 Tage",
@@ -2437,13 +2484,15 @@
"loadErrorTitle": "Diagramm konnte nicht geladen werden",
"loadErrorReload": "Neu laden",
"sparseDataCaption": "Mehr Messtage füllen den Trend.",
+ "tooltipHourlyMean": "{metric} (Durchschnitt der Stunden)",
"dataTable": {
"toggle": "Datenpunkte ({count})",
"columnDate": "Datum",
"noValue": "Kein Wert",
- "captionDay": "Eine Zeile pro Tag für {metric} — genau die Punkte, die das Diagramm zeichnet.",
- "captionWeek": "Eine Zeile pro Woche für {metric}, der Mittelwert der Tage dieser Woche — genau die Punkte, die das Diagramm zeichnet.",
- "captionMonth": "Eine Zeile pro Monat für {metric}, der Mittelwert der Tage dieses Monats — genau die Punkte, die das Diagramm zeichnet."
+ "captionDay": "Eine Zeile pro Tag für {metric}, genau die Punkte, die das Diagramm zeichnet.",
+ "captionWeek": "Eine Zeile pro Woche für {metric}, der Mittelwert der Tage dieser Woche, genau die Punkte, die das Diagramm zeichnet.",
+ "captionMonth": "Eine Zeile pro Monat für {metric}, der Mittelwert der Tage dieses Monats, genau die Punkte, die das Diagramm zeichnet.",
+ "hourlyMeanNote": "{metric} ist der Tagesdurchschnitt, bei dem jede Stunde einmal zählt, damit ein Training nicht den Rest des Tages überwiegt."
},
"personalBaseline": "Dein Mittel",
"deltaVsBaseline": "{delta} vs. dein Mittel",
@@ -2471,7 +2520,8 @@
"moodHeatmapEmpty": "Stimmungs-Heatmap, noch keine Tage erfasst.",
"complianceHeatmap": "Heatmap zur Medikamenteneinnahme von {start} bis {end}, {days} aktive Tage.",
"complianceHeatmapEmpty": "Heatmap zur Medikamenteneinnahme, noch keine aktiven Tage."
- }
+ },
+ "medicationsEmptyAction": "Medikament anlegen"
},
"chart": {
"overlay": {
@@ -2502,8 +2552,8 @@
},
"tooltipPrior": "Vorperiode",
"unavailable": {
- "lastMonth": "Vergleich nicht verfügbar — noch keine Daten aus dem Vormonat",
- "lastYear": "Vergleich nicht verfügbar — noch keine Daten aus dem Vorjahr"
+ "lastMonth": "Vergleich nicht verfügbar: noch keine Daten aus dem Vormonat",
+ "lastYear": "Vergleich nicht verfügbar: noch keine Daten aus dem Vorjahr"
},
"notAvailableForCompliance": "Vergleich für Therapietreue nicht verfügbar"
},
@@ -2515,7 +2565,9 @@
"dismissError": "Dieses Muster konnte nicht ausgeblendet werden.",
"restoreError": "Dieses Muster konnte nicht wiederhergestellt werden.",
"loadError": "Muster konnten nicht geladen werden.",
- "empty": "Kein aktuelles Muster hat die Evidenzschwelle erreicht."
+ "empty": "Kein aktuelles Muster hat die Evidenzschwelle erreicht.",
+ "seasonalNote": "Seit Version 1.42 rechnet HealthLog Jahreszeit und Langzeittrend heraus, bevor es Werte vergleicht. Du siehst dadurch weniger Zusammenhänge, aber die verbliebenen halten besser. Viele der früheren Muster waren der Kalender.",
+ "seasonalNoteDismiss": "Hinweis schließen"
},
"correlation": {
"direction": {
@@ -2547,29 +2599,37 @@
"ENV_DAYLIGHT": "das Tageslicht",
"ENV_PRECIP": "der Niederschlag",
"ENV_PRESSURE_MEAN": "der Luftdruck",
- "ENV_PRESSURE_DELTA": "die Luftdruckschwankung am Tag"
+ "ENV_PRESSURE_DELTA": "die Luftdruckschwankung am Tag",
+ "ENV_PM25": "der Feinstaub (PM2,5)",
+ "ENV_OZONE_8H": "das Ozon",
+ "ENV_POLLEN_MAX": "der Pollenflug"
},
"daily": {
- "high": "Wenn {behaviour} höher liegt, fällt {outcome} am Folgetag in deinen Daten tendenziell {direction} aus — ein Muster, das Beachtung verdient, keine Ursache.",
- "moderate": "Wenn {behaviour} höher liegt, scheint {outcome} am Folgetag nach bisheriger Datenlage eher {direction} auszufallen — ein Muster, das Beachtung verdient, keine Ursache.",
- "faint": "Wenn {behaviour} höher liegt, gibt es in deinen Daten allenfalls einen schwachen Hinweis darauf, dass {outcome} am Folgetag {direction} ausfällt — zu gering, um sich darauf zu verlassen, niemals eine Ursache."
+ "high": "Wenn {behaviour} höher liegt, fällt {outcome} am Folgetag in deinen Daten tendenziell {direction} aus. Das ist ein Muster, das Beachtung verdient, keine Ursache.",
+ "moderate": "Wenn {behaviour} höher liegt, scheint {outcome} am Folgetag nach bisheriger Datenlage eher {direction} auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache.",
+ "faint": "Wenn {behaviour} höher liegt, gibt es in deinen Daten allenfalls einen schwachen Hinweis darauf, dass {outcome} am Folgetag {direction} ausfällt. Das ist zu gering, um sich darauf zu verlassen, und niemals eine Ursache."
},
"lab": {
- "default": "Höhere {lab}-Werte gehen in deinen Daten damit einher, dass {outcome} im selben Zeitraum {direction} ausfällt — ein Zusammenhang, den du ärztlich im Blick behalten solltest, niemals eine Ursache.",
- "faint": "Höhere {lab}-Werte deuten in deinen Daten allenfalls schwach darauf hin, dass {outcome} im selben Zeitraum {direction} ausfällt — zu gering, um sich darauf zu verlassen, ein Zusammenhang, den du ärztlich abklären lassen solltest, niemals eine Ursache."
+ "default": "Höhere {lab}-Werte gehen in deinen Daten damit einher, dass {outcome} im selben Zeitraum {direction} ausfällt, ein Zusammenhang, den du ärztlich im Blick behalten solltest, niemals eine Ursache.",
+ "faint": "Höhere {lab}-Werte deuten in deinen Daten allenfalls schwach darauf hin, dass {outcome} im selben Zeitraum {direction} ausfällt. Das ist zu gering, um sich darauf zu verlassen, und niemals eine Ursache, aber ärztlich abklärenswert."
},
"hypothesis": {
"bpCompliance": {
- "support": "Höhere Medikamententreue geht mit niedrigeren systolischen Werten einher — ein Muster, das Beachtung verdient.",
- "surprising": "Medikamententreue und systolischer Wert bewegen sich in deinen Daten gemeinsam — überraschend; sprich mit deiner Ärztin oder deinem Arzt, bevor du etwas änderst.",
+ "support": "Höhere Medikamententreue geht mit niedrigeren systolischen Werten einher, ein Muster, das Beachtung verdient.",
+ "surprising": "Medikamententreue und systolischer Wert bewegen sich in deinen Daten gemeinsam, das ist überraschend. Sprich mit deiner Ärztin oder deinem Arzt, bevor du etwas änderst.",
"none": "Medikamententreue und systolischer Wert bewegen sich in diesem Zeitraum nicht deutlich gemeinsam."
},
"moodPulse": {
- "support": "Tage mit schlechterer Stimmung gehen mit höherem Ruhepuls einher — ein Muster, das Beachtung verdient.",
- "surprising": "Tage mit besserer Stimmung gehen in deinen Daten mit höherem Ruhepuls einher — überraschend.",
+ "support": "Tage mit schlechterer Stimmung gehen mit höherem Ruhepuls einher, ein Muster, das Beachtung verdient.",
+ "surprising": "Tage mit besserer Stimmung gehen in deinen Daten mit höherem Ruhepuls einher, das ist überraschend.",
"none": "Stimmung und Ruhepuls bewegen sich in diesem Zeitraum nicht deutlich gemeinsam."
},
- "weightWeekday": "Das Gewicht am {weekday} liegt {delta} kg {direction} deinem Durchschnitt der übrigen Tage — ein Muster, das Beachtung verdient."
+ "weightWeekday": "Das Gewicht am {weekday} liegt {delta} kg {direction} deinem Durchschnitt der übrigen Tage, ein Muster, das Beachtung verdient."
+ },
+ "dailySameDay": {
+ "high": "Mehr {behaviour} am selben Tag und am Vortag geht in deinen Daten tendenziell mit {direction} {outcome} einher. Das ist ein Muster, das sich zu beobachten lohnt, keine Ursache.",
+ "moderate": "Mehr {behaviour} am selben Tag und am Vortag scheint in deinen Daten nach bisheriger Lage mit {direction} {outcome} einherzugehen. Das ist ein Muster, das sich zu beobachten lohnt, keine Ursache.",
+ "faint": "Mehr {behaviour} am selben Tag und am Vortag zeigt in deinen Daten allenfalls einen schwachen Hinweis auf {direction} {outcome}. Das ist zu klein, um sich darauf zu stützen, und nie eine Ursache."
}
},
"emptyTitle": "Noch nicht genug Daten",
@@ -2616,7 +2676,7 @@
"narrativeWeek": "Diese Woche",
"narrativeMonth": "Dieser Monat",
"narrativeUpdating": "Wird aktualisiert…",
- "heroFallbackSubtitle": "Täglicher Blick auf deine Trends — direkt aus den Zahlen, die du protokollierst.",
+ "heroFallbackSubtitle": "Täglicher Blick auf deine Trends, direkt aus den Zahlen, die du protokollierst.",
"heroGreetingMorning": "Guten Morgen",
"heroGreetingAfternoon": "Guten Tag",
"heroGreetingEvening": "Guten Abend",
@@ -2681,7 +2741,7 @@
"referenceSleep": "Mindestens {low} Minuten pro Nacht; längerer Schlaf wird nicht abgewertet",
"referenceAdiposity": "Taille-zu-Körpergröße-Verhältnis unter {high}",
"referenceHba1c": "HbA1c {low} bis {high} %",
- "referenceDetail": "{label} · {source} · {kind} · {low} bis {high}",
+ "referenceDetail": "{label}: {low} bis {high} ({kind}, {source})",
"openBound": "offen",
"laboratoryReferenceSource": "Referenzbereiche des auswertenden Labors",
"personalTargetSource": "Persönliches Ziel",
@@ -2716,7 +2776,8 @@
"coverage": {
"recommendedMet": "Deckt die drei Felder ab, die der Score empfiehlt",
"areas": "{present} von {required} Feldern der Gesundheit"
- }
+ },
+ "historyLink": "Verlauf"
},
"dailyBriefing": {
"title": "Tagesbriefing",
@@ -2726,7 +2787,7 @@
"emptyDescription": "Erstelle das heutige Briefing, um eine Zusammenfassung deiner Trends zu bekommen.",
"emptyAction": "Briefing erstellen",
"loadingLabel": "Briefing wird zusammengestellt",
- "refreshFailedHint": "Briefing konnte nicht aktualisiert werden — es wird das letzte angezeigt.",
+ "refreshFailedHint": "Briefing konnte nicht aktualisiert werden. Es wird das letzte angezeigt.",
"retryAction": "Erneut versuchen",
"failedTitle": "Briefing konnte nicht erstellt werden",
"failedDescription": "Der KI-Anbieter hat das Zeitlimit überschritten oder einen Fehler zurückgegeben. Versuche es erneut oder erhöhe in den KI-Einstellungen das Antwort-Zeitlimit für ein langsames lokales Modell.",
@@ -2775,7 +2836,7 @@
"experimentCta": "7-Tage-Experiment starten",
"experimentTooltip": "Bald verfügbar",
"emptyTitle": "Mehr Daten nötig, um diesen Zusammenhang zu zeigen",
- "sourceChip": "basierend auf {n} gepaarten Messungen · {window}",
+ "sourceChip": "basierend auf {n} gepaarten Messungen ({window})",
"sourceWindowLast30": "letzte 30 Tage",
"confidenceHigh": "Hohe Konfidenz",
"confidenceModerate": "Mittlere Konfidenz",
@@ -2790,13 +2851,14 @@
}
},
"coach": {
- "refusalOutOfScope": "Ich kann nur bei den Gesundheitsdaten in deinem Log helfen — Messwerte, Medikamente, Stimmung, Trends. Fragen außerhalb dieses Themas beantworte ich nicht.",
- "refusalInjection": "In deiner Nachricht stehen Anweisungen, die meine Vorgaben überschreiben sollen. Ich bin der HealthLog-Coach und fasse ausschließlich deine eigenen Gesundheitsdaten zusammen — formuliere deine Frage bitte in diesem Rahmen neu.",
- "errorBudget": "Du hast das heutige Coach-Limit erreicht. Das Budget setzt sich um Mitternacht (UTC) zurück.",
+ "refusalOutOfScope": "Ich kann nur bei den Gesundheitsdaten in deinem Log helfen: Messwerte, Medikamente, Stimmung, Trends. Fragen außerhalb dieses Themas beantworte ich nicht.",
+ "refusalInjection": "In deiner Nachricht stehen Anweisungen, die meine Vorgaben überschreiben sollen. Ich bin der HealthLog-Coach und fasse ausschließlich deine eigenen Gesundheitsdaten zusammen. Formuliere deine Frage bitte in diesem Rahmen neu.",
+ "errorBudget": "Du hast das heutige Coach-Limit erreicht. Das Budget setzt sich um Mitternacht in deiner Zeitzone zurück.",
"errorProvider": "Der Coach konnte gerade keinen Auswertungs-Anbieter erreichen. Bitte versuche es in einem Moment erneut.",
- "errorNoProvider": "Der Coach liest deine erfassten Gesundheitsdaten und beantwortet Fragen dazu (keine medizinische Beratung). Dafür braucht HealthLog einen KI-Anbieter — verbinde deinen eigenen Schlüssel oder ein lokales Modell.",
+ "errorDemo": "In der Demo antwortet der Coach nicht.",
+ "errorNoProvider": "Der Coach liest deine erfassten Gesundheitsdaten und beantwortet Fragen dazu (keine medizinische Beratung). Dafür braucht HealthLog einen KI-Anbieter. Verbinde deinen eigenen Schlüssel oder ein lokales Modell.",
"errorNoProviderAction": "KI-Anbieter verbinden",
- "errorNetwork": "Keine Internetverbindung — versuche es erneut, sobald du online bist.",
+ "errorNetwork": "Keine Internetverbindung. Versuche es erneut, sobald du online bist.",
"prefsSaveError": "Coach-Einstellungen konnten nicht gespeichert werden. Bitte versuche es erneut.",
"errorCredentialExpired": "Deine Verbindung zum KI-Anbieter ist abgelaufen. Stelle sie in den Einstellungen wieder her, damit der Coach weiterläuft.",
"tagline": "Persönlicher Gesundheitscoach, auf deine Daten gestützt.",
@@ -2815,7 +2877,7 @@
"thinking": "Denke nach…",
"unverifiedFiguresNotice": "Einige Zahlen ließen sich nicht mit deinen Daten abgleichen und wurden weggelassen.",
"composerPlaceholder": "Frag mich etwas…",
- "nudgeBubbleLabel": "Neue Nachricht vom Coach — Chat öffnen",
+ "nudgeBubbleLabel": "Neue Nachricht vom Coach: Chat öffnen",
"fabLabel": "Coach öffnen",
"readAloud": "Vorlesen",
"readAloudStop": "Vorlesen beenden",
@@ -2833,14 +2895,13 @@
},
"rememberMessage": {
"action": "Merken",
- "done": "Gemerkt — in deiner Selbstauskunft gespeichert",
- "duplicate": "Schon in deiner Selbstauskunft enthalten",
- "failed": "Merken fehlgeschlagen. Bitte erneut versuchen."
+ "failed": "Merken fehlgeschlagen. Bitte erneut versuchen.",
+ "alreadyKept": "Das weiß der Coach schon."
},
"guided": {
"offerTitle": "Der Coach hat {count} Rückfragen an dich",
"offerTitleOne": "Der Coach hat eine Rückfrage an dich",
- "offerBody": "Kurze Antworten schärfen deine Selbstauskunft — eine Frage nach der anderen, direkt hier im Chat.",
+ "offerBody": "Kurze Antworten schärfen deine Selbstauskunft: eine Frage nach der anderen, direkt hier im Chat.",
"offerStart": "Jetzt beantworten",
"offerLater": "Später",
"offerDismiss": "Nicht mehr fragen",
@@ -2850,7 +2911,7 @@
"later": "Später",
"dismissRemaining": "Nicht mehr fragen",
"answerPlaceholder": "Deine Antwort…",
- "summaryTitle": "Geschafft — danke für deine Antworten!",
+ "summaryTitle": "Geschafft, danke für deine Antworten!",
"summaryAnswered": "{answered} von {total} beantwortet",
"summaryAdopted": "{adopted} in die Selbstauskunft übernommen",
"summaryAdoptedNone": "nichts Neues in die Selbstauskunft übernommen",
@@ -2862,6 +2923,7 @@
"historyEmpty": "Deine Unterhaltungen erscheinen hier. Stell eine Frage, um zu starten.",
"historyDeleteAria": "Unterhaltung löschen",
"historyDeleted": "Unterhaltung gelöscht",
+ "historyDeleteFailed": "Die Unterhaltung konnte nicht gelöscht werden.",
"rename": {
"action": "Unterhaltung umbenennen",
"input": "Titel der Unterhaltung",
@@ -2875,7 +2937,6 @@
"sourcesFresh": "Aktuell",
"sourcesStale": "Veraltet",
"sourcesFooter": "Der Coach liest nur Daten, die du verbunden hast.",
- "evidenceLabel": "Worauf bezieht sich das?",
"metric": {
"bp": "Blutdruck",
"weight": "Gewicht",
@@ -2921,14 +2982,6 @@
"skin_temp": "Hauttemperatur",
"workouts": "Workouts"
},
- "window": {
- "last7days": "letzte 7 Tage",
- "last30days": "letzte 30 Tage",
- "last90days": "letzte 90 Tage",
- "lastYear": "letzte 12 Monate",
- "allTime": "gesamter Zeitraum (12 Monate im Detail)"
- },
- "windowLabel": "Zeitraum",
"settingsAriaLabel": "Coach-Einstellungen",
"settingsTitle": "Coach-Einstellungen",
"settingsDescription": "Stell ein, wie der Coach mit dir spricht.",
@@ -2940,9 +2993,10 @@
"settingsVerbosityBrief": "Kurz",
"settingsVerbosityDefault": "Standard",
"settingsVerbosityDetailed": "Ausführlich",
- "settingsVerbosityConciseHint": "Der knappe Tonfall hält Antworten ohnehin kurz — die Ausführlichkeit ist dabei fest auf „Kurz“ gesetzt.",
- "settingsSourcesPointer": "Welche Datenquellen der Coach nutzen darf und das Standard-Analysefenster verwaltest du direkt im Coach-Chat (Leiste „Worauf ich zugreife“).",
- "settingsSourcesPointerLink": "Coach öffnen",
+ "settingsVerbosityConciseHint": "Der knappe Tonfall hält Antworten ohnehin kurz, die Ausführlichkeit ist dabei fest auf „Kurz“ gesetzt.",
+ "settingsSourcesSummary": "Der Coach sieht {count} von {total} Datenbereichen und blickt {window} zurück.",
+ "settingsSourcesSummaryAll": "Der Coach sieht {count} von {total} Datenbereichen und deinen ganzen Verlauf.",
+ "settingsSourcesPointerLink": "Coach-Einstellungen öffnen",
"settingsExcludeLabel": "Metriken ausschließen",
"settingsExcludeHint": "Der Coach sieht keine Daten der ausgewählten Metriken.",
"settingsContextLabel": "Kontext mitschicken",
@@ -2954,34 +3008,30 @@
"settingsSave": "Speichern",
"settingsSaved": "Gespeichert",
"settingsCancel": "Abbrechen",
- "feedbackHelpful": "Hilfreich",
- "feedbackUnhelpful": "Nicht ganz",
- "feedbackThanks": "Danke für das Signal.",
- "feedbackError": "Feedback konnte nicht gespeichert werden",
"dailyLimitTitle": "Heute-Limit erreicht",
- "dailyLimitBody": "Tageslimit erreicht. Das Budget setzt sich um Mitternacht (UTC) zurück.",
+ "dailyLimitBody": "Tageslimit erreicht. Das Budget setzt sich um Mitternacht in deiner Zeitzone zurück.",
"providerRateLimitTitle": "Provider rate-limited",
"providerRateLimitBody": "Provider ist temporär überlastet; Reset in ~5 min.",
"cluster": {
"cardio": {
"label": "Herz-Kreislauf",
- "hint": "Blutdruck · Puls · HRV · Ruhe- & Geh-Herzfrequenz"
+ "hint": "Blutdruck, Puls, HRV, Ruhe- und Geh-Herzfrequenz"
},
"body": {
"label": "Körperzusammensetzung",
- "hint": "Gewicht · Körperfett · Muskeln · BMI"
+ "hint": "Gewicht, Körperfett, Muskeln und BMI"
},
"activity": {
"label": "Aktivität",
- "hint": "Schritte · Energie · Distanz · VO₂max"
+ "hint": "Schritte, Energie, Distanz und VO₂max"
},
"workouts": {
"label": "Workouts",
- "hint": "Läufe · Radtouren · Einheiten"
+ "hint": "Läufe, Radtouren und Einheiten"
},
"sleep": {
"label": "Schlaf",
- "hint": "Dauer · Phasen"
+ "hint": "Dauer und Phasen"
},
"mood": {
"label": "Stimmung",
@@ -2993,56 +3043,68 @@
},
"medication": {
"label": "Medikation",
- "hint": "Therapietreue · GLP-1-Kontext"
+ "hint": "Therapietreue und GLP-1-Kontext"
},
"mobility": {
"label": "Mobilität & Gang",
- "hint": "Stabilität · Asymmetrie · Gangbild"
+ "hint": "Stabilität, Asymmetrie und Gangbild"
},
"environment": {
"label": "Umgebung",
- "hint": "Lärm · Tageslicht · Hauttemperatur"
+ "hint": "Lärm, Tageslicht und Hauttemperatur"
}
},
"settingsClustersLabel": "Daten, die der Coach sieht",
"settingsClustersHint": "Wähle, welche Datencluster der Coach lesen darf. Mehr Daten bedeuten reichhaltigere Antworten und einen größeren Prompt.",
"settingsClusterNoData": "Noch keine Daten",
- "sourcesMemberCount": "{count} Werte",
+ "sourcesMemberCountOne": "1 Wert",
+ "sourcesMemberCountFew": "{count} Werte",
+ "sourcesMemberCountOther": "{count} Werte",
+ "lookback": {
+ "label": "Wie weit der Coach zurückblickt",
+ "option": {
+ "last7days": "{days} Tage",
+ "last30days": "{days} Tage",
+ "last90days": "{days} Tage",
+ "lastYear": "{months} Monate",
+ "allTime": "Alles"
+ },
+ "detailDays": "Der Coach liest nichts, was älter als {days} Tage ist, in keiner Antwort.",
+ "detailMonths": "Der Coach liest nichts, was älter als {months} Monate ist, in keiner Antwort.",
+ "detailAll": "Alles, was du erfasst hast: {dailyMonths} Monate tagesgenau, ältere Werte als Monatswerte bis {years} Jahre zurück, Laborwerte der letzten {labMonths} Monate.",
+ "note": "Dein Profil, aktuelle Medikamente, eine laufende Erkrankung und anstehende Termine bleiben sichtbar; Auswertungen, die mehr Verlauf brauchen, werden gekürzt oder weggelassen. Antworten, die der Coach früher in älteren Unterhaltungen gegeben hat, können noch ältere Werte nennen."
+ },
"showConversations": "Unterhaltungen anzeigen",
"hideConversations": "Unterhaltungen ausblenden",
"heroGreeting": "Frage mich etwas zu deinen Daten",
"heroSubline": "Frag nach deinen Verläufen, Medikamenten oder Messwerten.",
- "thinkingElapsed": "{seconds}s",
"thinkingDuration": "Nachgedacht für {seconds}s",
"thinkingExpandAria": "Zeigen, was der Coach berücksichtigt hat",
"thinkingDetail": "Beantwortet aus deinem Gesundheits-Snapshot.",
"tokensUsed": "{count} Tokens",
- "tokensUsedWithModel": "{count} Tokens · {model}",
"askAboutThis": "Den Coach dazu fragen",
"readStrip": {
"label": "Üblicher Bereich",
"baselineWithin": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darin.",
"baselineAbove": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darüber.",
"baselineBelow": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darunter.",
- "insufficient": "Lerne noch deinen üblichen Bereich — protokolliere noch ein paar Tage weiter.",
+ "sameHoursWithin": "Bis zu dieser Tageszeit liegst du sonst bei {low}–{high} {unit}; heute bisher {value}, also im Bereich.",
+ "sameHoursAbove": "Bis zu dieser Tageszeit liegst du sonst bei {low}–{high} {unit}; heute bisher {value}, also darüber.",
+ "sameHoursBelow": "Bis zu dieser Tageszeit liegst du sonst bei {low}–{high} {unit}; heute bisher {value}, also darunter.",
+ "steadyWithin": "Deine Werte liegen meist bei {low} {unit}; heute passt {value} dazu.",
+ "steadyAbove": "Deine Werte liegen meist bei {low} {unit}; heute liegt {value} darüber.",
+ "steadyBelow": "Deine Werte liegen meist bei {low} {unit}; heute liegt {value} darunter.",
+ "latestWithin": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; dein letzter Wert vom {date} lag mit {value} darin.",
+ "latestAbove": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; dein letzter Wert vom {date} lag mit {value} darüber.",
+ "latestBelow": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; dein letzter Wert vom {date} lag mit {value} darunter.",
+ "steadyLatestWithin": "Deine Werte liegen meist bei {low} {unit}; dein letzter Wert vom {date} lag mit {value} genau dort.",
+ "steadyLatestAbove": "Deine Werte liegen meist bei {low} {unit}; dein letzter Wert vom {date} lag mit {value} darüber.",
+ "steadyLatestBelow": "Deine Werte liegen meist bei {low} {unit}; dein letzter Wert vom {date} lag mit {value} darunter.",
+ "insufficient": "Lerne noch deinen üblichen Bereich. Protokolliere noch ein paar Tage weiter.",
"driver": "{note}"
},
"scope": {
- "prefix": "Der Coach ist schon bei {metric}",
- "tap": "Zum Fragen tippen",
- "question": "Erkläre mir diese Kennzahl — worauf sollte ich achten?"
- },
- "seeded": {
- "prefix": "Heute einen Blick wert: {signal}",
- "signal": {
- "readiness": "Bereitschaft",
- "recovery": "Erholung"
- },
- "question": {
- "readiness": "Meine Bereitschaft ist heute auffällig — woran liegt das?",
- "recovery": "Meine Erholung hat sich verändert — was steckt dahinter?"
- },
- "dismiss": "Ausblenden"
+ "prefix": "Der Coach ist schon bei {metric}"
},
"settings": "Einstellungen",
"history": {
@@ -3052,39 +3114,39 @@
"groupEarlier": "Früher"
},
"seed": {
- "bloodPressure": "Geh mit mir meinen Blutdruckverlauf der letzten 30 Tage durch — gibt es etwas, das ich im Auge behalten sollte?",
+ "bloodPressure": "Geh mit mir meinen Blutdruckverlauf der letzten 30 Tage durch. Gibt es etwas, das ich im Auge behalten sollte?",
"weight": "Wie hat sich mein Gewicht zuletzt entwickelt, und ist die Richtung etwas, worauf ich reagieren sollte?",
"bmi": "Was sagt mir mein BMI-Verlauf, und wie sollte ich ihn lesen?",
- "pulse": "Geh mit mir meine Pulswerte durch — ist irgendetwas ungewöhnlich?",
+ "pulse": "Geh mit mir meine Pulswerte durch. Ist irgendetwas ungewöhnlich?",
"restingHr": "Wie hat sich mein Ruhepuls entwickelt, und was sagt er über meine Fitness?",
"hrv": "Was sagt mir meine Herzfrequenzvariabilität zuletzt über Erholung und Stress?",
- "sleep": "Geh mit mir meinen letzten Schlaf durch — Dauer, Regelmäßigkeit und was sich zu ändern lohnt.",
+ "sleep": "Geh mit mir meinen letzten Schlaf durch: Dauer, Regelmäßigkeit und was sich zu ändern lohnt.",
"mood": "Welche Muster siehst du in meiner Stimmung über die letzten Wochen?",
"medications": "Wie sieht meine Therapietreue aus, und passt sie dazu, wie ich mich gefühlt habe?",
"steps": "Wie aktiv war ich zuletzt gemessen an meinen Schritten, und wie ist das im Vergleich zu einem gesunden Richtwert?",
"activeEnergy": "Was sagt mein Aktivenergie-Verlauf über mein Aktivitätsniveau zuletzt?",
"cardioFitness": "Was bedeutet mein Cardio-Fitness-Verlauf (VO₂max), und wie bewege ich ihn in die richtige Richtung?",
- "bloodGlucose": "Geh mit mir meine letzten Glukosewerte durch — liegen sie in einem gesunden Bereich?",
+ "bloodGlucose": "Geh mit mir meine letzten Glukosewerte durch. Liegen sie in einem gesunden Bereich?",
"oxygenSaturation": "Was sagt mir mein Sauerstoffsättigungs-Verlauf (SpO₂)?",
"respiratoryRate": "Was sagt mein Atemfrequenz-Verlauf über meine Gesundheit?",
- "workouts": "Geh mit mir meine letzten Workouts durch — Belastung, Häufigkeit und wie ich mich dazwischen erhole.",
+ "workouts": "Geh mit mir meine letzten Workouts durch: Belastung, Häufigkeit und wie ich mich dazwischen erhole.",
"recoveryPage": "Warum steht meine Erholung gerade dort, wo sie steht, und was treibt das?",
- "briefing": "Geh mit mir das heutige Briefing durch — worauf sollte ich mich konzentrieren, und gibt es etwas, das einen Schritt wert ist?",
+ "briefing": "Geh mit mir das heutige Briefing durch. Worauf sollte ich mich konzentrieren, und gibt es etwas, das einen Schritt wert ist?",
"healthScore": "Warum liegt mein Gesundheits-Score bei {score} von 100, und was würde ihn am stärksten bewegen?",
- "periodWeek": "Geh mit mir durch, wie meine letzte Woche über meine Gesundheitswerte hinweg lief — was sticht heraus?",
- "periodMonth": "Geh mit mir durch, wie mein letzter Monat über meine Gesundheitswerte hinweg lief — was sticht heraus?",
+ "periodWeek": "Geh mit mir durch, wie meine letzte Woche über meine Gesundheitswerte hinweg lief. Was sticht heraus?",
+ "periodMonth": "Geh mit mir durch, wie mein letzter Monat über meine Gesundheitswerte hinweg lief. Was sticht heraus?",
"correlationBpCompliance": "Wie hängt meine Therapietreue mit meinem Blutdruck zusammen? Geh mit mir durch, was dieser Zusammenhang für mich bedeutet.",
"correlationMoodPulse": "Gibt es einen echten Zusammenhang zwischen meiner Stimmung und meinem Puls? Hilf mir, diesen Zusammenhang zu lesen.",
"correlationWeightWeekday": "Folgt mein Gewicht einem Wochenmuster? Was sagt mir dieser Wochentags-Zusammenhang?",
- "recommendation": "Erzähl mir mehr zu dieser Empfehlung: „{text}“ — warum trifft sie auf mich zu, und was sollte ich tun?"
+ "recommendation": "Erzähl mir mehr zu dieser Empfehlung: „{text}“ Warum trifft sie auf mich zu, und was sollte ich tun?"
},
- "assessmentPrompt": "Erkläre mir meine {metric}-Einschätzung — was bedeutet sie und was sollte ich tun?",
+ "assessmentPrompt": "Erkläre mir meine {metric}-Einschätzung. Was bedeutet sie und was sollte ich tun?",
"attach": {
"menuChooseFromDocuments": "Aus Dokumenten wählen",
"menuUploadNew": "Neues Dokument hochladen",
"trigger": "Dokumente anhängen",
"pickerTitle": "Dokumente anhängen",
- "pickerDescription": "Der Coach liest nur die angehängten Dokumente — nicht deine Gesundheitsdaten.",
+ "pickerDescription": "Der Coach liest nur die angehängten Dokumente, nicht deine Gesundheitsdaten.",
"pickerSearchPlaceholder": "Dokumente durchsuchen…",
"pickerEmpty": "Noch keine Dokumente in deinem Tresor.",
"pickerNotIndexed": "Noch nicht indexiert",
@@ -3092,13 +3154,13 @@
"pickerConfirm": "Anhängen ({count})",
"pillRemove": "{title} entfernen",
"indexing": "Wird indexiert…",
- "indexingHint": "Ein Dokument wird noch indexiert — du kannst senden, sobald es bereit ist.",
- "fencedHint": "Diese Unterhaltung läuft ohne Zugriff auf deine Gesundheitsdaten — der Coach liest nur die angehängten Dokumente.",
+ "indexingHint": "Ein Dokument wird noch indexiert. Du kannst senden, sobald es bereit ist.",
+ "fencedHint": "Diese Unterhaltung läuft ohne Zugriff auf deine Gesundheitsdaten. Der Coach liest nur die angehängten Dokumente.",
"documentsAttachedOne": "1 Dokument angehängt",
"documentsAttachedOther": "{count} Dokumente angehängt",
"railBadge": "Dokument-Chat (abgeschottet)",
"uploadError": "Upload fehlgeschlagen. Bitte versuche es erneut.",
- "notFoundError": "Dieser Dokumentlink ist nicht mehr gültig — es wird ein normaler Chat gestartet."
+ "notFoundError": "Dieser Dokumentlink ist nicht mehr gültig. Es wird ein normaler Chat gestartet."
},
"interrupted": {
"note": "Diese Antwort wurde unterbrochen, bevor sie fertig war.",
@@ -3111,7 +3173,102 @@
"errorNotPermitted": "Der Coach ist in einer Akte, die jemand mit dir teilt, nicht verfügbar.",
"errorUnavailable": "Der Coach konnte seine Einstellungen gerade nicht prüfen. Bitte versuch es gleich noch einmal.",
"errorOperatorOff": "KI ist auf diesem Server abgeschaltet, deshalb kann der Coach nicht antworten.",
- "errorCoachOff": "Der Coach ist ausgeschaltet. Du kannst ihn unter Einstellungen, Module wieder einschalten."
+ "errorCoachOff": "Der Coach ist ausgeschaltet. Du kannst ihn unter Einstellungen, Module wieder einschalten.",
+ "frame": {
+ "showPanel": "Unterhaltungen einblenden",
+ "hidePanel": "Unterhaltungen ausblenden",
+ "panelDescription": "Deine Unterhaltungen mit dem Coach, neueste zuerst.",
+ "rowActions": "Aktionen für {title}",
+ "rename": "Umbenennen",
+ "delete": "Löschen",
+ "modelSection": "Modell",
+ "provider": "Antwortet mit",
+ "modelManaged": "Das Modell für diesen Anbieter legst du in den KI-Einstellungen fest.",
+ "noProvider": "Kein Anbieter verfügbar",
+ "saving": "Wird gespeichert…",
+ "saveFailed": "Die Änderung ließ sich nicht speichern.",
+ "allSettings": "Alle KI-Einstellungen",
+ "providerNotActive": "{provider} konnte nicht übernehmen: Es fehlt eine funktionierende Anmeldung oder ein Schlüssel. Richte das in den KI-Einstellungen ein.",
+ "operatorCodex": "ChatGPT (vom Betreiber bereitgestellt)",
+ "operatorManaged": "Diese Verbindung verwaltet der Betreiber deines Servers. Sobald einer deiner eigenen Anbieter eingerichtet ist, antwortet er zuerst."
+ },
+ "answer": {
+ "scrollToBottom": "Zur neuesten Nachricht",
+ "rememberDone": "Gemerkt",
+ "timeYesterday": "gestern {time}",
+ "timeEarlier": "{date}, {time}",
+ "info": "Details zur Antwort",
+ "infoModel": "Modell: {model}"
+ },
+ "activity": {
+ "thinking": "Denkt nach…",
+ "fetching": "Holt {domain}, {window}…",
+ "digestOne": "Fasst {count} Wert zusammen…",
+ "digestFew": "Fasst {count} Werte zusammen…",
+ "digestOther": "Fasst {count} Werte zusammen…",
+ "digestDoneOne": "{count} Wert aus {areas}",
+ "digestDoneFew": "{count} Werte aus {areas}",
+ "digestDoneOther": "{count} Werte aus {areas}",
+ "areasOne": "{count} Bereich",
+ "areasFew": "{count} Bereichen",
+ "areasOther": "{count} Bereichen",
+ "memoryOne": "Erinnert sich an {count} Eintrag",
+ "memoryFew": "Erinnert sich an {count} Einträge",
+ "memoryOther": "Erinnert sich an {count} Einträge",
+ "remember": "Merkt sich etwas…",
+ "plan": "Schlägt einen Plan vor…",
+ "asking": "Fragt nach…",
+ "answer": "Schreibt die Antwort…",
+ "stop": {
+ "budget": "Budget erreicht, antwortet mit dem, was da ist",
+ "time": "Zeit erreicht, antwortet mit dem, was da ist",
+ "cap": "Viele Schritte, fasst jetzt zusammen",
+ "noProgress": "Keine neuen Daten mehr, fasst zusammen"
+ },
+ "thoughtProcessOne": "Denkprozess ({count} Schritt)",
+ "thoughtProcessFew": "Denkprozess ({count} Schritte)",
+ "thoughtProcessOther": "Denkprozess ({count} Schritte)",
+ "awaitingAnswer": "Antwort nötig"
+ },
+ "memory": {
+ "saved": "Gemerkt: {fact}",
+ "undo": "Rückgängig",
+ "accept": "Ja, merken",
+ "decline": "Nein",
+ "confirmed": "Gemerkt.",
+ "declined": "Alles klar, dann nicht."
+ },
+ "plan": {
+ "accept": "Plan übernehmen",
+ "decline": "Nicht jetzt",
+ "confirmed": "Ist notiert. Ich schaue in {days} Tagen mit dir drauf.",
+ "declined": "Alles klar, dann nicht jetzt."
+ },
+ "assumption": {
+ "line": "Angenommen: {value}",
+ "change": "Ändern"
+ },
+ "suggestedReplies": {
+ "groupLabel": "Antwortvorschläge"
+ },
+ "chart": {
+ "compare": {
+ "periods": "{a} gegen {b}"
+ }
+ },
+ "reasoning": {
+ "label": "Denktiefe",
+ "hint": "Gründlicher denken heißt genauere Antworten und etwas mehr Wartezeit.",
+ "off": "Aus",
+ "minimal": "Minimal",
+ "low": "Niedrig",
+ "medium": "Mittel",
+ "high": "Hoch",
+ "capped": "(vom Admin begrenzt)",
+ "disabled": "Der Admin hat gründliches Denken abgeschaltet."
+ },
+ "memoryLink": "Gedächtnis ({count})",
+ "newConversation": "Neue Unterhaltung"
},
"relativeJustNow": "gerade eben",
"relativeMinutesAgoOne": "vor {count} Minute",
@@ -3174,7 +3331,7 @@
},
"hrv": {
"title": "HRV",
- "description": "Herzratenvariabilität im Verlauf — dein Erholungssignal des autonomen Nervensystems, nächtlich von Apple Watch und kompatiblen Wearables erfasst.",
+ "description": "Herzratenvariabilität im Verlauf: dein Erholungssignal des autonomen Nervensystems, nächtlich von Apple Watch und kompatiblen Wearables erfasst.",
"chartTitle": "Herzratenvariabilität",
"emptyState": {
"title": "Noch keine HRV-Werte",
@@ -3183,17 +3340,17 @@
},
"restingHr": {
"title": "Ruhepuls",
- "description": "Die tägliche Grundlinie deines Herzschlags — unterschieden vom Spot-Puls. Apple Health liefert einen Wert pro Tag.",
+ "description": "Die tägliche Grundlinie deines Herzschlags, unterschieden vom Spot-Puls. Apple Health liefert einen Wert pro Tag.",
"chartTitle": "Ruhepuls",
"emptyState": {
"title": "Noch kein Ruhepuls",
"description": "Der Ruhepuls erscheint, sobald die Apple Watch oder ein kompatibles Withings-Gerät einen ruhigen Tag durchgetragen hat."
},
- "coachPrefill": "Ich habe noch keinen Ruhepuls erfasst — warum unterscheidet sich der tägliche Ruhewert von einem punktuellen Puls, und wie verbessere ich ihn?"
+ "coachPrefill": "Ich habe noch keinen Ruhepuls erfasst. Warum unterscheidet sich der tägliche Ruhewert von einem punktuellen Puls, und wie verbessere ich ihn?"
},
"oxygenSaturation": {
"title": "Sauerstoffsättigung",
- "description": "Blutsauerstoff (SpO₂) — klinischer Normalbereich 95–100 %; Zielzonen sind am Chart hinterlegt.",
+ "description": "Blutsauerstoff (SpO₂), klinischer Normalbereich 95–100 %; Zielzonen sind am Chart hinterlegt.",
"chartTitle": "Sauerstoffsättigung",
"emptyState": {
"title": "Noch keine SpO₂-Werte",
@@ -3206,13 +3363,13 @@
"chartTitle": "Körpertemperatur",
"emptyState": {
"title": "Noch keine Körpertemperatur",
- "description": "Erfasse eine Körpertemperatur oder lass dein Wearable einen Wert synchronisieren — der Verlauf erscheint dann hier.",
+ "description": "Erfasse eine Körpertemperatur oder lass dein Wearable einen Wert synchronisieren. Der Verlauf erscheint dann hier.",
"cta": "Körpertemperatur erfassen"
}
},
"steps": {
"title": "Schritte",
- "description": "Tägliche Schrittzahl — eine Zeile pro Tag, aus Apple Health oder einem verbundenen Aktivitätsgerät.",
+ "description": "Tägliche Schrittzahl, eine Zeile pro Tag, aus Apple Health oder einem verbundenen Aktivitätsgerät.",
"chartTitle": "Schritte pro Tag",
"emptyState": {
"title": "Noch keine Schrittdaten",
@@ -3221,7 +3378,7 @@
},
"activeEnergy": {
"title": "Aktive Energie",
- "description": "Tägliche aktive-Energie-Summe in Kilokalorien — eine Zeile pro Tag, aus Apple Health oder Withings-Aktivität.",
+ "description": "Tägliche aktive-Energie-Summe in Kilokalorien, eine Zeile pro Tag, aus Apple Health oder Withings-Aktivität.",
"chartTitle": "Aktive Energie",
"emptyState": {
"title": "Noch keine Aktive-Energie-Werte",
@@ -3241,11 +3398,11 @@
},
"explainer": {
"bloodPressureBody": "Der Blutdruck ist die Kraft, die das Blut auf die Gefäßwände ausübt, angegeben als systolisch zu diastolisch (Spitzenwert beim Herzschlag über dem Ruhewert dazwischen). Dauerhaft hohe Werte belasten Herz und Gefäße auf Dauer.",
- "pulseBody": "Der Puls gibt an, wie oft das Herz pro Minute schlägt. Eine Einzelmessung spiegelt den Moment wider — in Ruhe liegt er meist bei 60–100 Schlägen pro Minute und steigt natürlicherweise bei Bewegung, Stress oder Koffein.",
+ "pulseBody": "Der Puls gibt an, wie oft das Herz pro Minute schlägt. Eine Einzelmessung spiegelt den Moment wider. In Ruhe liegt er meist bei 60–100 Schlägen pro Minute und steigt natürlicherweise bei Bewegung, Stress oder Koffein.",
"weightBody": "Das Körpergewicht ist eines der einfachsten Signale für die langfristige Energiebilanz und die allgemeine Gesundheit. Der Trend über Wochen zählt weit mehr als ein einzelner Wert, der mit Flüssigkeit, Ernährung und Tageszeit schwankt.",
"bmiBody": "Der BMI ist dein Gewicht geteilt durch deine Körpergröße zum Quadrat und ordnet dich einer groben Gewichtskategorie zu. Er ist ein Orientierungswert, keine Diagnose, und unterscheidet nicht zwischen Muskeln und Fett.",
"sleepBody": "Die Schlafdauer ist die tatsächlich geschlafene Zeit pro Nacht, aufgeteilt in Leicht-, Tief- und REM-Schlaf. Die meisten Erwachsenen brauchen etwa sieben bis neun Stunden; gleichmäßige Menge und Qualität unterstützen Erholung, Stimmung und Stoffwechsel.",
- "moodBody": "Die Stimmungserfassung hält auf einer einfachen Skala fest, wie du dich fühlst. Regelmäßiges Protokollieren zeigt Muster — wie Schlaf, Aktivität oder Medikamente mit dem Befinden zusammenhängen –, die ein einzelner Tag nie offenbart.",
+ "moodBody": "Die Stimmungserfassung hält auf einer einfachen Skala fest, wie du dich fühlst. Regelmäßiges Protokollieren zeigt Muster (wie Schlaf, Aktivität oder Medikamente mit dem Befinden zusammenhängen), die ein einzelner Tag nie offenbart.",
"medicationsBody": "Die Therapietreue zeigt, wie zuverlässig du deine Medikamente wie verordnet einnimmst, hier als Anteil der erwarteten Dosen, die du erfasst hast. Eine gleichmäßige Einnahme entscheidet oft darüber, ob eine Therapie wie beabsichtigt wirkt.",
"restingHrBody": "Der Ruhepuls ist die Herzfrequenz in völliger Ruhe, meist über Nacht gemessen. Ein niedriger Ruhepuls deutet in der Regel auf eine bessere Herz-Kreislauf-Fitness hin, ein steigender Trend kann ein frühes Zeichen für Stress oder Krankheit sein.",
"hrvBody": "Die HRV ist die kleine Schwankung der Zeit zwischen aufeinanderfolgenden Herzschlägen. Eine höhere Variabilität spiegelt meist ein gut erholtes, anpassungsfähiges Nervensystem wider, ein anhaltender Rückgang kann auf Erschöpfung, Stress oder mangelnde Erholung hinweisen.",
@@ -3256,12 +3413,12 @@
"respiratoryRateBody": "Die Atemfrequenz ist die Anzahl der Atemzüge pro Minute. In Ruhe atmen die meisten Erwachsenen etwa zwölf- bis zwanzigmal pro Minute; ein anhaltender Anstieg kann mit Krankheit, Anstrengung oder Stress einhergehen.",
"bodyWaterBody": "Das Gesamtkörperwasser ist der Anteil deines Körpergewichts, der aus Flüssigkeit in und um die Zellen besteht. Es schwankt mit Flüssigkeitshaushalt, Ernährung und Aktivität, daher sagt der Trend über Wochen mehr aus als ein einzelner Wert.",
"boneMassBody": "Die Knochenmasse ist das geschätzte Gewicht der Mineralien in deinem Skelett. Waagengestützte Schätzungen sind ungefähr, doch ein gleichbleibender oder steigender Trend unterstützt die langfristige Knochenstärke.",
- "fatFreeMassBody": "Die fettfreie Masse ist alles in deinem Körper, das kein Fett ist — Muskeln, Knochen, Organe und Wasser. Sie zu halten oder aufzubauen, während das Gewicht sinkt, deutet meist auf eine gesündere Körperzusammensetzung hin.",
- "fatMassBody": "Die Fettmasse ist das Gesamtgewicht des Körperfetts. Etwas Fett ist für Hormone und Energiespeicher unverzichtbar, doch zu viel — besonders am Bauch — erhöht das langfristige Gesundheitsrisiko (WHO 2024).",
+ "fatFreeMassBody": "Die fettfreie Masse ist alles in deinem Körper, das kein Fett ist: Muskeln, Knochen, Organe und Wasser. Sie zu halten oder aufzubauen, während das Gewicht sinkt, deutet meist auf eine gesündere Körperzusammensetzung hin.",
+ "fatMassBody": "Die Fettmasse ist das Gesamtgewicht des Körperfetts. Etwas Fett ist für Hormone und Energiespeicher unverzichtbar, doch zu viel (besonders am Bauch) erhöht das langfristige Gesundheitsrisiko (WHO 2024).",
"bodyFatBody": "Der Körperfettanteil ist der Teil deines Gewichts, der aus Fett besteht. Gesunde Bereiche hängen von Geschlecht und Alter ab, und Waagen schätzen den Wert per Bioimpedanz, deshalb sagt der Verlauf über mehrere Wochen mehr als eine einzelne Messung.",
"muscleMassBody": "Die Muskelmasse ist das geschätzte Gewicht der Muskeln in deinem Körper. Mehr Muskeln unterstützen Kraft, Stoffwechsel und Beweglichkeit und müssen mit zunehmendem Alter aktiv erhalten werden.",
"visceralFatBody": "Viszeralfett ist das Fett, das tief im Bauchraum um die Organe gespeichert wird. Anders als das Unterhautfett ist ein hoher Wert eng mit Herzkrankheiten und Typ-2-Diabetes verbunden, daher ist niedriger meist besser.",
- "leanBodyMassBody": "Die Magermasse ist dein Gesamtgewicht abzüglich des Körperfetts — Muskeln, Knochen, Organe und Wasser zusammen. Sie neben dem Gewicht zu verfolgen zeigt, ob Veränderungen aus Muskeln oder aus Fett stammen.",
+ "leanBodyMassBody": "Die Magermasse ist dein Gesamtgewicht abzüglich des Körperfetts, also Muskeln, Knochen, Organe und Wasser zusammen. Sie neben dem Gewicht zu verfolgen zeigt, ob Veränderungen aus Muskeln oder aus Fett stammen.",
"flightsClimbedBody": "Die gestiegenen Etagen zählen die Stockwerke, die du täglich nach oben gehst, wobei eine Etage etwa drei Metern Höhe entspricht. Es ist ein einfacher Marker für Alltagsaktivität und Beinarbeit.",
"walkingDistanceBody": "Die Geh- und Laufstrecke ist die Strecke, die du täglich zu Fuß zurücklegst. Sie ergänzt die Schrittzahl, indem sie zeigt, wie weit du dich bewegt hast, nicht nur wie oft du getreten bist.",
"walkingSteadinessBody": "Die Gehstabilität schätzt, wie stabil und ausgewogen dein Gangbild ist, eingeteilt von OK bis niedrig. Ein niedriger Wert deutet auf ein höheres Sturzrisiko hin und sollte ärztlich besprochen werden.",
@@ -3278,28 +3435,28 @@
"daylightBody": "Die Zeit im Tageslicht ist, wie lange du dich täglich im Freien im Licht aufhältst. Regelmäßiges Tageslicht unterstützt einen gesunden Schlaf-Wach-Rhythmus und die Stimmung (CDC 2024), und Zeit im Freien ist mit der Augengesundheit verbunden.",
"bloodGlucoseBody": "Der Blutzucker ist die Zuckermenge in deinem Blut, die wichtigste Energiequelle des Körpers. Er steigt nach dem Essen und pendelt sich zwischen den Mahlzeiten ein; dauerhaft hohe Werte können auf Diabetes oder Prädiabetes hinweisen.",
"skinTemperatureBody": "Die Hauttemperatur ist hier die kleine nächtliche Abweichung deiner Handgelenkstemperatur von deinem eigenen Ausgangswert, keine absolute Fiebermessung. Veränderungen können Schlaf, Umgebung, Krankheit oder Zyklusphase widerspiegeln.",
- "cardioFitnessBody": "VO₂max gibt an, wie viel Sauerstoff dein Körper bei maximaler Anstrengung verwerten kann — der beste Laborindex der Ausdauerfitness, den Wearables indirekt schätzen. Ein höherer oder steigender Trend ist ein starkes Zeichen für eine gute Herz-Kreislauf-Gesundheit.",
- "cardioRecoveryBody": "Der Erholungspuls gibt an, um wie viele Schläge pro Minute dein Puls in der ersten Minute nach dem Ende einer anstrengenden Belastung fällt. Ein größerer Abfall bedeutet, dass sich dein Herz schneller beruhigt — ein Zeichen guter Fitness.",
+ "cardioFitnessBody": "VO₂max gibt an, wie viel Sauerstoff dein Körper bei maximaler Anstrengung verwerten kann. Es ist der beste Laborindex der Ausdauerfitness, den Wearables indirekt schätzen. Ein höherer oder steigender Trend ist ein starkes Zeichen für eine gute Herz-Kreislauf-Gesundheit.",
+ "cardioRecoveryBody": "Der Erholungspuls gibt an, um wie viele Schläge pro Minute dein Puls in der ersten Minute nach dem Ende einer anstrengenden Belastung fällt. Ein größerer Abfall bedeutet, dass sich dein Herz schneller beruhigt, ein Zeichen guter Fitness.",
"wristTemperatureBody": "Die Handgelenktemperatur wird im Schlaf gemessen und als Abweichung von deinem persönlichen Ausgangswert angegeben, nicht als absoluter Wert. Ein gleichmäßiger Wert ist normal, ein anhaltender Anstieg kann auf Krankheit, intensives Training oder eine Zyklusveränderung hindeuten.",
- "fallsBody": "Hier werden die von deinem Wearable erkannten schweren Stürze gezählt. Null ist das Ziel; eine steigende Zahl ist ein Hinweis auf ein erhöhtes Sturzrisiko, das du — besonders im höheren Alter — mit einer Ärztin oder einem Arzt besprechen solltest (CDC STEADI 2024).",
- "sixMinuteWalkBody": "Dies ist eine Schätzung, wie weit du in sechs Minuten gehen könntest — ein etablierter klinischer Test für Mobilität und kardiopulmonale Ausdauer. Der Wearable-Wert ist ein Näherungswert, nicht der überwachte klinische Test; aussagekräftig ist der gleichbleibende oder steigende Trend.",
- "stairAscentSpeedBody": "Dies gibt an, wie schnell du eine Treppe hinaufgehst. Es ist ein empfindlicher Marker für Beinkraft und Mobilität — ein gleichbleibendes oder steigendes Tempo ist gut, ein sinkendes solltest du im Blick behalten.",
- "stairDescentSpeedBody": "Dies gibt an, wie schnell du eine Treppe hinabgehst. Zusammen mit dem Steig-Tempo zeigt es Beinkontrolle und Gleichgewicht — ein gleichmäßiges Tempo ist gut, ein sinkendes solltest du im Blick behalten.",
- "breathingDisturbancesBody": "Ein Wearable kann schätzen, wie oft deine Atmung im Schlaf unterbrochen war. Ein niedriger Index ist besser; ein als erhöht eingestufter Wert — vor allem über viele Nächte — sollte mit einer Ärztin oder einem Arzt besprochen werden. Es ist ein Screening-Näherungswert, keine Schlafapnoe-Diagnose.",
- "stepsBody": "Die Schrittzahl ist die Gesamtzahl der Schritte, die du täglich gehst — das einfachste Maß für Alltagsbewegung. Eine stetige Gewohnheit zählt mehr als ein einzelner großer Tag, und das runde Ziel von 10.000 Schritten ist ein Werbeslogan, keine Forschung.",
- "readinessBody": "Die Bereitschaft ist ein einzelner Tageswert dafür, wie bereit dein Körper für den Tag ist — er verbindet Ruhepuls, Herzfrequenzvariabilität und Schlaf. Es ist ein Geräte-Näherungswert an deinem eigenen Ausgangswert, keine klinische Messung: ein höherer Wert heißt erholt, ein niedrigerer ist ein sanfter Hinweis, einen Gang zurückzuschalten.",
- "sleepScoreBody": "Dein Schlafscore fasst eine ganze Nacht in einer Zahl zusammen — aus Dauer, Effizienz, Regelmäßigkeit und der Verteilung von Leicht-, Tief- und REM-Schlaf. Achte eher auf das Muster über Wochen als auf einen perfekten Wert in einer einzelnen Nacht.",
- "recoveryBody": "Die Erholung zeigt, wie gut sich dein Körper regeneriert hat — gestützt auf nächtliche Signale wie Herzfrequenzvariabilität und Ruhepuls. Es ist ein geräteabgeleiteter Näherungswert an deinem eigenen Ausgangswert, keine klinische Messung, und zwischen Geräten nicht vergleichbar. Ein hoher Wert ist grünes Licht, mehr zu geben; ein niedrigerer deutet an, dass dein System noch aufbaut.",
- "strainBody": "Die Belastung erfasst, wie viel kardiovaskuläre und körperliche Last du deinem Körper zugemutet hast — von Workouts bis zu einem aktiven Tag auf den Beinen. Es ist ein geräteabgeleiteter Index, gemessen an deinem eigenen üblichen Aufwand, keine klinische Messung, und der Kennwert ist zwischen Geräten nicht vergleichbar.",
- "stressBody": "Stress ist hier eine Geräte-Heuristik aus Mustern der Herzfrequenzvariabilität — keine Cortisol-Messung und nicht, wie du dich bewusst fühlst, und auch kein klinischer Stress-Score. Lies ihn an deinem eigenen Ausgangswert; zwischen Geräten ist er nicht vergleichbar. Ein niedrigerer Wert deutet auf einen ruhigen, gut regulierten Zustand hin; ein höherer ist eine Einladung, Erholung einzuplanen.",
- "recoveryPageBody": "Die Erholung fasst die nächtlichen Signale deines Wearables zusammen — Recharge, Belastung, Load sowie deine Herzfrequenz- und Energiewerte des Tages — und zeigt, wie gut sich dein Körper erholt hat. Achte eher auf das Muster über mehrere Tage als auf einen einzelnen Wert; ein schwächerer Wert ist eine Einladung, kürzerzutreten, kein Alarm.",
+ "fallsBody": "Hier werden die von deinem Wearable erkannten schweren Stürze gezählt. Null ist das Ziel; eine steigende Zahl ist ein Hinweis auf ein erhöhtes Sturzrisiko, das du (besonders im höheren Alter) mit einer Ärztin oder einem Arzt besprechen solltest (CDC STEADI 2024).",
+ "sixMinuteWalkBody": "Dies ist eine Schätzung, wie weit du in sechs Minuten gehen könntest, ein etablierter klinischer Test für Mobilität und kardiopulmonale Ausdauer. Der Wearable-Wert ist ein Näherungswert, nicht der überwachte klinische Test; aussagekräftig ist der gleichbleibende oder steigende Trend.",
+ "stairAscentSpeedBody": "Dies gibt an, wie schnell du eine Treppe hinaufgehst. Es ist ein empfindlicher Marker für Beinkraft und Mobilität. Ein gleichbleibendes oder steigendes Tempo ist gut, ein sinkendes solltest du im Blick behalten.",
+ "stairDescentSpeedBody": "Dies gibt an, wie schnell du eine Treppe hinabgehst. Zusammen mit dem Steig-Tempo zeigt es Beinkontrolle und Gleichgewicht. Ein gleichmäßiges Tempo ist gut, ein sinkendes solltest du im Blick behalten.",
+ "breathingDisturbancesBody": "Ein Wearable kann schätzen, wie oft deine Atmung im Schlaf unterbrochen war. Ein niedriger Index ist besser; ein als erhöht eingestufter Wert (vor allem über viele Nächte) sollte mit einer Ärztin oder einem Arzt besprochen werden. Es ist ein Screening-Näherungswert, keine Schlafapnoe-Diagnose.",
+ "stepsBody": "Die Schrittzahl ist die Gesamtzahl der Schritte, die du täglich gehst, das einfachste Maß für Alltagsbewegung. Eine stetige Gewohnheit zählt mehr als ein einzelner großer Tag, und das runde Ziel von 10.000 Schritten ist ein Werbeslogan, keine Forschung.",
+ "readinessBody": "Die Bereitschaft ist ein einzelner Tageswert dafür, wie bereit dein Körper für den Tag ist. Er verbindet Ruhepuls, Herzfrequenzvariabilität und Schlaf. Es ist ein Geräte-Näherungswert an deinem eigenen Ausgangswert, keine klinische Messung: ein höherer Wert heißt erholt, ein niedrigerer ist ein sanfter Hinweis, einen Gang zurückzuschalten.",
+ "sleepScoreBody": "Dein Schlafscore fasst eine ganze Nacht in einer Zahl zusammen, aus Dauer, Effizienz, Regelmäßigkeit und der Verteilung von Leicht-, Tief- und REM-Schlaf. Achte eher auf das Muster über Wochen als auf einen perfekten Wert in einer einzelnen Nacht.",
+ "recoveryBody": "Die Erholung zeigt, wie gut sich dein Körper regeneriert hat, gestützt auf nächtliche Signale wie Herzfrequenzvariabilität und Ruhepuls. Es ist ein geräteabgeleiteter Näherungswert an deinem eigenen Ausgangswert, keine klinische Messung, und zwischen Geräten nicht vergleichbar. Ein hoher Wert ist grünes Licht, mehr zu geben; ein niedrigerer deutet an, dass dein System noch aufbaut.",
+ "strainBody": "Die Belastung erfasst, wie viel kardiovaskuläre und körperliche Last du deinem Körper zugemutet hast, von Workouts bis zu einem aktiven Tag auf den Beinen. Es ist ein geräteabgeleiteter Index, gemessen an deinem eigenen üblichen Aufwand, keine klinische Messung, und der Kennwert ist zwischen Geräten nicht vergleichbar.",
+ "stressBody": "Stress ist hier eine Geräte-Heuristik aus Mustern der Herzfrequenzvariabilität. Sie ist keine Cortisol-Messung und nicht, wie du dich bewusst fühlst, und auch kein klinischer Stress-Score. Lies ihn an deinem eigenen Ausgangswert; zwischen Geräten ist er nicht vergleichbar. Ein niedrigerer Wert deutet auf einen ruhigen, gut regulierten Zustand hin; ein höherer ist eine Einladung, Erholung einzuplanen.",
+ "recoveryPageBody": "Die Erholung fasst die nächtlichen Signale deines Wearables zusammen (Recharge, Belastung, Load sowie deine Herzfrequenz- und Energiewerte des Tages) und zeigt, wie gut sich dein Körper erholt hat. Achte eher auf das Muster über mehrere Tage als auf einen einzelnen Wert; ein schwächerer Wert ist eine Einladung, kürzerzutreten, kein Alarm.",
"pulsePressureBody": "Der Pulsdruck ist die Differenz zwischen deinem systolischen und diastolischen Wert (systolisch minus diastolisch). Als allgemeine Orientierung sind etwa 40 mmHg in Ruhe typisch; eine große Differenz über rund 60 mmHg im mittleren Alter kann auf versteifte Arterien hinweisen, eine kleine Differenz unter etwa 25 mmHg kann mit niedrigem Schlagvolumen einhergehen (StatPearls/NIH 2023). Ein Gesprächsanlass, keine Diagnose.",
- "meanArterialPressureBody": "Der mittlere arterielle Druck ist der durchschnittliche Druck, den deine Organe über einen Herzschlag sehen — grob der diastolische Wert plus ein Drittel der Differenz zum systolischen. Als allgemeine Orientierung liegt ein Ruhewert bei etwa 70–100 mmHg (StatPearls/NIH 2023). Die oft genannte Grenze von ~60 mmHg ist ein Konzept der Akutmedizin, kein Alarm für die Selbstmessung.",
- "cycleEducationBody": "Dein Zyklus durchläuft vier hormongesteuerte Phasen, und seine Länge und Regelmäßigkeit sind ein gut zugängliches Fenster auf die allgemeine Gesundheit. Als allgemeine Orientierung dauern Zyklen bei Erwachsenen typischerweise 21–35 Tage (klinische Referenzen nennen 24–38), mit einer Blutung von rund fünf Tagen (ACOG 2015). Die Lutealphase ist der stabile Anker, daher schätzt man den Eisprung am besten durch Rückrechnen von der nächsten erwarteten Periode — nie über einen festen „Tag 14“. Ein vorhergesagtes fruchtbares Fenster ist eine Schätzung, die mehrere erfasste Zyklen braucht und ungefähr bleibt, und die Basaltemperatur bestätigt den Eisprung im Nachhinein, statt ihn vorherzusagen. Dies ist keine Verhütungsmethode.",
+ "meanArterialPressureBody": "Der mittlere arterielle Druck ist der durchschnittliche Druck, den deine Organe über einen Herzschlag sehen, grob der diastolische Wert plus ein Drittel der Differenz zum systolischen. Als allgemeine Orientierung liegt ein Ruhewert bei etwa 70–100 mmHg (StatPearls/NIH 2023). Die oft genannte Grenze von ~60 mmHg ist ein Konzept der Akutmedizin, kein Alarm für die Selbstmessung.",
+ "cycleEducationBody": "Dein Zyklus durchläuft vier hormongesteuerte Phasen, und seine Länge und Regelmäßigkeit sind ein gut zugängliches Fenster auf die allgemeine Gesundheit. Als allgemeine Orientierung dauern Zyklen bei Erwachsenen typischerweise 21–35 Tage (klinische Referenzen nennen 24–38), mit einer Blutung von rund fünf Tagen (ACOG 2015). Die Lutealphase ist der stabile Anker, daher schätzt man den Eisprung am besten durch Rückrechnen von der nächsten erwarteten Periode, nie über einen festen „Tag 14“. Ein vorhergesagtes fruchtbares Fenster ist eine Schätzung, die mehrere erfasste Zyklen braucht und ungefähr bleibt, und die Basaltemperatur bestätigt den Eisprung im Nachhinein, statt ihn vorherzusagen. Dies ist keine Verhütungsmethode.",
"sleepHygieneBody": "Einige evidenzbasierte Gewohnheiten unterstützen den Schlaf: gleichbleibende Zubett- und Aufstehzeiten auch am Wochenende, helles Morgenlicht und gedämpfte Abende, ein kühles, dunkles und ruhiges Zimmer sowie der Verzicht auf Koffein am Nachmittag, späten Alkohol und große späte Mahlzeiten (CDC 2024). Regelmäßigkeit sagt die Gesundheit mindestens so stark voraus wie die Dauer, und die Gesamtschlafzeit eines Wearables ist weit verlässlicher als seine Aufteilung in Tief-, Leicht- und REM-Schlaf."
},
"bpDerived": {
- "pulsePressure": "Der Pulsdruck (die Differenz zwischen deinen Werten) beträgt etwa {value} mmHg — rund 40 ist in Ruhe typisch.",
+ "pulsePressure": "Der Pulsdruck (die Differenz zwischen deinen Werten) beträgt etwa {value} mmHg; rund 40 ist in Ruhe typisch.",
"meanArterialPressure": "Der mittlere arterielle Druck (der Durchschnitt, den deine Organe sehen) liegt bei etwa {value} mmHg, normalerweise um 70–100.",
"caveat": "Diese Werte werden als allgemeine Orientierung aus deiner letzten Messung abgeleitet, nicht als Diagnose."
},
@@ -3311,7 +3468,7 @@
"mean": "Mittelwert"
},
"showAllValues": "Alle Werte anzeigen",
- "valuesPageTitle": "{metric} — alle Werte",
+ "valuesPageTitle": "{metric}: alle Werte",
"valuesPageDescription": "Jeder einzelne Wert hinter dieser Kennzahl. Einträge lassen sich direkt bearbeiten oder entfernen.",
"diversity": {
"trigger": "Messhinweis",
@@ -3333,7 +3490,7 @@
},
"sleep": {
"title": "Schlaf",
- "description": "Phasen-Aufschlüsselung und nächtliche Dauer — gezogen aus Apple Health.",
+ "description": "Phasen-Aufschlüsselung und nächtliche Dauer, gezogen aus Apple Health.",
"mainSleep": "Hauptschlaf",
"totalSleep": "Schlaf gesamt",
"compositionTitle": "Phasen-Verteilung",
@@ -3351,11 +3508,11 @@
"inBed": "Im Bett",
"nap": "Nickerchen",
"napCount": "{count} Nickerchen",
- "unavailable": "Noch keine Phasen-Daten — Schlafphasen erscheinen hier, sobald eine verbundene Quelle sie liefert."
+ "unavailable": "Noch keine Phasen-Daten. Schlafphasen erscheinen hier, sobald eine verbundene Quelle sie liefert."
},
"empty": {
"title": "Noch keine Schlafdaten",
- "description": "Verbinde eine Quelle wie Apple Health oder ein Wearable — oder erfasse Schlaf manuell — und diese Ansicht füllt sich automatisch.",
+ "description": "Verbinde eine Quelle wie Apple Health oder ein Wearable (oder erfasse Schlaf manuell), und diese Ansicht füllt sich automatisch.",
"action": "Geräte verwalten"
},
"debt": {
@@ -3364,21 +3521,21 @@
"caption": "Offene Bilanz der letzten {nights} Nächte",
"clearValue": "Ausgeglichen",
"clearCaption": "Nichts offen über die letzten {nights} Nächte",
- "learning": "Dein Schlaf wird noch erfasst — {counted} Nächte bisher, noch {remaining} für eine verlässliche Bilanz.",
+ "learning": "Dein Schlaf wird noch erfasst: {counted} Nächte bisher, noch {remaining} für eine verlässliche Bilanz.",
"computedInfo": "Das ist HealthLogs eigene Schätzung: eine gleitende Bilanz aus Bedarf minus Schlaf über die letzten Nächte, die Aufholschlaf wieder abbaut. Sie kann vom nativen Schlafdefizit eines Wearables abweichen, das ein eigenes Bedarfsmodell nutzt."
},
"average": {
"title": "Durchschnitt pro Nacht",
"value": "{hours} Std. {minutes} Min.",
"caption": "Mittlerer Schlaf über die letzten {nights} Nächte",
- "learning": "Dein Schlaf wird noch erfasst — {counted} Nächte bisher, noch {remaining} für einen verlässlichen Durchschnitt."
+ "learning": "Dein Schlaf wird noch erfasst: {counted} Nächte bisher, noch {remaining} für einen verlässlichen Durchschnitt."
},
"chronotype": {
"title": "Chronotyp",
- "learning": "Dein Rhythmus wird noch erfasst — {counted} von {total} freien Nächten. Ein Chronotyp braucht ein paar weckerfreie Morgen, um aussagekräftig zu sein.",
+ "learning": "Dein Rhythmus wird noch erfasst: {counted} von {total} freien Nächten. Ein Chronotyp braucht ein paar weckerfreie Morgen, um aussagekräftig zu sein.",
"advancedToggle": "Mehr Details",
"socialJetlagValue": "{hours} Std. {minutes} Min. sozialer Jetlag",
- "socialJetlagCaption": "Der Unterschied zwischen deinem Schlaf an Arbeits- und freien Tagen — wie eine kleine wöchentliche Zeitzonenverschiebung.",
+ "socialJetlagCaption": "Der Unterschied zwischen deinem Schlaf an Arbeits- und freien Tagen, wie eine kleine wöchentliche Zeitzonenverschiebung.",
"msfScValue": "Korrigierte Schlafmitte {clock}",
"msfScCaption": "Bereinigt um den Schlaf, den du am Wochenende nachholst (MSFsc).",
"dayTypeNote": "Freie Tage werden als Wochenenden angenommen, bis eine Tagestyp-Erfassung ergänzt wird.",
@@ -3457,7 +3614,7 @@
"navRecovery": "Erholung",
"workouts": {
"title": "Workouts",
- "description": "Letzte Läufe, Touren, Spaziergänge und Krafteinheiten — quellenübergreifend entdoppelt zwischen Apple Watch und Withings.",
+ "description": "Letzte Läufe, Touren, Spaziergänge und Krafteinheiten, quellenübergreifend entdoppelt zwischen Apple Watch und Withings.",
"emptyState": {
"title": "Noch keine Workouts",
"description": "Workouts kommen per Apple Health vom iPhone und von Withings. Du kannst eins auch von Hand erfassen."
@@ -3567,7 +3724,10 @@
"endInFuture": "Das Workout würde in der Zukunft enden. Stell den Beginn auf die Uhrzeit, zu der es angefangen hat.",
"distanceInvalid": "Gib die Distanz als Zahl in einem realistischen Bereich ein.",
"energyInvalid": "Gib die aktive Energie als Zahl bis 20.000 kcal ein."
- }
+ },
+ "editTitle": "Training bearbeiten",
+ "editAction": "Bearbeiten",
+ "updated": "Training aktualisiert"
}
},
"bloodGlucose": {
@@ -3582,11 +3742,11 @@
"clinical": {
"title": "Glukose-Überblick",
"subtitle": "Ein Blick auf deine letzten {days} Tage, aus {count} Messungen.",
- "spotCaveat": "Das sind Schätzungen aus einzelnen Stichproben-Messungen, kein Profil eines kontinuierlichen Sensors — lies sie als Richtung, nicht als klinisches Urteil.",
+ "spotCaveat": "Das sind Schätzungen aus einzelnen Stichproben-Messungen, kein Profil eines kontinuierlichen Sensors. Lies sie als Richtung, nicht als klinisches Urteil.",
"labsLink": "Laborergebnisse? HbA1c und Nüchternglukose aus einer Blutabnahme trägst du unter Labor ein.",
"learningTitle": "Lerne deine Glukose noch kennen",
- "learningBody": "Bisher sind das {count} über {days}. Mit etwas mehr Verlauf wird das Bild klarer — mess weiter oder lass es ein verbundenes Gerät ergänzen.",
- "learningBodyEmpty": "Sobald ein paar Messungen vorliegen — von dir erfasst oder von einem Gerät synchronisiert — nehmen Zeit-im-Zielbereich, Durchschnitt und Schwankung hier Gestalt an.",
+ "learningBody": "Bisher sind das {count} über {days}. Mit etwas mehr Verlauf wird das Bild klarer. Mess weiter oder lass es ein verbundenes Gerät ergänzen.",
+ "learningBodyEmpty": "Sobald ein paar Messungen vorliegen (von dir erfasst oder von einem Gerät synchronisiert), nehmen Zeit-im-Zielbereich, Durchschnitt und Schwankung hier Gestalt an.",
"readingsCount": "{count} Messungen",
"readingsCountOne": "1 Messung",
"daysCount": "{days} Tage",
@@ -3624,7 +3784,7 @@
"intro": "Forschungsnahe Kennzahlen für den genaueren Blick. Jede fasst deine Messungen zu einer risikogewichteten Zahl zusammen.",
"jIndex": {
"label": "J-Index",
- "help": "Ein einzelner Wert aus Durchschnitt und Streuung — niedriger steht für engere Kontrolle."
+ "help": "Ein einzelner Wert aus Durchschnitt und Streuung; niedriger steht für engere Kontrolle."
},
"lbgi": {
"label": "Risiko für Tiefwerte (LBGI)",
@@ -3741,7 +3901,7 @@
"navSkinTemperature": "Hauttemperatur",
"pulseWaveVelocity": {
"title": "Pulswellengeschwindigkeit",
- "description": "Deine arterielle Pulswellengeschwindigkeit im Zeitverlauf, in m/s — höhere Werte deuten auf steifere Arterien hin.",
+ "description": "Deine arterielle Pulswellengeschwindigkeit im Zeitverlauf, in m/s. Höhere Werte deuten auf steifere Arterien hin.",
"chartTitle": "Pulswellengeschwindigkeit",
"emptyState": {
"title": "Noch keine Daten",
@@ -3891,7 +4051,6 @@
"navWalkingSpeed": "Gehgeschwindigkeit",
"mood": {
"logLink": "Stimmung eintragen",
- "mentalWellbeingLink": "Check-in zum seelischen Wohlbefinden machen",
"heatmapTitle": "Stimmungskalender",
"heatmapNoEntry": "Kein Eintrag",
"legendGreat": "Super",
@@ -3902,7 +4061,7 @@
"distributionDaysUnit": "Tage",
"weekdayTitle": "Durchschnitt nach Wochentag",
"weekdayNoData": "Keine Daten",
- "weekdayBestWorst": "Bester Tag: {best} · Schwerster Tag: {worst}",
+ "weekdayBestWorst": "Bester Tag: {best}, schwerster Tag: {worst}",
"tagsTitle": "Tags und Auslöser",
"tagAvgMood": "Durchschnittliche Stimmung mit diesem Tag",
"correlationsTitle": "Stimmung und andere Werte",
@@ -3918,7 +4077,7 @@
"strengthModerate": "Moderat",
"strengthWeak": "Schwach",
"strengthNone": "Keine",
- "source": "{n} gepaarte Tage · r = {r}",
+ "source": "{n} gepaarte Tage, r = {r}",
"emptyTitle": "Noch nicht genug gepaarte Daten",
"emptyDescription": "Erfasse Stimmung und diesen Wert an denselben Tagen, um einen Zusammenhang zu sehen.",
"pendingDescription": "{n} von {needed} gepaarten Tagen. Ein Zusammenhang erscheint, sobald genug Tage da sind, um Muster von Zufall zu unterscheiden.",
@@ -3935,7 +4094,7 @@
"inTargetCaption": "der letzten Tage im guten Stimmungsbereich",
"narrative": {
"title": "Das fällt auf",
- "weekdayDip": "Deine Stimmung sinkt am stärksten am {weekday} — {delta} unter deinem Wochenschnitt.",
+ "weekdayDip": "Deine Stimmung sinkt am stärksten am {weekday}, {delta} unter deinem Wochenschnitt.",
"weekdayPeak": "{weekday} ist durchgehend dein bester Tag mit einem Schnitt von {value}.",
"trendUp": "Deine Stimmung steigt über die letzten {days} Tage (+{delta} gegenüber dem Vorzeitraum).",
"trendDown": "Deine Stimmung sinkt über die letzten {days} Tage (-{delta} gegenüber dem Vorzeitraum).",
@@ -3944,7 +4103,7 @@
"tagLift": "Tage mit „{tag}“ liegen im Schnitt {delta} über deiner allgemeinen Stimmung.",
"tagDrop": "Tage mit „{tag}“ liegen {delta} unter deinem Durchschnitt.",
"inTarget": "Du warst an {pct} % der letzten 30 Tage in deinem guten Stimmungsbereich.",
- "streak": "Du hast deine Stimmung {days} Tage in Folge erfasst — weiter so.",
+ "streak": "Du hast deine Stimmung {days} Tage in Folge erfasst. Weiter so.",
"timeOfDay": "Am {bucket} fühlst du dich am besten, im Schnitt {value}."
},
"timeOfDay": {
@@ -3977,7 +4136,7 @@
"crosstab": {
"title": "Tags und deine Gesundheitswerte",
"description": "Wie sich ein Gesundheitswert an Tagen mit einem Tag von Tagen ohne diesen Tag unterscheidet.",
- "pairLabel": "{tag} · {metric}",
+ "pairLabel": "{metric} an Tagen mit {tag}",
"metricActiveEnergy": "Aktive Energie",
"metricSleepDuration": "Schlaf",
"metricNextDayRecovery": "Erholung am Folgetag",
@@ -3994,7 +4153,7 @@
"factorCrosstab": {
"title": "Deine bewerteten Faktoren und deine Vitalwerte",
"description": "Wie sich ein Vitalwert an den Tagen unterscheidet, an denen du einen Faktor niedrig bewertet hast, verglichen mit den Tagen mit hoher Bewertung.",
- "pairLabel": "{factor} · {metric}",
+ "pairLabel": "{metric} an Tagen mit {factor}",
"metricSleepDuration": "Schlaf",
"metricSteps": "Schritte",
"metricRestingHeartRate": "Ruhepuls",
@@ -4015,12 +4174,12 @@
"title": "Was auffällt",
"subheading": "Entdeckte Zusammenhänge",
"footer": "Herausgefiltert aus {tested} Tag-zu-Tag-Paaren, die über all deine Werte getestet wurden, nach der Korrektur für Fehlentdeckungen.",
- "disclaimer": "Statistisch gefiltert, aber weiterhin beobachtend — ein Muster, das einen Blick wert ist, keine Ursache.",
+ "disclaimer": "Statistisch gefiltert, aber weiterhin beobachtend: ein Muster, das einen Blick wert ist, keine Ursache.",
"moodUp": "Höheres {factor} geht in deinen Daten tendenziell mit besserer Stimmung am Folgetag einher.",
"moodDown": "Höheres {factor} geht in deinen Daten tendenziell mit schlechterer Stimmung am Folgetag einher.",
"outcomeUp": "Bessere Stimmung geht in deinen Daten tendenziell mit höherem {factor} am Folgetag einher.",
"outcomeDown": "Bessere Stimmung geht in deinen Daten tendenziell mit niedrigerem {factor} am Folgetag einher.",
- "stat": "{n} gepaarte Tage · r = {r} · q = {q}",
+ "stat": "{n} gepaarte Tage, r = {r}, q = {q}",
"statLabel": "Wie das berechnet wurde",
"explainerLabel": "Wie das berechnet wurde"
},
@@ -4054,8 +4213,8 @@
"deviationAbove": "Deine eigene Einschätzung liegt {amount} Punkte über diesem Bereich.",
"deviationBelow": "Deine eigene Einschätzung liegt {amount} Punkte unter diesem Bereich.",
"weightedTitle": "Was an diesem Tag am stärksten gewichtet wurde",
- "weightedUp": "{label} — zählte in Richtung eines höheren erwarteten Werts",
- "weightedDown": "{label} — zählte in Richtung eines niedrigeren erwarteten Werts",
+ "weightedUp": "{label}: zählte in Richtung eines höheren erwarteten Werts",
+ "weightedDown": "{label}: zählte in Richtung eines niedrigeren erwarteten Werts",
"caveat": "Das ist ein Vergleich mit deinen eigenen bisherigen Tagen, keine Aussage darüber, wie ein Tag zustande gekommen ist.",
"learning": "Lernphase: {entries} von {threshold} Tagen erfasst. Ein Vergleich erscheint, sobald genug davon zusammengekommen sind.",
"noPattern": "In den bisher erfassten {entries} Tagen ist noch nichts gleichmäßig genug, um einen Tag damit zu vergleichen. Wenn du auch den Kontext eines Tages festhältst, wird mehr davon vergleichbar.",
@@ -4064,15 +4223,15 @@
},
"derived": {
"coverage": {
- "summary": "{present} von {required} Eingaben · {percent}%",
+ "summary": "{present} von {required} Eingaben ({percent}%)",
"ratioLabel": "{present}/{required}",
"historyDays": "Basiert auf {count} Tagen Verlauf",
"missing": "Erfasse diese, um den Wert zu schärfen: {list}",
- "summaryLabelled": "{label} · {percent}%"
+ "summaryLabelled": "{label} ({percent}%)"
},
"scoreRing": {
"aria": "Wert {score} von 100, {band}",
- "ariaProvisional": "Wert noch nicht verfügbar — zu wenig Daten",
+ "ariaProvisional": "Wert noch nicht verfügbar, zu wenig Daten",
"provisionalCaption": "Noch zu wenig Daten",
"band": {
"green": "gut",
@@ -4086,7 +4245,7 @@
"inputsLabel": "Eingaben",
"noInputs": "Noch keine Eingaben für diesen Wert vorhanden.",
"methodLabel": "Methode",
- "window": "Aus deinen letzten {days} Tagen · {source}-Daten",
+ "window": "Aus deinen letzten {days} Tagen mit {source}-Daten",
"asOf": "Stand {date}",
"standardLink": "Standard: {name}"
},
@@ -4096,7 +4255,7 @@
"loadingLabel": "Vitalwerte werden geladen",
"loadError": "Deine Vitalwerte konnten gerade nicht geladen werden.",
"infoLabel": "Wie wird das berechnet?",
- "building": "Typischer Bereich entsteht — {count} von {target} Tagen",
+ "building": "Typischer Bereich entsteht: {count} von {target} Tagen",
"typicalRange": "Typisch für dich: {low}–{high}",
"fitnessYounger": "Ausdauer wie ~{years} J. jünger",
"fitnessOlder": "Ausdauer ~{years} J. über deinem Alter",
@@ -4128,9 +4287,8 @@
"anatomy": {
"outOf": "/100",
"contributorsLabel": "Faktoren",
- "contributorValue": "{value} · {weight}% Gewicht",
+ "contributorValue": "{value}, Gewichtung {weight}%",
"contributorMissing": "keine Daten",
- "disclaimer": "Richtwert — keine klinische Bewertung.",
"insufficient": "Noch nicht genug Daten für eine Bewertung. Tracke weiter, dann erscheint sie hier."
},
"composite": {
@@ -4149,77 +4307,75 @@
"READINESS": {
"title": "Bereitschaft",
"description": "Ein täglicher Wohlbefindens-Indikator daraus, wie deine Werte zu deiner eigenen Baseline stehen.",
- "method": "Eine 0–100-Mischung aus Ruhepuls-Abweichung, HRV-(SDNN-)Abweichung, Schlaf, Atemfrequenz-Stabilität und Stimmungsstabilität — jeweils gegen deine persönliche Baseline gemessen. Gewichte werden über die von deinen Daten gestützten Komponenten neu verteilt. Ein Wohlbefindens-Indikator, kein Erholungs- oder klinischer Score.",
+ "method": "Eine 0–100-Mischung aus Ruhepuls-Abweichung, HRV-(SDNN-)Abweichung, Schlaf, Atemfrequenz-Stabilität und Stimmungsstabilität, jeweils gegen deine persönliche Baseline gemessen. Gewichte werden über die von deinen Daten gestützten Komponenten neu verteilt. Ein Wohlbefindens-Indikator, kein Erholungs- oder klinischer Score.",
"component": {
"rhr": "Ruhepuls",
"hrv": "HRV (SDNN)",
"sleep": "Schlaf",
"respiratory": "Atemfrequenz",
"mood": "Stimmungsstabilität"
- },
- "caveat": "Ein Wellness-Proxy, kein Erholungs- oder klinischer Wert."
+ }
},
"VITALS_BASELINE": {
- "method": "Dein persönlicher Normalbereich ist der Median deiner Tageswerte über den Zeitraum, ± eine robuste Streuung (skalierte mittlere absolute Abweichung). Er folgt DEINEN Werten, keiner Bevölkerungsnorm — ein Wert im Band ist also typisch für dich."
+ "method": "Dein persönlicher Normalbereich ist der Median deiner Tageswerte über den Zeitraum, ± eine robuste Streuung (skalierte mittlere absolute Abweichung). Er folgt DEINEN Werten, keiner Bevölkerungsnorm. Ein Wert im Band ist also typisch für dich."
},
"HRV_BALANCE": {
"method": "Dein jüngster HRV-(SDNN-)Durchschnitt im Vergleich zu deiner eigenen gleitenden Baseline (Median ± robuste Streuung). Ausgeglichen heißt: er liegt in deinem persönlichen Bereich; das Band stammt aus deinen Daten, nie aus einem Bevölkerungswert."
},
"BMI": {
- "method": "Gewicht ÷ Größe², eingeordnet nach den WHO-Kategorien für Erwachsene (Untergewicht / Normal / Übergewicht / Adipositas). Nur ein Screening-Verhältnis — es unterscheidet nicht Muskel von Fett."
+ "method": "Gewicht ÷ Größe², eingeordnet nach den WHO-Kategorien für Erwachsene (Untergewicht / Normal / Übergewicht / Adipositas). Nur ein Screening-Verhältnis, es unterscheidet nicht Muskel von Fett."
},
"FITNESS_AGE": {
- "method": "Dein VO₂max im Vergleich zu alters- und geschlechtsbezogenen Perzentilen der kardiorespiratorischen Fitness, als Fitnessalter-Differenz. Das VO₂max ist die Schätzung deines Geräts — hier nie neu berechnet."
+ "method": "Dein VO₂max im Vergleich zu alters- und geschlechtsbezogenen Perzentilen der kardiorespiratorischen Fitness, als Fitnessalter-Differenz. Das VO₂max ist die Schätzung deines Geräts, hier nie neu berechnet."
},
"VASCULAR_AGE_DELTA": {
"method": "Die Gefäßalter- bzw. Pulswellengeschwindigkeits-Schätzung deines Geräts im Vergleich zu deinem chronologischen Alter, als Differenz in Jahren. Die Schätzung stammt vom Gerät; HealthLog ordnet sie nur ein.",
- "caveat": "Die eigene Schätzung deines Geräts, nur eingeordnet — beschreibend, keine klinische Gefäßbewertung.",
+ "caveat": "Die eigene Schätzung deines Geräts, nur eingeordnet: beschreibend, keine klinische Gefäßbewertung.",
"description": "Wie die Gefäßalter-Schätzung deines Geräts zu deinem chronologischen Alter steht."
},
"COINCIDENT_DEVIATION": {
"method": "Ein Hinweis, wenn zwei oder mehr deiner Vitalwerte am selben Tag außerhalb ihres persönlichen Normalbereichs liegen. Jedes Band ist der robuste Median ± k·MAD deiner eigenen Verlaufsdaten und passt sich an dich an, nicht an einen Bevölkerungswert.",
- "caveat": "Ein Hinweis aus deinen eigenen Baselines — beschreibend, keine Diagnose und kein klinischer Alarm."
+ "caveat": "Ein Hinweis aus deinen eigenen Baselines: beschreibend, keine Diagnose und kein klinischer Alarm."
},
"RECOVERY_SCORE": {
- "method": "Ein nächtlicher 0–100-Erholungs-Proxy aus HRV, Ruhepuls und Schlaf gegenüber deiner Baseline. Höher heißt besser erholt. Einmal pro Nacht berechnet und hier nur ausgelesen — nie spontan neu berechnet.",
- "caveat": "Ein beschreibender Wellness-Proxy, kein trainings- oder klinikgerechtes Erholungsmaß.",
+ "method": "Ein nächtlicher 0–100-Erholungs-Proxy aus HRV, Ruhepuls und Schlaf gegenüber deiner Baseline. Höher heißt besser erholt. Einmal pro Nacht berechnet und hier nur ausgelesen, nie spontan neu berechnet.",
"description": "Ein nächtlicher Erholungs-Proxy aus HRV, Ruhepuls und Schlaf."
},
"STRESS_SCORE": {
"method": "Ein nächtlicher 0–100-Stress-Proxy aus dem Verlauf deiner HRV über den Tag gegenüber deiner Baseline. Höher heißt mehr physiologische Belastung.",
- "caveat": "Ein aus der HRV abgeleiteter Proxy, KEINE EDA-/Hautleitwert-Stressmessung — deine Uhr hat keinen EDA-Sensor. Beschreibend, nicht klinisch.",
"description": "Ein nächtlicher Stress-Proxy aus dem Tagesverlauf deiner HRV."
},
"STRAIN_SCORE": {
"method": "Ein nächtlicher 0–100-Belastungs-Proxy aus deiner herzfrequenzgewichteten Aktivitätslast (ein TRIMP-ähnliches Trainingsimpuls-Modell). Höher heißt ein anstrengenderer Tag.",
- "anchorPersonal": "An deine eigene jüngste Trainingstagslast angelehnt — bewertet relativ zu deiner typischen Anstrengung, sodass ein für dich wirklich harter Tag hoch ausfällt.",
+ "anchorPersonal": "An deine eigene jüngste Trainingstagslast angelehnt und bewertet relativ zu deiner typischen Anstrengung, sodass ein für dich wirklich harter Tag hoch ausfällt.",
"anchorPopulation": "Gegen einen allgemeinen Referenzwert bewertet, während du noch Trainingsverlauf aufbaust; sobald genug vorhanden ist, wechselt die Bewertung auf deine eigene typische Anstrengung.",
- "caveat": "Ein beschreibender Aktivitätslast-Proxy, kein klinisches oder trainingsgerechtes Lastmaß.",
- "description": "Ein nächtlicher Aktivitätslast-Proxy aus deiner herzfrequenzgewichteten Anstrengung, relativ zu deiner typischen Anstrengung."
+ "description": "Ein nächtlicher Aktivitätslast-Proxy aus deiner herzfrequenzgewichteten Anstrengung, relativ zu deiner typischen Anstrengung.",
+ "deviceCaption": "Tagesbelastung deines Geräts: {value} von {max}",
+ "deviceSource": "Bis HealthLog nachts einen eigenen Wert berechnet hat, zeigt die Karte die Tagesbelastung deines Geräts, umgerechnet auf 0–100."
},
"WRIST_TEMPERATURE_BASELINE": {
- "method": "Deine typische nächtliche Handgelenkstemperatur ist der Median deiner nächtlichen Messwerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Sie folgt DEINEN Werten — letzte Nacht wird als Abweichung von deiner eigenen Baseline gezeigt, nicht von einer Bevölkerungsnorm.",
- "caveat": "Nur ein persönliches Abweichungsband — beschreibend, kein Anzeichen für Krankheit, Fieber oder Zyklusphase. Du interpretierst es selbst."
+ "method": "Deine typische nächtliche Handgelenkstemperatur ist der Median deiner nächtlichen Messwerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Sie folgt DEINEN Werten: Letzte Nacht wird als Abweichung von deiner eigenen Baseline gezeigt, nicht von einer Bevölkerungsnorm.",
+ "caveat": "Nur ein persönliches Abweichungsband: beschreibend, kein Anzeichen für Krankheit, Fieber oder Zyklusphase. Du interpretierst es selbst."
},
"STAIR_ASCENT_SPEED_BASELINE": {
- "method": "Dein typisches Treppensteig-Tempo ist der Median deiner Tageswerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Das Band wird aus deinen Daten gebildet — es gibt keinen Bevölkerungs-Grenzwert für Treppentempo, da es von Treppengeometrie und Beinlänge abhängt.",
- "caveat": "Ein persönliches Mobilitäts-Trendband — beschreibend, keine Gebrechlichkeits- oder Sturzrisiko-Bewertung."
+ "method": "Dein typisches Treppensteig-Tempo ist der Median deiner Tageswerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Das Band wird aus deinen Daten gebildet. Es gibt keinen Bevölkerungs-Grenzwert für Treppentempo, da es von Treppengeometrie und Beinlänge abhängt.",
+ "caveat": "Ein persönliches Mobilitäts-Trendband: beschreibend, keine Gebrechlichkeits- oder Sturzrisiko-Bewertung."
},
"STAIR_DESCENT_SPEED_BASELINE": {
- "method": "Dein typisches Treppen-Abstiegstempo ist der Median deiner Tageswerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Das Band wird aus deinen Daten gebildet — es gibt keinen Bevölkerungs-Grenzwert für Treppentempo, da es von Treppengeometrie und Beinlänge abhängt.",
- "caveat": "Ein persönliches Mobilitäts-Trendband — beschreibend, keine Gebrechlichkeits- oder Sturzrisiko-Bewertung."
+ "method": "Dein typisches Treppen-Abstiegstempo ist der Median deiner Tageswerte im Zeitraum, ± einer robusten Streuung (skalierte mittlere absolute Abweichung). Das Band wird aus deinen Daten gebildet. Es gibt keinen Bevölkerungs-Grenzwert für Treppentempo, da es von Treppengeometrie und Beinlänge abhängt.",
+ "caveat": "Ein persönliches Mobilitäts-Trendband: beschreibend, keine Gebrechlichkeits- oder Sturzrisiko-Bewertung."
},
"SIX_MINUTE_WALK_BAND": {
- "method": "Die von deinem Gerät GESCHÄTZTE 6-Minuten-Gehstrecke, eingeordnet gegen die Referenz von Enright & Sherrill für Alter, Größe, Gewicht und Geschlecht, als Prozent des Vorhersagewerts. Die Strecke ist die Schätzung des Geräts — hier nie neu berechnet. Ohne Alter, Größe, Gewicht und Geschlecht wird der Prozentwert ausgeblendet und nur Strecke und Trend gezeigt.",
- "caveat": "Eine gegen eine veröffentlichte Referenz neu eingeordnete Schätzung — ein Hinweis zur funktionellen Kapazität, kein klinischer 6-Minuten-Gehtest und keine Diagnose. Die Referenz wurde für Alter 40–80 erstellt; außerhalb dieses Bereichs ist der Vorhersagewert eine Extrapolation."
+ "method": "Die von deinem Gerät GESCHÄTZTE 6-Minuten-Gehstrecke, eingeordnet gegen die Referenz von Enright & Sherrill für Alter, Größe, Gewicht und Geschlecht, als Prozent des Vorhersagewerts. Die Strecke ist die Schätzung des Geräts, hier nie neu berechnet. Ohne Alter, Größe, Gewicht und Geschlecht wird der Prozentwert ausgeblendet und nur Strecke und Trend gezeigt.",
+ "caveat": "Eine gegen eine veröffentlichte Referenz neu eingeordnete Schätzung: ein Hinweis zur funktionellen Kapazität, kein klinischer 6-Minuten-Gehtest und keine Diagnose. Die Referenz wurde für Alter 40–80 erstellt; außerhalb dieses Bereichs ist der Vorhersagewert eine Extrapolation."
},
"TRAJECTORY": {
- "method": "Eine Ausgleichsgerade (kleinste Quadrate), an deine jüngsten Tageswerte angepasst und über einen kurzen Zeitraum fortgeschrieben — mit dem klassischen Vorhersageintervall, das umso breiter wird, je weiter es reicht. Wird nur berechnet, wenn der jüngste Trend deutlich genug und deine Datenreihe lang und aktuell genug ist; sonst wird keine Linie gezeichnet.",
- "caveat": "Eine Fortschreibung deines eigenen jüngsten Trends — keine Vorhersage, kein Ziel und keine klinische Prognose. Sie gilt nur, solange das jüngste Muster anhält; das schattierte Band zeigt, wie schnell diese Sicherheit nachlässt."
+ "method": "Eine Ausgleichsgerade (kleinste Quadrate), an deine jüngsten Tageswerte angepasst und über einen kurzen Zeitraum fortgeschrieben, mit dem klassischen Vorhersageintervall, das umso breiter wird, je weiter es reicht. Wird nur berechnet, wenn der jüngste Trend deutlich genug und deine Datenreihe lang und aktuell genug ist; sonst wird keine Linie gezeichnet.",
+ "caveat": "Eine Fortschreibung deines eigenen jüngsten Trends, keine Vorhersage, kein Ziel und keine klinische Prognose. Sie gilt nur, solange das jüngste Muster anhält; das schattierte Band zeigt, wie schnell diese Sicherheit nachlässt."
},
"SAME_TIME_BASELINE": {
"method": "Dein heutiger laufender Gesamtwert im Vergleich zum Median derselben Ortszeit-Stunde aus deinen letzten vier Wochen, mit einem robusten Median-±-MAD-Band darum. Nur Tage mit ausreichend aufgezeichneten Details zählen, und es wird kein Vergleich gezeigt, bevor mindestens vierzehn davon vorliegen.",
- "caveat": "Ein Vergleich mit deiner eigenen jüngeren Vergangenheit, kein Ziel. Er sagt, wo der heutige Tag zu dieser Stunde gegenüber deinem üblichen Muster steht — und nichts darüber, ob dieses Muster für dich das richtige ist."
+ "caveat": "Ein Vergleich mit deiner eigenen jüngeren Vergangenheit, kein Ziel. Er sagt, wo der heutige Tag zu dieser Stunde gegenüber deinem üblichen Muster steht, und nichts darüber, ob dieses Muster für dich das richtige ist."
}
},
"scores": {
@@ -4230,7 +4386,9 @@
"recovery": "Erholungs-Score",
"recoveryCrossLink": "Erholungs- und Belastungsdaten deines Wearables ansehen",
"stress": "Stress",
- "strain": "Belastung"
+ "strain": "Belastung",
+ "historyTitle": "Verlauf",
+ "historyWindow": "Letzte {days} Tage"
},
"coincident": {
"label": "Mehrere Vitalwerte außerhalb der Baseline",
@@ -4240,31 +4398,31 @@
"allClear": "Alle deine Vitalwerte liegen heute in ihrem persönlichen Bereich.",
"allClearMeta": "{count} deiner Vitalwerte gegen deinen eigenen typischen Bereich geprüft.",
"watch": "Ein Vitalwert liegt heute außerhalb seines üblichen Bereichs.",
- "watchVital": "Außerhalb des Bereichs: {vital}. Oft nur einmalig — behalte es in den nächsten Tagen im Blick.",
+ "watchVital": "Außerhalb des Bereichs: {vital}. Oft nur einmalig. Behalte es in den nächsten Tagen im Blick.",
"firedHeadline": "{count} deiner Vitalwerte liegen heute außerhalb ihres persönlichen Bereichs.",
- "factors": "Mögliche Faktoren — nie eine Ursache: ein hartes Training, schlechter Schlaf, Alkohol, Höhe, Stress oder Krankheit. Das ist ein Hinweis aus deinen eigenen Baselines, keine Diagnose.",
- "building": "Deine persönlichen Baselines entstehen — erfasse ein paar Tage mehr, dann erscheint dies.",
+ "factors": "Mögliche Faktoren (nie eine Ursache): ein hartes Training, schlechter Schlaf, Alkohol, Höhe, Stress oder Krankheit. Das ist ein Hinweis aus deinen eigenen Baselines, keine Diagnose.",
+ "building": "Deine persönlichen Baselines entstehen. Erfasse ein paar Tage mehr, dann erscheint dies.",
"illnessExplained": "Diese Abweichungen hängen wahrscheinlich mit der erfassten Erkrankung zusammen."
},
"trajectory": {
"cardTitle": "Kurzfrist-Projektion",
- "headline": "Wenn dieses Muster anhält — die nächsten {days} Tage",
+ "headline": "Wenn dieses Muster anhält: die nächsten {days} Tage",
"range": "Projiziert auf rund {low}–{high} {unit} in {days} Tagen.",
"caveat": "Eine Fortschreibung deines eigenen jüngsten Trends, keine Vorhersage. Das schattierte Band wird breiter und zeigt, wie schnell die Unsicherheit wächst.",
- "insufficient": "Noch kein klar genuger Trend für eine Projektion — erfasse weiter, dann erscheint eine Projektion, sobald das jüngste Muster deutlich genug ist.",
+ "insufficient": "Noch kein klar genuger Trend für eine Projektion. Erfasse weiter, dann erscheint eine Projektion, sobald das jüngste Muster deutlich genug ist.",
"daysNeeded": "Noch etwa {count} Tage Erfassung nötig, bis eine Projektion erscheinen kann."
},
"sameTime": {
"sectionTitle": "Heute bisher",
"sectionSubtitle": "Deine laufenden Summen im Vergleich zu dem, was zu dieser Stunde für dich üblich ist",
- "learning": "Lernt deinen üblichen Tag — {count} von {target} Tagen",
+ "learning": "Lernt deinen üblichen Tag: {count} von {target} Tagen",
"framing": "Üblich bis {time}: {typical} {unit}",
"comparisonLabel": "Dein üblicher Tag"
}
},
"cardioFitness": {
"title": "Fitness",
- "description": "Deine VO₂max im Zeitverlauf — das wichtigste Maß für die Ausdauerfitness.",
+ "description": "Deine VO₂max im Zeitverlauf, das wichtigste Maß für die Ausdauerfitness.",
"chartTitle": "VO₂max (mL/(kg·min))",
"emptyState": {
"title": "Noch keine Fitnessdaten",
@@ -4344,7 +4502,7 @@
"navBreathingDisturbances": "Atmung",
"rhythmEvents": {
"sectionTitle": "Gesundheitshinweise des Geräts",
- "sectionIntro": "Hinweise, die deine Uhr eigenständig gesendet hat. HealthLog zeigt diese nur an — es erkennt, bestätigt oder interpretiert sie nicht.",
+ "sectionIntro": "Hinweise, die deine Uhr eigenständig gesendet hat. HealthLog zeigt diese nur an. Es erkennt, bestätigt oder interpretiert sie nicht.",
"loadError": "Die Gerätehinweise konnten nicht geladen werden.",
"disclaimer": "Dies sind Hinweise, die dein Gerät mit seiner eigenen integrierten Erkennung erzeugt hat. Sie werden hier nur zu deiner Information angezeigt. Dies ist keine medizinische Bewertung durch HealthLog, und HealthLog stellt keine Diagnose. Wenn dich ein Hinweis beunruhigt, sprich mit deiner Ärztin oder deinem Arzt.",
"event": {
@@ -4389,13 +4547,13 @@
"detail": "Ausgeblendete Detailseiten verschwinden auch aus der oberen Navigation."
},
"customize": "Insights anpassen",
- "regenerateRateLimited": "Aktualisierungslimit erreicht — bitte später erneut versuchen.",
- "regenerateUnavailable": "Keine neue Analyse erhalten — bitte später erneut versuchen.",
- "regenerateError": "Aktualisierung läuft noch — bitte gleich erneut versuchen.",
+ "regenerateRateLimited": "Aktualisierungslimit erreicht. Bitte später erneut versuchen.",
+ "regenerateUnavailable": "Keine neue Analyse erhalten. Bitte später erneut versuchen.",
+ "regenerateError": "Aktualisierung läuft noch. Bitte gleich erneut versuchen.",
"regenerateSettling": "Die Einschätzung wird noch erstellt",
"regenerateSettleFailed": "Die Aktualisierung konnte nicht abgeschlossen werden. Bitte später erneut versuchen.",
"deviceScore": {
- "learning": "Deine Basislinie wird noch gelernt — ein paar weitere Nächte vervollständigen den Verlauf.",
+ "learning": "Deine Basislinie wird noch gelernt. Ein paar weitere Nächte vervollständigen den Verlauf.",
"average": "30-Tage-Durchschnitt {value}",
"unitScore": "Punkte",
"unitMinutes": "Min.",
@@ -4408,14 +4566,14 @@
},
"recovery": {
"title": "Erholung",
- "description": "Wie gut sich dein Körper erholt hat — die Erholungs-, Belastungs- und Load-Signale, die dein Wearable über Nacht aufzeichnet.",
+ "description": "Wie gut sich dein Körper erholt hat: die Erholungs-, Belastungs- und Load-Signale, die dein Wearable über Nacht aufzeichnet.",
"scoreCrossLink": "Den zusammengesetzten Erholungs-Score ansehen",
"emptyTitle": "Noch keine Erholungsdaten",
"empty": "Noch keine Erholungsdaten. Verbinde ein WHOOP-, Polar- oder Oura-Konto, dann füllen sich diese Werte, sobald dein Gerät synchronisiert.",
"block": {
- "ansChargeExplainer": "Wie aufgeladen dein autonomes Nervensystem in den Tag startet — ein Recharge-Wert aus deinen nächtlichen Herz- und Atemsignalen.",
+ "ansChargeExplainer": "Wie aufgeladen dein autonomes Nervensystem in den Tag startet, ein Recharge-Wert aus deinen nächtlichen Herz- und Atemsignalen.",
"dayStrainExplainer": "Die kardiovaskuläre Tagesbelastung auf der Belastungsskala deines Geräts. Lies sie im Verhältnis zu deiner Erholung, nicht als Ziel für sich.",
- "workoutStrainExplainer": "Wie fordernd ein einzelnes Training auf derselben Belastungsskala war — die kardiovaskuläre Last dieser Einheit.",
+ "workoutStrainExplainer": "Wie fordernd ein einzelnes Training auf derselben Belastungsskala war: die kardiovaskuläre Last dieser Einheit.",
"cardioLoadExplainer": "Ein Trainingslast-Näherungswert, der die akut aufgebaute kardiovaskuläre Belastung abbildet. Vergleiche ihn mit deinen eigenen letzten Tagen.",
"averageHeartRateExplainer": "Deine durchschnittliche Herzfrequenz über den ganzen Tag bzw. Zyklus, wie dein Wearable sie meldet.",
"maxHeartRateExplainer": "Deine höchste Herzfrequenz über den ganzen Tag bzw. Zyklus, wie dein Wearable sie meldet.",
@@ -4427,8 +4585,8 @@
"subtitle": "Nächtliche Abweichung von deiner Basislinie"
},
"resilience": {
- "explainer": "Wie gut dein Körper die tägliche Belastung mit der Erholung ausbalanciert — aus deinen nächtlichen Signalen der letzten Tage abgelesen.",
- "learning": "Deine Resilienz wird noch erlernt — ein paar Tage mehr festigen das Niveau.",
+ "explainer": "Wie gut dein Körper die tägliche Belastung mit der Erholung ausbalanciert, aus deinen nächtlichen Signalen der letzten Tage abgelesen.",
+ "learning": "Deine Resilienz wird noch erlernt. Ein paar Tage mehr festigen das Niveau.",
"band": {
"limited": "Eingeschränkt",
"adequate": "Ausreichend",
@@ -4459,6 +4617,7 @@
"shiftUp": "{vital} hat sich gegenüber deiner jüngsten Norm nach oben verschoben.",
"shiftDown": "{vital} hat sich gegenüber deiner jüngsten Norm nach unten verschoben.",
"rangeContext": "Aktuell {value}, sonst {low}–{high}.",
+ "rangeContextTimeOfDay": "Aktuell {value}, zu dieser Tageszeit sonst {low}–{high}.",
"shiftContext": "Etwa {before} davor, {after} seitdem."
},
"breathingScreening": {
@@ -4472,7 +4631,7 @@
"nights": "{count} Nächte erfasst",
"indexLine": "jüngster Durchschnittsindex {value}",
"eventsFlagged": "{count} vom Gerät gemeldete Ereignisse",
- "disclaimer": "Dies ist ein Screening-Signal, keine Diagnose — bitte mit einer Ärztin oder einem Arzt besprechen."
+ "disclaimer": "Dies ist ein Screening-Signal, keine Diagnose. Bitte mit einer Ärztin oder einem Arzt besprechen."
},
"labsChanges": {
"sectionTitle": "Seit deinem letzten Laborbefund",
@@ -4488,14 +4647,14 @@
"navWaistToHeight": "Taille/Größe",
"gripStrength": {
"title": "Griffkraft",
- "description": "Maximale Handgriffkraft — ein robuster Stellvertreter für die Ganzkörperkraft. Unter dem geschlechtsspezifischen Grenzwert (Männer 27 kg, Frauen 16 kg) deutet sie auf eine mögliche Sarkopenie hin; dein eigener Verlauf führt die Einschätzung.",
+ "description": "Maximale Handgriffkraft, ein robuster Stellvertreter für die Ganzkörperkraft. Unter dem geschlechtsspezifischen Grenzwert (Männer 27 kg, Frauen 16 kg) deutet sie auf eine mögliche Sarkopenie hin; dein eigener Verlauf führt die Einschätzung.",
"chartTitle": "Griffkraft",
"emptyState": {
"title": "Noch keine Griffkraft",
- "description": "Erfasse einen Wert mit einem Handdynamometer — notiere den besten von mehreren Versuchen — und der Verlauf erscheint hier.",
+ "description": "Erfasse einen Wert mit einem Handdynamometer (notiere den besten von mehreren Versuchen), und der Verlauf erscheint hier.",
"cta": "Griffkraft erfassen"
},
- "coachPrefill": "Ich habe noch keine Griffkraft erfasst — was sagt sie über meine Muskelgesundheit aus, und wie messe und verbessere ich sie?"
+ "coachPrefill": "Ich habe noch keine Griffkraft erfasst. Was sagt sie über meine Muskelgesundheit aus, und wie messe und verbessere ich sie?"
},
"pain": {
"title": "Schmerz (0–10)",
@@ -4506,29 +4665,29 @@
"description": "Erfasse auf der Skala 0–10, wie sich dein Schmerz gerade anfühlt, und der Verlauf erscheint hier.",
"cta": "Schmerzwert erfassen"
},
- "coachPrefill": "Ich habe noch keine Schmerzwerte erfasst — wie sollte ich Schmerz auf der Skala 0–10 verfolgen, und was gilt als bedeutsame Veränderung?"
+ "coachPrefill": "Ich habe noch keine Schmerzwerte erfasst. Wie sollte ich Schmerz auf der Skala 0–10 verfolgen, und was gilt als bedeutsame Veränderung?"
},
"waist": {
"title": "Taillenumfang",
- "description": "Taillenumfang in Zentimetern — ein Marker für Bauchfett. Der WHO-Grenzwert für erhöhtes Risiko ist geschlechtsspezifisch (Männer 94 cm, Frauen 80 cm); dein eigener Verlauf führt die Einschätzung.",
+ "description": "Taillenumfang in Zentimetern, ein Marker für Bauchfett. Der WHO-Grenzwert für erhöhtes Risiko ist geschlechtsspezifisch (Männer 94 cm, Frauen 80 cm); dein eigener Verlauf führt die Einschätzung.",
"chartTitle": "Taillenumfang",
"emptyState": {
"title": "Noch keine Taillenwerte",
"description": "Miss deine Taille mittig zwischen der untersten Rippe und dem Hüftknochen am Ende einer normalen Ausatmung und erfasse den Wert hier.",
"cta": "Taillenumfang erfassen"
},
- "coachPrefill": "Ich habe noch keine Taillenwerte erfasst — wie messe ich meine Taille richtig, und was bedeutet der Wert für meine Gesundheit?"
+ "coachPrefill": "Ich habe noch keine Taillenwerte erfasst. Wie messe ich meine Taille richtig, und was bedeutet der Wert für meine Gesundheit?"
},
"waistToHeight": {
"title": "Taille-zu-Größe-Verhältnis",
- "description": "Deine Taille geteilt durch deine Größe — die ethnienunabhängige Einzelzahl. Sie unter 0,5 zu halten (Taille unter der halben Körpergröße) ist das einfache Ziel.",
+ "description": "Deine Taille geteilt durch deine Größe, die ethnienunabhängige Einzelzahl. Sie unter 0,5 zu halten (Taille unter der halben Körpergröße) ist das einfache Ziel.",
"chartTitle": "Taille-zu-Größe-Verhältnis",
"emptyState": {
"title": "Noch kein Taille-zu-Größe-Verhältnis",
"description": "Erfasse ein Taille-zu-Größe-Verhältnis, und der Verlauf erscheint hier. Faustregel: Halte deine Taille unter der halben Körpergröße.",
"cta": "Taille-zu-Größe-Verhältnis erfassen"
},
- "coachPrefill": "Ich habe noch kein Taille-zu-Größe-Verhältnis erfasst — warum ist es eine nützliche Zahl, und wie halte ich es unter 0,5?"
+ "coachPrefill": "Ich habe noch kein Taille-zu-Größe-Verhältnis erfasst. Warum ist es eine nützliche Zahl, und wie halte ich es unter 0,5?"
},
"ecg": {
"sectionTitle": "EKG-Aufzeichnungen",
@@ -4569,9 +4728,9 @@
"emptyCta": "Gerät verbinden"
},
"intradayPulse": {
- "title": "Der Verlauf deines Tages",
- "caption": "Dein Puls über den Tag, gemittelt in 10-Minuten-Schritten.",
- "hourlyNote": "Stündlicher Durchschnitt für diesen Tag — aktuelle Tage zeigen das volle 10-Minuten-Detail.",
+ "title": "Puls über den Tag",
+ "caption": "Gemittelt in 10-Minuten-Schritten.",
+ "hourlyNote": "Stündlicher Durchschnitt für diesen Tag; aktuelle Tage zeigen das volle 10-Minuten-Detail.",
"empty": "Für diesen Tag noch nicht genug Pulswerte, um den Tagesverlauf zu zeigen.",
"coverageOne": "Basierend auf 1 Messung über {hours}h.",
"coverageMany": "Basierend auf {count} Messungen über {hours}h.",
@@ -4580,10 +4739,10 @@
"previousDay": "Vorheriger Tag",
"nextDay": "Nächster Tag",
"tension": {
- "morning": "Eine anhaltend erhöhte Ruhephase heute Vormittag — möglicherweise etwas Anspannung.",
- "afternoon": "Eine anhaltend erhöhte Ruhephase heute Nachmittag — möglicherweise etwas Anspannung.",
- "evening": "Eine anhaltend erhöhte Ruhephase heute Abend — möglicherweise etwas Anspannung.",
- "night": "Eine anhaltend erhöhte Ruhephase in der Nacht — möglicherweise etwas Anspannung."
+ "morning": "Eine anhaltend erhöhte Ruhephase heute Vormittag, möglicherweise etwas Anspannung.",
+ "afternoon": "Eine anhaltend erhöhte Ruhephase heute Nachmittag, möglicherweise etwas Anspannung.",
+ "evening": "Eine anhaltend erhöhte Ruhephase heute Abend, möglicherweise etwas Anspannung.",
+ "night": "Eine anhaltend erhöhte Ruhephase in der Nacht, möglicherweise etwas Anspannung."
}
},
"navEcg": "EKG",
@@ -4594,6 +4753,19 @@
"settingsBody": "HealthLog funktioniert vollständig ohne KI. Verbinde unten einen Anbieter, wenn du ein tägliches Briefing, kurze Notizen zu deinen Werten und den Coach möchtest.",
"action": "KI einrichten",
"dismiss": "Nicht jetzt"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Ich habe meine Körpergröße noch nicht eingetragen. Warum ist der BMI wichtig, und was sollte ich wissen, bevor ich sie eintrage?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Ich habe noch keine Medikamente angelegt. Was sollte ich wissen, bevor ich hier anfange, meine Therapietreue zu verfolgen?"
+ },
+ "scoreHistory": {
+ "healthTitle": "Verlauf des Gesundheitsscores",
+ "healthDescription": "Dein Gesundheitsscore Tag für Tag, so wie er an jedem Tag angezeigt wurde.",
+ "emptyDescription": "In diesem Zeitraum wurde noch kein Score erfasst.",
+ "usualRange": "Schattiert: dein üblicher Bereich, {lo} bis {hi}.",
+ "seam": "Die Linie bricht dort ab, wo sich geändert hat, was in den Score einfließt."
}
},
"thresholds": {
@@ -4623,14 +4795,15 @@
"metricBodyFat": "Körperfett",
"metricSleep": "Schlafdauer",
"metricSteps": "Schritte/Tag",
- "metricGlucoseFasting": "Glukose — nüchtern",
- "metricGlucosePostprandial": "Glukose — postprandial",
- "metricGlucoseRandom": "Glukose — beliebig",
- "metricGlucoseBedtime": "Glukose — vor dem Schlafen",
+ "metricGlucoseFasting": "Glukose, nüchtern",
+ "metricGlucosePostprandial": "Glukose, postprandial",
+ "metricGlucoseRandom": "Glukose, beliebig",
+ "metricGlucoseBedtime": "Glukose, vor dem Schlafen",
"metricBodyWater": "Körperwasser",
"metricBoneMass": "Knochenmasse",
"metricOxygenSaturation": "Sauerstoffsättigung (SpO₂)",
- "unsetExplanation": "Kein eigener Wert — berechneter Default aktiv."
+ "unsetExplanation": "Kein eigener Wert, berechneter Default aktiv.",
+ "cardTitle": "Bereiche je Messgröße"
},
"onboarding": {
"errorGeneric": "Beim Abschluss des Setups ist etwas schiefgelaufen. Bitte versuche es erneut.",
@@ -4643,11 +4816,11 @@
"steps": {
"dashboardOverview": {
"title": "Dein Dashboard",
- "body": "Das Dashboard bündelt alle Module auf einen Blick. Halte eine Kachel lange gedrückt, um sie neu anzuordnen oder auszublenden (Einstellungen → Dashboard-Layout). Jede Kachel ist nur so aussagekräftig wie die Daten dahinter — die folgenden Module füllen sie."
+ "body": "Das Dashboard bündelt alle Module auf einen Blick. Halte eine Kachel lange gedrückt, um sie neu anzuordnen oder auszublenden (Einstellungen → Dashboard-Layout). Jede Kachel ist nur so aussagekräftig wie die Daten dahinter. Die folgenden Module füllen sie."
},
"quickAdd": {
"title": "In Sekunden erfasst",
- "body": "Über „+ Hinzufügen“ trägst du jeden Messwert in Sekunden ein — Gewicht, Blutdruck, Puls, Glukose. Versteckte Stärke: Werte lassen sich nachträglich datieren. Alles, was du hier einträgst, fließt in Trends, Health-Score und die KI-Insights."
+ "body": "Über „+ Hinzufügen“ trägst du jeden Messwert in Sekunden ein: Gewicht, Blutdruck, Puls, Glukose. Versteckte Stärke: Werte lassen sich nachträglich datieren. Alles, was du hier einträgst, fließt in Trends, Health-Score und die KI-Insights."
},
"measurements": {
"title": "Messwerte & Zielbereiche",
@@ -4659,7 +4832,7 @@
},
"labs": {
"title": "Laborwerte",
- "body": "Labore erfasst Biomarker mit Referenzbereichen — Blutbild, Cholesterin, HbA1c. Stärke: Import und FHIR-Export. Auffällige Laborwerte tauchen als Belege in den Insights und im Arztbericht wieder auf."
+ "body": "Labore erfasst Biomarker mit Referenzbereichen: Blutbild, Cholesterin, HbA1c. Stärke: Import und FHIR-Export. Auffällige Laborwerte tauchen als Belege in den Insights und im Arztbericht wieder auf."
},
"illness": {
"title": "Krankheits-Journal",
@@ -4675,31 +4848,31 @@
},
"mood": {
"title": "Stimmung",
- "body": "Stimmung erfasst dein Befinden und Tags in zwei Sekunden. Stärke: eigene Tags. Stimmungsverläufe korrelieren mit Schlaf, Medikamenten und Zyklus — der Coach erkennt Muster."
+ "body": "Stimmung erfasst dein Befinden und Tags in zwei Sekunden. Stärke: eigene Tags. Stimmungsverläufe korrelieren mit Schlaf, Medikamenten und Zyklus, der Coach erkennt Muster."
},
"insights": {
"title": "Insights",
- "body": "Insights ist das Herzstück: belegbasierte, dynamische Auswertungen statt generischer Tipps — jede Aussage verweist auf deine echten Daten. Versteckt: das Layout ist anpassbar. Insights zieht alles zusammen, was die Module oben liefern."
+ "body": "Insights ist das Herzstück: belegbasierte, dynamische Auswertungen statt generischer Tipps. Jede Aussage verweist auf deine echten Daten. Versteckt: das Layout ist anpassbar. Insights zieht alles zusammen, was die Module oben liefern."
},
"coach": {
"title": "Coach",
- "body": "Der Coach beantwortet Fragen zu deinen Daten im Dialog und nutzt Anamnese, Medikamente und Laborwerte als Kontext. Den KI-Anbieter wählst du selbst (eigener Schlüssel oder lokal). Er rechnet nichts neu — er liest dieselben Zahlen wie das Dashboard."
+ "body": "Der Coach beantwortet Fragen zu deinen Daten im Dialog und nutzt Anamnese, Medikamente und Laborwerte als Kontext. Den KI-Anbieter wählst du selbst (eigener Schlüssel oder lokal). Er rechnet nichts neu, er liest dieselben Zahlen wie das Dashboard."
},
"integrations": {
"title": "Integrationen",
- "body": "Integrationen synchronisieren Withings, WHOOP, Polar, Oura und Apple Health automatisch. So füllen sich Gewicht, Schlaf und Erholung ohne manuelles Tippen — und nähren wiederum Insights und Coach."
+ "body": "Integrationen synchronisieren Withings, WHOOP, Polar, Oura und Apple Health automatisch. So füllen sich Gewicht, Schlaf und Erholung ohne manuelles Tippen und nähren wiederum Insights und Coach."
},
"export": {
"title": "Arztbericht",
- "body": "Der Arztbericht bündelt Messwerte, Medikamente, Laborwerte und Vorsorge als PDF oder FHIR für den Termin. Hier laufen alle Module zusammen — ein vollständiger Bericht entsteht nur bei gepflegten Daten."
+ "body": "Der Arztbericht bündelt Messwerte, Medikamente, Laborwerte und Vorsorge als PDF oder FHIR für den Termin. Hier laufen alle Module zusammen. Ein vollständiger Bericht entsteht nur bei gepflegten Daten."
},
"achievements": {
"title": "Erfolge",
- "body": "Erfolge belohnen Konsistenz — Streaks und Meilensteine. Ein kleiner Motivations-Loop, der das Dranbleiben leichter macht."
+ "body": "Erfolge belohnen Konsistenz: Streaks und Meilensteine. Ein kleiner Motivations-Loop, der das Dranbleiben leichter macht."
},
"wrapUp": {
"title": "Das war die Tour",
- "body": "Diese Führung läuft einmalig — sie öffnet sich nicht von selbst erneut. Alles Gezeigte bleibt erhalten, und du kannst Funktionen jederzeit unter Einstellungen → Module ein- oder ausschalten."
+ "body": "Diese Führung läuft einmalig und öffnet sich nicht von selbst erneut. Alles Gezeigte bleibt erhalten, und du kannst Funktionen jederzeit unter Einstellungen → Module ein- oder ausschalten."
}
}
},
@@ -4730,7 +4903,7 @@
"title": "Alles bereit",
"body": "Willkommen an Bord. Dein Dashboard wartet.",
"returnCta": "Zum Dashboard",
- "learning": "Manche Einblicke — Glukose, Schlafrhythmus, Blutdrucktrends, Zyklus — schärfen sich in den ersten ein bis zwei Wochen, sobald sich deine Daten ansammeln. Leere Kacheln heißen jetzt nur, dass wir deine Basislinie noch lernen.",
+ "learning": "Manche Einblicke (Glukose, Schlafrhythmus, Blutdrucktrends, Zyklus) schärfen sich in den ersten ein bis zwei Wochen, sobald sich deine Daten ansammeln. Leere Kacheln heißen jetzt nur, dass wir deine Basislinie noch lernen.",
"connectCta": "Gerät verbinden",
"logCta": "Ersten Wert eintragen",
"importCta": "Bestehende Daten importieren",
@@ -4747,33 +4920,33 @@
"anamnesis": {
"title": "Zu deiner Gesundheit (optional)",
"subtitle": "Etwas Kontext, damit HealthLog deine Werte fairer einordnet.",
- "intro": "Teile so viel oder so wenig, wie du möchtest — jedes Feld ist optional und jederzeit in den Einstellungen änderbar.",
+ "intro": "Teile so viel oder so wenig, wie du möchtest. Jedes Feld ist optional und jederzeit in den Einstellungen änderbar.",
"conditionsLabel": "Bestehende Erkrankungen",
"conditionsPlaceholder": "z. B. Bluthochdruck, Typ-2-Diabetes",
"conditionsHint": "Hilft HealthLog, deine Werte im Kontext einzuordnen, statt Erwartbares zu melden.",
"allergiesLabel": "Allergien oder Unverträglichkeiten",
"allergiesPlaceholder": "z. B. Penicillin, Laktose",
- "privacyNote": "Verschlüsselt gespeichert und nur für besseren Kontext genutzt. Medikamente fragen wir hier nicht ab — die kannst du jederzeit auf der Medikamenten-Seite ergänzen."
+ "privacyNote": "Verschlüsselt gespeichert und nur für besseren Kontext genutzt. Medikamente fragen wir hier nicht ab, die kannst du jederzeit auf der Medikamenten-Seite ergänzen."
},
"ai": {
"panelTitle": "So sehen Insights aus",
- "panelIntro": "Coach, das tägliche Briefing und die Einschätzungen je Messwert übersetzen deine eigenen Zahlen in Klartext. So sieht das aus — bevor du irgendetwas verbindest.",
+ "panelIntro": "Coach, das tägliche Briefing und die Einschätzungen je Messwert übersetzen deine eigenen Zahlen in Klartext. So sieht das aus, bevor du irgendetwas verbindest.",
"showSample": "Beispiel-Briefing ansehen",
"sampleTitle": "Tägliches Briefing",
"sampleTag": "Beispiel",
"sampleAriaLabel": "Beispielhaftes tägliches Briefing",
- "sampleBody": "Gewicht in zwei Wochen um 1,2 kg gesunken. Ruhepuls stabil bei 58 bpm. Blutdruck an 6 von 7 Tagen im Zielbereich — der einzige hohe Wert folgte auf ein spätes, salziges Abendessen.",
- "sampleCaption": "Nur ein Beispiel — nicht deine eigenen Daten. So liest sich ein Briefing, sobald Insights aktiv sind.",
+ "sampleBody": "Gewicht in zwei Wochen um 1,2 kg gesunken. Ruhepuls stabil bei 58 bpm. Blutdruck an 6 von 7 Tagen im Zielbereich; der einzige hohe Wert folgte auf ein spätes, salziges Abendessen.",
+ "sampleCaption": "Nur ein Beispiel, nicht deine eigenen Daten. So liest sich ein Briefing, sobald Insights aktiv sind.",
"ladderTitle": "Drei Wege, es zu aktivieren",
- "ladderLocalTitle": "Lokales Modell — KI-Anfragen bleiben in deinem Netzwerk",
- "ladderLocalBody": "Richte HealthLog auf Ollama oder LM Studio auf deinem eigenen Rechner aus. Kostenlos und vollständig privat — der ruhige Standard.",
+ "ladderLocalTitle": "Lokales Modell: KI-Anfragen bleiben in deinem Netzwerk",
+ "ladderLocalBody": "Richte HealthLog auf Ollama oder LM Studio auf deinem eigenen Rechner aus. Kostenlos und vollständig privat, der ruhige Standard.",
"ladderByokTitle": "Eigenen Schlüssel verwenden",
"ladderByokBody": "Nutze deinen eigenen OpenAI- oder Anthropic-Schlüssel. Er läuft über dein Konto und deine Abrechnung, und die API-Stufe hält deine Daten standardmäßig aus dem Training heraus.",
"ladderOauthTitle": "Mit einem Abo anmelden",
- "ladderOauthBody": "Verbinde ein ChatGPT-Konto — keine Kosten pro Token, aber die Bedingungen der Verbraucherstufe können Inhalte fürs Training nutzen. Deshalb eine bewusste Wahl, nie der Standard für deine Gesundheitsdaten.",
- "keylessLine": "HealthLog ist auch ohne KI vollständig nutzbar. Deine Kacheln, Diagramme, Trends und Exporte funktionieren alle — KI ergänzt nur den Klartext obendrauf.",
+ "ladderOauthBody": "Verbinde ein ChatGPT-Konto: keine Kosten pro Token, aber die Bedingungen der Verbraucherstufe können Inhalte fürs Training nutzen. Deshalb eine bewusste Wahl, nie der Standard für deine Gesundheitsdaten.",
+ "keylessLine": "HealthLog ist auch ohne KI vollständig nutzbar. Deine Kacheln, Diagramme, Trends und Exporte funktionieren alle. KI ergänzt nur den Klartext obendrauf.",
"setupCta": "Einrichten (etwa eine Minute)",
- "sharedKeyNote": "Diese Instanz stellt bereits einen gemeinsamen KI-Anbieter bereit, deshalb funktionieren Insights für dich sofort — keine Einrichtung nötig.",
+ "sharedKeyNote": "Diese Instanz stellt bereits einen gemeinsamen KI-Anbieter bereit, deshalb funktionieren Insights für dich sofort, keine Einrichtung nötig.",
"offer": {
"title": "Die Zusammenfassung dieser Instanz ist bereit.",
"body": "Der Betreiber stellt einen Anbieter bereit, der zuletzt innerhalb der letzten 24 Stunden funktioniert hat. Mit einem Tipp schaltest du den Klartext ein; deine Daten gehen dann an diesen einen Anbieter, an niemanden sonst.",
@@ -4990,7 +5163,7 @@
"profileDescription": "Größe, Geburtsdatum und Geschlecht schalten personalisierte Zielwerte frei.",
"profileCta": "Profil öffnen",
"measurementTitle": "Erste Messung hinzufügen",
- "measurementDescription": "Ein Messpunkt reicht, um die Kachel zu aktivieren — der Trend zeichnet sich ab dem 5. Eintrag.",
+ "measurementDescription": "Ein Messpunkt reicht, um die Kachel zu aktivieren. Der Trend zeichnet sich ab dem 5. Eintrag.",
"measurementCta": "Messung hinzufügen",
"medicationTitle": "Medikament hinzufügen",
"medicationDescription": "Verfolge die Therapietreue und erhalte Erinnerungen genau dann, wenn du sie brauchst.",
@@ -4999,7 +5172,7 @@
"dataSourceDescription": "Gewicht, Blutdruck, Schlaf und mehr automatisch aus Withings, WHOOP, Oura, Polar, Nightscout, Fitbit oder Apple Health.",
"dataSourceCta": "Verbinden",
"notificationsTitle": "Benachrichtigungen einrichten",
- "notificationsDescription": "Wähle, wie HealthLog dich erinnert — Telegram, ntfy oder Web Push.",
+ "notificationsDescription": "Wähle, wie HealthLog dich erinnert: Telegram, ntfy oder Web Push.",
"notificationsCta": "Benachrichtigungen öffnen",
"insightsTitle": "Insights aktivieren (optional)",
"insightsDescription": "Füge ein lokales Modell oder deinen eigenen KI-Schlüssel hinzu, um das tägliche Briefing und Coach freizuschalten.",
@@ -5088,11 +5261,6 @@
"brief": "Kurz",
"default": "Standard",
"detailed": "Ausführlich",
- "defaultWindow": "Standardzeitraum",
- "last7days": "Letzte 7 Tage",
- "last30days": "Letzte 30 Tage",
- "last90days": "Letzte 90 Tage",
- "allTime": "Gesamter Zeitraum",
"excludedMetrics": "Ausgeschlossene Messwerte",
"dataClusters": "Datenbereiche",
"customClusters": "Eigene Auswahl von Datenbereichen verwenden",
@@ -5152,12 +5320,10 @@
"sections": {
"account": {
"title": "Konto",
- "description": "Profil, Passwort, Passkeys und deine Onboarding-Tour.",
"subtitle": "Dein Profil, deine Identität und deine Anmeldedaten."
},
"integrations": {
"title": "Integrationen",
- "description": "Verbundene Dienste, Zustellkanäle und die Quellen-Priorität an einem Ort.",
"subtitle": "Datenquellen und Geräte verbinden und verwalten.",
"connectionsHeading": "Verbindungen",
"channelsHeading": "Zustellkanäle",
@@ -5166,16 +5332,15 @@
},
"notifications": {
"title": "Benachrichtigungen",
- "description": "Alle Erinnerungen an einem Ort — was wann erinnert wird und wohin es zugestellt wird.",
"subtitle": "Wähle, welche Erinnerungen du erhältst und wohin sie zugestellt werden.",
"remindersHeading": "Erinnerungen",
"matrixLink": "Benachrichtigungen je Kanal",
- "channelsLink": "Zustellkanäle"
+ "channelsLink": "Zustellkanäle",
+ "displayHeading": "Auf dem Handy"
},
"layout": {
- "title": "Darstellung",
- "description": "Lege fest, wie Dashboard, Auswertungen und deine Tracking-Module dargestellt und angeordnet sind.",
- "backToHub": "Zurück zu Darstellung",
+ "title": "Layout",
+ "backToHub": "Zurück zu Layout",
"dashboard": {
"title": "Dashboard",
"description": "Kachel-Layout und Reihenfolge."
@@ -5184,7 +5349,7 @@
"title": "Insights",
"description": "Reihenfolge der Übersicht und der Navigations-Pills."
},
- "subtitle": "Lege fest, wie Dashboard, Auswertungen und Tracking-Module dargestellt werden.",
+ "subtitle": "Lege fest, was Dashboard, Auswertungen und Tracking-Module zeigen und in welcher Reihenfolge.",
"medications": {
"title": "Medikamente",
"description": "Listenansicht und manuelle Reihenfolge."
@@ -5204,6 +5369,10 @@
"mood": {
"title": "Stimmungs-Tags",
"description": "Gruppen, Tags und archivierte Tags der Stimmungs-Auswahl."
+ },
+ "documents": {
+ "title": "Dokumente",
+ "description": "Ansicht und Anordnung der Monate."
}
},
"dashboard": {
@@ -5211,15 +5380,13 @@
},
"thresholds": {
"title": "Zielwerte",
- "description": "Pro Metrik dein eigener Zielbereich für gesunde Werte.",
"subtitle": "Lege deine persönlichen Zielbereiche je Messgröße fest."
},
"sources": {
"title": "Quellen-Priorität",
- "description": "Lege fest, welche Quelle führt, wenn mehrere am selben Tag dieselbe Metrik liefern.",
"integrationsHint": "Neue Datenquellen verbindest oder trennst du auf der Integrationen-Seite.",
"integrationsHintLink": "Integrationen öffnen",
- "cardTitle": "Quellen-Priorität pro Metrik",
+ "cardTitle": "Rangfolge je Messgröße",
"help": "Liefern mehrere Quellen denselben Wert am selben Tag, zählt die oberste Quelle als kanonisch.",
"resetDefaults": "Auf Standard zurücksetzen",
"resetDefaultsTitle": "Quellen-Reihenfolge zurücksetzen?",
@@ -5232,7 +5399,7 @@
"resetSuccess": "Quellen-Priorität zurückgesetzt",
"perMetricToggle": "Pro Metrik anpassen ({count})",
"perMetricHelp": "Metriken mit individueller Reihenfolge. Über diese Liste lässt sich eine einzelne Metrik mit einem Klick auf den Standard zurücksetzen, ohne die anderen anzufassen.",
- "perMetricEmpty": "Keine individuellen Anpassungen — jede Metrik nutzt die globale Reihenfolge oben.",
+ "perMetricEmpty": "Keine individuellen Anpassungen. Jede Metrik nutzt die globale Reihenfolge oben.",
"resetMetric": "Zurücksetzen",
"deviceTypeToggle": "Geräte-Typ-Reihenfolge ({count})",
"deviceTypeHelp": "Innerhalb einer Quelle entscheidet die Geräte-Typ-Reihenfolge. Wenn Apple Watch und iPhone beide Schritte aufzeichnen, zählt der höher gereihte Geräte-Typ.",
@@ -5257,7 +5424,8 @@
"respiratoryRate": "Atemfrequenz",
"recovery": "Erholung",
"stress": "Stress",
- "sleepDebt": "Schlafdefizit"
+ "sleepDebt": "Schlafdefizit",
+ "hrvRmssd": "HRV (RMSSD)"
},
"sourceLabels": {
"WITHINGS": "Withings",
@@ -5274,7 +5442,8 @@
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava",
- "EXTERNAL": "Externes Gerät"
+ "EXTERNAL": "Externes Gerät",
+ "HEALTH_CONNECT": "Health Connect"
},
"deviceLabels": {
"watch": "Uhr",
@@ -5294,28 +5463,25 @@
"daysWithData": "{count} von {days} Tagen",
"moduleHint": "Die Synchronisierung steuert das Modul „Nährstoffzufuhr“.",
"moduleHintLink": "Module verwalten",
- "detail": "Nur lesend — die Quell-App besitzt die Werte."
+ "detail": "Nur lesend, die Quell-App besitzt die Werte."
},
- "helpDetail": "Die übrigen bleiben für die Audit-Spur gespeichert."
+ "helpDetail": "Die übrigen bleiben für die Audit-Spur gespeichert. Aufgeführt sind nur Quellen, die du verbunden hast oder von denen Daten vorliegen.",
+ "noneInUse": "Noch keine Quelle zum Sortieren. Verbinde eine unter Integrationen oder trage einen Wert ein."
},
"ai": {
"title": "KI-Anbieter",
- "description": "Provider, Modell und API-Key für die Auswertungen — dazu dein „Über mich“-Kontext.",
"subtitle": "Wähle deinen KI-Anbieter und hinterlege deinen eigenen Schlüssel."
},
"api": {
"title": "API & Tokens",
- "description": "Bearer-Tokens für eigene Skripte und Apps — erfasse Messwerte oder Einnahmen von überall.",
"subtitle": "API-Endpunkte und Zugriffstoken verwalten."
},
"advanced": {
"title": "Erweitert",
- "description": "Der Notausgang: alle Gesundheitsdaten unwiderruflich löschen oder das Konto auflösen.",
"subtitle": "Daten zurücksetzen und Konto löschen."
},
"export": {
"title": "Export & Import",
- "description": "Lade deine Daten als CSV oder vollständiges JSON-Backup herunter — oder importiere aus Apple Health, JSON oder CSV. Den Arztbericht findest du jetzt unter Gesundheitsakte.",
"otherOptionsHeading": "Weitere Export-Optionen",
"downloadFailed": "Der Export konnte nicht heruntergeladen werden. Bitte versuche es erneut.",
"actions": {
@@ -5342,12 +5508,12 @@
"fullBackup": {
"title": "Voll-Backup",
"description": "Deine gesamte Akte in einer JSON-Datei.",
- "scopeNote": "Dokumentmetadaten und die KI-Zusammenfassung, falls vorhanden, sind als Klartext enthalten; die Originaldateien der Dokumente sowie GPS-Routen und Einzelmesswerte von Workouts sind es nicht — lade diese separat aus dem Dokumententresor bzw. dem Workout-Export herunter.",
+ "scopeNote": "Dokumentmetadaten und die KI-Zusammenfassung, falls vorhanden, sind als Klartext enthalten; die Originaldateien der Dokumente sowie GPS-Routen und Einzelmesswerte von Workouts sind es nicht. Lade diese separat aus dem Dokumententresor bzw. dem Workout-Export herunter.",
"encryptToggle": "Mit einer Passphrase verschlüsseln",
"passphrasePlaceholder": "Passphrase (mind. 12 Zeichen)",
"passphraseHelp": "Leitet den Archivschlüssel per Argon2id ab und versiegelt ihn mit AES-256-GCM. Speichere die Datei mit der Endung .hlx.",
- "noRecoveryWarning": "Es gibt keine Wiederherstellung — geht diese Passphrase verloren, lässt sich das Archiv nicht mehr öffnen. Bewahre sie sicher auf.",
- "detail": "Gleiche Form wie die wöchentliche automatische Sicherung. Einige Satzarten sind noch nicht enthalten — das Manifest der Datei nennt, was drinsteht.",
+ "noRecoveryWarning": "Es gibt keine Wiederherstellung. Geht diese Passphrase verloren, lässt sich das Archiv nicht mehr öffnen. Bewahre sie sicher auf.",
+ "detail": "Gleiche Form wie die wöchentliche automatische Sicherung. Einige Satzarten sind noch nicht enthalten; das Manifest der Datei nennt, was drinsteht.",
"contents": "Deine Messwerte, Medikamente, Stimmung, Zyklus, Laborwerte, Krankheitsepisoden, Allergien, Familienanamnese, Nährstoffe, Workouts, dein Gesundheitsprofil und deine eigenen Messgrößen in einer JSON-Datei."
}
},
@@ -5362,7 +5528,7 @@
"dropHint": "oder zum Auswählen klicken",
"fileInputLabel": "Eine Apple-Health-export.zip-Datei auswählen",
"choose": "Datei auswählen",
- "idempotencyNote": "Ein erneuter Upload desselben Archivs führt zusammen — es entstehen keine Duplikate. Große Archive werden im Hintergrund verarbeitet.",
+ "idempotencyNote": "Ein erneuter Upload desselben Archivs führt die Daten zusammen, ohne Duplikate. Große Archive werden im Hintergrund verarbeitet.",
"uploading": "Wird hochgeladen…",
"uploadFailed": "Upload fehlgeschlagen. Bitte erneut versuchen.",
"rateLimited": "Zu viele Uploads. Bitte kurz warten und erneut versuchen.",
@@ -5371,8 +5537,11 @@
"failureInterrupted": "Der Import wurde unterbrochen, bevor er fertig war, meist durch einen Server-Neustart oder Speichermangel. Die Daten sind sicher; das Archiv einfach erneut hochladen.",
"failureInsufficientMemory": "Das Speicherlimit des Servers ist für ein Archiv dieser Größe zu niedrig. Bitte die Betreiberin oder den Betreiber das Node.js-Heap-Limit erhöhen lassen (NODE_OPTIONS=--max-old-space-size) und das Archiv danach erneut hochladen.",
"rowsImported": "Bisher {count} Einträge importiert",
- "doneSummary": "Fertig — {imported} von {read} Datensätzen importiert, {skipped} klinische Datensätze übersprungen.",
+ "doneSummary": "Fertig: {imported} von {read} Datensätzen importiert, {skipped} klinische Datensätze übersprungen.",
"doneNothing": "Aus diesem Archiv wurde nichts importiert: 0 von {read} Datensätzen sind angekommen.",
+ "writtenByHealthLogOne": "{count} Eintrag, den HealthLog selbst in Apple Health geschrieben hat, wurde ausgelassen, weil er schon in deinen Daten ist.",
+ "writtenByHealthLogOther": "{count} Einträge, die HealthLog selbst in Apple Health geschrieben hat, wurden ausgelassen, weil sie schon in deinen Daten sind.",
+ "writtenByHealthLogFew": "{count} Einträge, die HealthLog selbst in Apple Health geschrieben hat, wurden ausgelassen, weil sie schon in deinen Daten sind.",
"ecgResult": "EKGs: {discovered} gefunden, {imported} importiert, {updated} aktualisiert, {skipped} übersprungen, {failed} fehlgeschlagen. Fehler bei EKGs verwerfen keine unterstützten Datensätze, die aus export.xml importiert wurden.",
"estimateWarning": "Kumulative Summen für {count} Tag(e) wurden anhand der größten Exportquelle geschätzt. Native Apple-Health-Statistiken bleiben maßgeblich.",
"phase": {
@@ -5381,12 +5550,12 @@
"parsing": "Export wird gelesen…",
"upserting": "Datensätze werden importiert…"
},
- "skipUnsupported": "{count} Samples übersprungen — Datentypen, die HealthLog nicht importiert.",
+ "skipUnsupported": "{count} Samples übersprungen: Datentypen, die HealthLog nicht importiert.",
"skipRefused": "{count} Samples konnten nicht geschrieben werden (unlesbar, außerhalb des Wertebereichs oder Speichern fehlgeschlagen).",
"skipBreakdown": "Aufschlüsselung anzeigen ({count} Typen)",
"skipBreakdownMore": "+ {count} weitere Typen",
"cycleSummary": "{samples} Zyklus-Samples zu {days} Tagesprotokollen zusammengeführt.",
- "cycleModuleOff": "{count} Zyklus-Samples übersprungen — Zyklus-Tracking ist für dieses Konto deaktiviert.",
+ "cycleModuleOff": "{count} Zyklus-Samples übersprungen, weil Zyklus-Tracking für dieses Konto deaktiviert ist.",
"cycleDaysFailed": "{count} Zyklustage konnten nicht geschrieben werden: {reason}"
},
"json": {
@@ -5471,8 +5640,8 @@
"failed": "Der Import ist fehlgeschlagen. Es wurde nichts geschrieben.",
"columnsHeading": "Was mit jeder Spalte passiert",
"jsonOnly": "nur JSON",
- "previewLine": "Zeilen gelesen: {rows} · würde importieren: {queued} · abgewiesen: {refused}",
- "submittedLine": "Zeilen gelesen: {rows} · zum Import eingereiht: {queued}",
+ "previewLine": "{rows} Zeilen gelesen, {queued} würden importiert, {refused} abgewiesen",
+ "submittedLine": "{rows} Zeilen gelesen, {queued} zum Import eingereiht",
"unmatched": "Kein Medikament im Bestand passt zu: {names}",
"ambiguous": "Passt auf mehr als ein Medikament: {names}",
"mirrored": "Aus einer anderen App gespiegelt, daher keine Dosis ergänzbar: {names}",
@@ -5508,18 +5677,59 @@
"tooManyRows": "Diese Datei enthält mehr Zeilen, als ein Import aufnehmen kann. Teilen und in Teilen importieren."
},
"detail": "Eine Datei deckt alle Medikamente darin ab."
+ },
+ "healthConnect": {
+ "title": "Health-Connect-Export",
+ "description": "Importiere die ZIP-Datei, die die Health-Connect-App auf Android beim Export deiner Daten schreibt.",
+ "choose": "ZIP-Datei wählen",
+ "uploading": "Wird hochgeladen …",
+ "uploadFailed": "Das Hochladen ist fehlgeschlagen. Bitte versuch es noch einmal.",
+ "tooLarge": "Die Datei ist größer, als dieser Server annimmt.",
+ "doneSummary": "{count} Werte importiert.",
+ "failed": "Der Import ist fehlgeschlagen: {reason}",
+ "phase": {
+ "queued": "Wartet auf den Start",
+ "unpacking": "Datei wird entpackt",
+ "parsing": "Einträge werden gelesen",
+ "upserting": "Werte werden gespeichert",
+ "done": "Fertig",
+ "failed": "Fehlgeschlagen"
+ },
+ "steps": {
+ "export": "Öffne auf deinem Android-Handy Health Connect, geh zu „Daten verwalten“, dann „Exportieren und importieren“, und starte einen Export.",
+ "cloud": "Health Connect speichert den Export nur in einer Cloud-App wie Google Drive, Dropbox oder Nextcloud, nie auf dem Handy selbst. Wähl dort eine aus.",
+ "upload": "Lade die ZIP-Datei aus dieser Cloud auf dieses Gerät herunter und lade sie hier hoch."
+ },
+ "dropLabel": "Health-Connect-ZIP hier ablegen",
+ "dropHint": "oder klicken, um sie auszuwählen. Bis 1 GB.",
+ "fileInputLabel": "ZIP-Datei des Health-Connect-Exports",
+ "privacyNote": "Die Datei wird vom Server gelöscht, sobald der Import endet. Einträge von Apps, die du schon direkt verbunden hast, etwa Withings oder Fitbit, werden ausgelassen, damit nichts doppelt ankommt. Dieselbe Datei noch einmal hochzuladen ändert nichts.",
+ "rateLimited": "Zu viele Uploads in kurzer Zeit. Warte eine Minute und versuch es noch einmal.",
+ "busy": "Für dein Konto läuft schon ein Import. Warte, bis er fertig ist.",
+ "rowsSoFar": "Bisher {count} Werte gespeichert",
+ "doneNothing": "Der Import ist fertig, es gab aber nichts Neues.",
+ "workouts": "Trainings",
+ "leftOutConnected": "{count} Einträge von Apps, die du schon direkt verbunden hast, wurden ausgelassen.",
+ "nutrientsModuleOff": "Wasser- und Nährstoffsummen wurden nicht importiert, weil das Modul Nährstoffe ausgeschaltet ist.",
+ "cycleModuleOff": "Periodendaten wurden nicht importiert, weil das Zyklus-Tracking ausgeschaltet ist.",
+ "failure": {
+ "unsupportedVersion": "Dieser Health-Connect-Export ist zu alt, um ihn zu lesen. Aktualisiere Health Connect auf dem Handy und exportiere noch einmal.",
+ "notHealthConnect": "Diese Datei ist kein Health-Connect-Export. Wähl die ZIP-Datei, die Health Connect gespeichert hat.",
+ "unsafeSchema": "Diese Datei hat nicht den Aufbau eines Health-Connect-Exports und wurde nicht gelesen.",
+ "tooLarge": "Der Export ist größer, als dieser Server annimmt.",
+ "stagingMissing": "Die hochgeladene Datei war nicht mehr auf dem Server. Lade sie noch einmal hoch.",
+ "interrupted": "Der Import wurde durch einen Neustart des Servers unterbrochen. Lade die Datei noch einmal hoch."
+ }
}
},
"subtitle": "Exportiere deine Daten oder importiere aus einer Sicherung."
},
"about": {
"title": "Über diese App",
- "description": "Version, Lizenz, Links.",
"subtitle": "Version, Lizenz und Projektlinks."
},
"sharing": {
"title": "Freigabe",
- "description": "Erstelle einen schreibgeschützten Link, um deine Gesundheitsakte mit einer Ärztin oder einem Arzt zu teilen.",
"subtitle": "Teile schreibgeschützten Zugriff auf deine Daten."
},
"insights": {
@@ -5546,7 +5756,7 @@
"error": "Konnte nicht gespeichert werden. Bitte erneut versuchen.",
"toggleable": {
"title": "Was du trackst",
- "description": "Deaktivierte Module verschwinden aus Navigation, Kacheln und Auswertungen — deine Daten bleiben erhalten."
+ "description": "Deaktivierte Module verschwinden aus Navigation, Kacheln und Auswertungen, deine Daten bleiben erhalten."
},
"manageIn": "In {section} verwalten",
"operatorDisabled": "Serverweit deaktiviert",
@@ -5554,12 +5764,10 @@
},
"gesundheitsakte": {
"title": "Gesundheitsakte",
- "description": "Vollständige Gesundheitsakte als PDF, FHIR R4 oder kombiniertes ZIP-Paket exportieren.",
"subtitle": "Exportiere eine vollständige, arzttaugliche Gesundheitsakte."
},
"coach": {
"title": "Coach",
- "description": "Coach aktivieren, Ton und Umfang, gespeicherte Erinnerungen.",
"subtitle": "Stelle den Coach, seine proaktiven Hinweise und sein Gedächtnis ein."
},
"security": {
@@ -5568,8 +5776,7 @@
},
"privacy": {
"title": "Daten & Datenschutz",
- "subtitle": "Deine Daten, Aufbewahrung, Verschlüsselung und Kontosicherheit an einem Ort.",
- "description": "Export, Löschung, Aufbewahrung, Verschlüsselungsstatus, Sitzungen und Sicherheitsaktivität."
+ "subtitle": "Deine Daten, Aufbewahrung, Verschlüsselung und Kontosicherheit an einem Ort."
},
"mcp": {
"title": "MCP-Connector",
@@ -5577,8 +5784,7 @@
},
"anamnesis": {
"title": "Anamnese",
- "subtitle": "Verwalte deine Allergien und deine Familienanamnese.",
- "description": "Strukturierte Datensätze zu Allergien und Familienanamnese."
+ "subtitle": "Deine Vorgeschichte, Lebensumstände, Notfalldaten und was die KI lesen darf."
},
"environment": {
"title": "Standort & Wetter",
@@ -5600,19 +5806,20 @@
"storedDays": "{count} Tage Wetter gespeichert.",
"home": {
"title": "Heimatort",
- "description": "Ein grober Standort auf Stadtebene — niemals eine genaue Spur. Wetter und Tageslicht werden für diesen Ort an Tagen ohne Ortszeitraum abgerufen, ab dem Tag der Festlegung.",
+ "description": "Ein grober Standort auf Stadtebene, niemals eine genaue Spur. Wetter und Tageslicht werden für diesen Ort an Tagen ohne Ortszeitraum abgerufen, ab dem Tag der Festlegung.",
"current": "Aktuell: {label}",
"none": "Noch kein Heimatort festgelegt.",
"since": "Wetter wird ab {date} erfasst. Frühere Tage bleiben leer, bis du einen Ortszeitraum hinzufügst."
},
"travel": {
"title": "Ortszeiträume",
- "description": "Füge einen Ort und einen Zeitraum hinzu — eine Reise oder einen früheren Abschnitt deiner Verlaufsdaten. Der Zeitraum überschreibt für seine Tage deinen Wohnort, damit das Wetter zu deinem tatsächlichen Aufenthalt passt. So korrigierst du die Vergangenheit.",
+ "description": "Füge einen Ort und einen Zeitraum hinzu, etwa eine Reise oder einen früheren Abschnitt deiner Verlaufsdaten. Der Zeitraum überschreibt für seine Tage deinen Wohnort, damit das Wetter zu deinem tatsächlichen Aufenthalt passt. So korrigierst du die Vergangenheit.",
"none": "Noch keine Ortszeiträume.",
"add": "{label} hinzufügen",
"remove": "Ortszeitraum entfernen",
"removeConfirmTitle": "Diese Reise entfernen?",
- "removeConfirmBody": "Messwerte aus diesem Zeitraum bleiben erhalten; nur der Reise-Eintrag verschwindet."
+ "removeConfirmBody": "Messwerte aus diesem Zeitraum bleiben erhalten; nur der Reise-Eintrag verschwindet.",
+ "range": "{start} bis {end}"
},
"backfill": {
"title": "Vergangene Tage nacherfassen",
@@ -5622,7 +5829,21 @@
"noResults": "Keine passenden Orte gefunden.",
"fetchFailedOne": "Die letzte Wetteraktualisierung im Hintergrund ist fehlgeschlagen ({when}).",
"fetchFailedFew": "Die letzten {count} Wetteraktualisierungen im Hintergrund sind fehlgeschlagen, zuletzt {when}.",
- "fetchFailedOther": "Die letzten {count} Wetteraktualisierungen im Hintergrund sind fehlgeschlagen, zuletzt {when}."
+ "fetchFailedOther": "Die letzten {count} Wetteraktualisierungen im Hintergrund sind fehlgeschlagen, zuletzt {when}.",
+ "airQuality": {
+ "title": "Luftqualität und Pollen",
+ "description": "Tägliche Luftqualität, Pollen und UV für deinen Ort, aus dem CAMS-Modell über Open-Meteo.",
+ "toggle": "Luftqualität und Pollen einbeziehen",
+ "operatorDisabled": "Luftqualität ist auf diesem Server abgeschaltet.",
+ "coverage": "{count} Tage mit Luftqualitätsdaten",
+ "pollenEuropeOnly": "Pollendaten gibt es nur für Europa.",
+ "historyProgress": "Luftqualität wird nachgeladen: {done} von {total} Tagen",
+ "historyComplete": "Luftqualität ist für alle früheren Tage mit Einträgen nachgeladen, die die Quelle abdeckt ({total} Tage). Ältere Tage bleiben leer.",
+ "attribution": "Datenquellen und Lizenzen",
+ "saveError": "Die Einstellung konnte nicht gespeichert werden.",
+ "latest": "Jüngster Tag ({date})",
+ "modelNote": "Modellierte Außenwerte für deine Gegend, nicht deine persönliche Belastung."
+ }
},
"score": {
"title": "Gesundheitsscore",
@@ -5655,7 +5876,7 @@
"omitted": "{count} Bereiche stehen nicht in der Liste, weil ihre Erfassung abgeschaltet ist. Schalte ein Modul wieder ein, dann kommt sein Bereich zurück.",
"omittedLink": "Module öffnen",
"refusal": {
- "empty": "Ein Health Score braucht mindestens ein Feld der Gesundheit, das er bewerten kann. Behalte mindestens eines ausgewählt — der Score nennt selbst, worauf er beruht."
+ "empty": "Ein Health Score braucht mindestens ein Feld der Gesundheit, das er bewerten kann. Behalte mindestens eines ausgewählt. Der Score nennt selbst, worauf er beruht."
},
"notice": {
"upgrade": {
@@ -5749,7 +5970,6 @@
},
"changePassword": "Passwort ändern",
"changePasswordDescription": "Ersetze dein aktuelles Passwort durch ein neues.",
- "passwordReset": "Passwort-Reset",
"currentPassword": "Aktuelles Passwort",
"newPassword": "Neues Passwort",
"confirmNewPassword": "Neues Passwort bestätigen",
@@ -5794,9 +6014,9 @@
"botToken": "Bot-Token",
"chatId": "Chat-ID",
"enableNotifications": "Benachrichtigungen aktivieren",
- "telegramStep1": "1. Erstelle einen Bot über @BotFather in Telegram und kopiere den Token.",
- "telegramStep2": "2. Sende /start an deinen Bot, um den Chat zu aktivieren.",
- "telegramStep3": "3. Finde deine Chat-ID über @userinfobot oder die Bot-API.",
+ "telegramStep1": "Erstelle einen Bot über @BotFather in Telegram und kopiere den Token.",
+ "telegramStep2": "Sende /start an deinen Bot, um den Chat zu aktivieren.",
+ "telegramStep3": "Finde deine Chat-ID über @userinfobot oder die Bot-API.",
"ntfy": "ntfy",
"ntfyDescription": "Benachrichtigungen über ntfy (self-hosted oder ntfy.sh).",
"ntfyEnable": "ntfy aktivieren",
@@ -5805,7 +6025,7 @@
"ntfyAuthToken": "Auth-Token (optional)",
"ntfyAuthTokenHint": "Nur nötig bei privaten Topics mit Zugangsschutz.",
"webhook": "Webhook",
- "webhookDescription": "Benachrichtigungen per POST an eine beliebige URL — Gotify, Discord, Slack, Matrix oder Home Assistant.",
+ "webhookDescription": "Benachrichtigungen per POST an eine URL: Discord, Slack, Gotify, Home Assistant, n8n oder jedes Relay, das JSON annimmt.",
"webhookEnable": "Webhook aktivieren",
"webhookUrl": "Webhook-URL",
"webhookUrlHint": "Die vollständige URL für den POST. Eine Adresse im privaten Netz muss der Betreiber des Servers freigeben.",
@@ -5813,6 +6033,7 @@
"webhookHeaderValue": "Header-Wert (optional)",
"webhookFormat": "Nachrichtenformat",
"webhookFormatGeneric": "Generisches JSON",
+ "webhookFormatGenericHint": "Eine Discord- oder Slack-Webhook-URL einfach so einfügen. Home Assistant und n8n lesen die Felder title, message, eventType und priority.",
"webhookFormatGotify": "Gotify",
"webhookFormatGotifyHint": "Nutze die URL, die auf /message endet, und trage das App-Token als Header X-Gotify-Key ein.",
"email": "E-Mail",
@@ -5829,7 +6050,6 @@
"webPushSubscribed": "Push-Benachrichtigungen aktiviert!",
"webPushUnsubscribed": "Push-Benachrichtigungen deaktiviert.",
"webPushSubscribeFailed": "Aktivierung fehlgeschlagen",
- "kiInsightsDescription": "Optional: Hinterlege deinen OpenAI-Key für tägliche, automatische Auswertungen in den Insights.",
"codexConnected": "ChatGPT erfolgreich verbunden! Insights sind jetzt aktiv.",
"codexDisconnected": "ChatGPT-Verbindung getrennt.",
"codexConnectionFailed": "ChatGPT-Verbindung fehlgeschlagen. Bitte versuche es erneut.",
@@ -5839,7 +6059,7 @@
"rawDataWarning": "Ausführlicher Verlauf ist an: Jede Auswertung sendet zusätzlich Tagesdurchschnitte der letzten 90 Tage, Wochendurchschnitte für das Jahr davor und Monatsdurchschnitte bis zu 5 Jahre zurück.",
"regenerateInsights": "Reports neu generieren",
"regenerateSuccess": "Reports wurden neu generiert",
- "regenerateRateLimit": "Bitte warte — du hast das stündliche Limit für Analysen erreicht.",
+ "regenerateRateLimit": "Bitte warte, du hast das stündliche Limit für Analysen erreicht.",
"lastGeneratedAt": "Zuletzt generiert",
"ai": {
"chatgptConnectedBadge": "ChatGPT verbunden",
@@ -5849,11 +6069,11 @@
"deviceCodeHeading": "Verbindung auf chatgpt.com abschließen",
"deviceCodeStep1": "Öffne diesen Link auf einem beliebigen Gerät:",
"deviceCodeStep2": "Gib diesen Einmalcode ein:",
- "deviceCodeStep3": "Bestätige die Verbindung — diese Seite aktualisiert sich automatisch.",
+ "deviceCodeStep3": "Bestätige die Verbindung. Diese Seite aktualisiert sich automatisch.",
"deviceCodeCopy": "kopieren",
"deviceCodeWaiting": "Warte auf Bestätigung…",
"deviceCodeCancel": "Abbrechen",
- "oauthNotConfigured": "ChatGPT-OAuth ist auf dieser Instanz nicht konfiguriert — nutze stattdessen einen eigenen API-Key unten.",
+ "oauthNotConfigured": "ChatGPT-OAuth ist auf dieser Instanz nicht konfiguriert. Nutze stattdessen einen eigenen API-Key unten.",
"modelLabel": "Modell",
"modelOptionDefault": "— Standard —",
"modelOptionCustom": "Eigenes…",
@@ -5861,7 +6081,7 @@
"anthropicKeyLabel": "Anthropic API-Key",
"baseUrlLabel": "Base URL",
"localKeyLabel": "API-Key (optional)",
- "localDescription": "Hier funktioniert jeder OpenAI-kompatible Endpunkt — ein lokales Modell (Ollama, LM Studio) genauso wie ein Gateway wie LiteLLM, OpenRouter oder vLLM. Die Basis-URL endet auf /v1; der API-Schlüssel ist optional.",
+ "localDescription": "Hier funktioniert jeder OpenAI-kompatible Endpunkt: ein lokales Modell (Ollama, LM Studio) genauso wie ein Gateway wie LiteLLM, OpenRouter oder vLLM. Die Basis-URL endet auf /v1; der API-Schlüssel ist optional.",
"savedPreview": "(gespeichert {preview})",
"savedShort": "(gespeichert)",
"saveCta": "Speichern",
@@ -5877,7 +6097,7 @@
"admin-openai": "Admin-OpenAI"
},
"title": "Fallback-Kette",
- "description": "Wenn der primäre Provider ausfällt, läuft HealthLog die Kette der Reihe nach durch. Reihenfolge per Pfeile ändern, Schalter deaktiviert einen Eintrag ohne ihn zu entfernen.",
+ "description": "Fällt der aktive Anbieter aus, versucht HealthLog den nächsten eingeschalteten in dieser Reihenfolge.",
"moveUp": "Nach oben",
"moveDown": "Nach unten",
"removeFromChain": "Aus Kette entfernen",
@@ -5890,11 +6110,8 @@
"resetConfirmTitle": "Kette auf Standard zurücksetzen?",
"resetConfirmBody": "Die Kette wird auf Codex → OpenAI → Anthropic → Lokal → Admin-OpenAI zurückgesetzt. Gespeicherte Zugangsdaten bleiben erhalten."
},
- "providerCardTitle": "KI-Anbieter",
"activeProviderHeading": "Aktiver Provider",
- "activeProviderBody": "Wähle den Provider, der zuerst genutzt wird. Das Formular darunter konfiguriert ausschließlich den ausgewählten Provider; die Fallback-Kette unten regelt, was passiert, wenn er ausfällt.",
"activeProviderLabel": "Primärer Provider",
- "providerConfigTitle": "Provider-Konfiguration",
"providerSelect": {
"codex": "ChatGPT-Account (Codex)",
"openai": "OpenAI (eigener API-Key)",
@@ -5904,7 +6121,7 @@
"admin-openai": "Admin-OpenAI"
},
"compat": {
- "description": "Ein vollwertiger Anbieter für jeden Endpunkt, der die OpenAI-API /v1/chat/completions spricht: LiteLLM, OpenRouter, vLLM oder ein anderes Gateway. Die Basis-URL endet auf /v1, der API-Key ist optional, und das Modell ist das, was dein Gateway weiterleitet. Dein OpenAI-Key erreicht diesen Endpunkt nie — dieser Anbieter hat einen eigenen.",
+ "description": "Ein vollwertiger Anbieter für jeden Endpunkt, der die OpenAI-API /v1/chat/completions spricht: LiteLLM, OpenRouter, vLLM oder ein anderes Gateway. Die Basis-URL endet auf /v1, der API-Key ist optional, und das Modell ist das, was dein Gateway weiterleitet. Dein OpenAI-Key erreicht diesen Endpunkt nie, dieser Anbieter hat einen eigenen.",
"keyLabel": "API-Key (optional)",
"modelHint": "Der Modellname, den dein Gateway erwartet. Bleibt das Feld leer, wird das für deine anderen Anbieter gespeicherte Modell verwendet; ist auch das leer, wird dieser Anbieter übersprungen."
},
@@ -5940,25 +6157,24 @@
"notConfigured": "Der Betreiber hat keinen geteilten OpenAI-Key auf dieser Instanz konfiguriert."
},
"testProvider": "Aktiven Provider testen",
- "testSuccess": "OK — {provider} ({model})",
+ "testSuccess": "OK: {provider} ({model})",
"testFailedShort": "Test fehlgeschlagen: {message}",
- "testUnexpectedResponse": "Verbindung zum KI-Anbieter fehlgeschlagen — unerwartete Antwort vom Server.",
- "testReasonCredentials": "Der Anbieter hat die Anmeldedaten abgelehnt — melde dich in den KI-Einstellungen erneut an.",
- "testReasonRateLimited": "Der Anbieter hat die Anfrage gedrosselt — versuche es in Kürze erneut.",
+ "testUnexpectedResponse": "Verbindung zum KI-Anbieter fehlgeschlagen: unerwartete Antwort vom Server.",
+ "testReasonCredentials": "Der Anbieter hat die Anmeldedaten abgelehnt. Melde dich in den KI-Einstellungen erneut an.",
+ "testReasonRateLimited": "Der Anbieter hat die Anfrage gedrosselt. Versuche es in Kürze erneut.",
"testReasonServerError": "Der KI-Anbieter hat einen Serverfehler zurückgegeben.",
- "testReasonBadRequest": "Der Anbieter hat die Anfrage abgelehnt (HTTP {status}) — der Endpunkt hat geantwortet; das Problem liegt also am Anfrageformat oder Modellnamen, nicht an der Verbindung.",
+ "testReasonBadRequest": "Der Anbieter hat die Anfrage abgelehnt (HTTP {status}). Der Endpunkt hat geantwortet; das Problem liegt also am Anfrageformat oder Modellnamen, nicht an der Verbindung.",
"testReasonUnreachable": "Der KI-Anbieter war nicht erreichbar.",
"coachMemory": {
"title": "Woran sich der Coach erinnert",
"description": "Dauerhafte Angaben, die du dem Coach mitgeteilt hast.",
- "empty": "Der Coach hat noch keine Angaben gespeichert. Im Gespräch merkt er sich dauerhafte Details, die du teilst — deine Ziele, Beschwerden, Vorlieben — und du kannst sie hier jederzeit löschen.",
+ "empty": "Der Coach hat noch keine Angaben gespeichert. Im Gespräch merkt er sich dauerhafte Details, die du teilst (deine Ziele, Beschwerden, Vorlieben), und du kannst sie hier jederzeit löschen.",
"learnedPrefix": "gelernt {when}",
"categoryPreference": "Vorlieben",
"categoryCondition": "Beschwerden",
"categoryGoal": "Ziele",
"categoryConstraint": "Einschränkungen",
"categoryContext": "Kontext",
- "forget": "Vergessen",
"forgetAria": "Diese Angabe vergessen",
"forgetTitle": "Diese Notiz vergessen?",
"forgetBody": "Der Coach nutzt sie ab der nächsten Nachricht nicht mehr. Eine vergessene Notiz lässt sich nicht wiederherstellen.",
@@ -5968,18 +6184,19 @@
"forgetAllConfirmBody": "Dadurch werden alle gespeicherten Angaben entfernt. Der Coach beginnt beim nächsten Gespräch von vorn. Das lässt sich nicht rückgängig machen.",
"forgetAllConfirmAction": "Alles vergessen",
"cancel": "Abbrechen",
- "summaryNote": "Der Coach behält außerdem eine kurze, fortlaufende Zusammenfassung jedes Gesprächs. Dafür gibt es keine Einzelsteuerung — wenn du ein Gespräch löschst, wird auch dessen Zusammenfassung entfernt.",
+ "summaryNote": "Der Coach behält außerdem eine kurze, fortlaufende Zusammenfassung jedes Gesprächs. Dafür gibt es keine Einzelsteuerung. Wenn du ein Gespräch löschst, wird auch dessen Zusammenfassung entfernt.",
"loadError": "Konnte nicht laden, woran sich der Coach erinnert.",
"forgotToast": "Angabe vergessen.",
"forgotError": "Konnte diese Angabe nicht vergessen. Bitte erneut versuchen.",
"clearedToast": "{count} gespeicherte Angaben vergessen.",
"clearError": "Konnte die gespeicherten Angaben nicht löschen. Bitte erneut versuchen.",
- "detail": "Er nutzt sie, damit seine Antworten relevant bleiben. Entferne alles, was er besser vergessen sollte."
+ "detail": "Er nutzt sie, damit seine Antworten relevant bleiben. Entferne alles, was er besser vergessen sollte.",
+ "categoryMedication": "Medikamente"
},
"coachReminders": {
"title": "Deine Erinnerungen",
"description": "Dinge, um die du den Coach gebeten hast, später darauf zurückzukommen.",
- "empty": "Noch keine Erinnerungen. Bitte den Coach, dich an etwas zu erinnern — dann erscheint es hier.",
+ "empty": "Noch keine Erinnerungen. Bitte den Coach, dich an etwas zu erinnern, dann erscheint es hier.",
"loadError": "Konnte deine Erinnerungen nicht laden.",
"dueHeading": "Jetzt fällig",
"allHeading": "Gespeichert",
@@ -5988,7 +6205,7 @@
"markDone": "Erledigt",
"dismiss": "Verwerfen",
"deleteAria": "Diese Erinnerung löschen",
- "note": "Erinnerungen werden verschlüsselt gespeichert. Lösche eine jederzeit — sie wird aus dem entfernt, was der Coach sehen kann.",
+ "note": "Erinnerungen werden verschlüsselt gespeichert. Lösche eine jederzeit, sie wird aus dem entfernt, was der Coach sehen kann.",
"status": {
"proposed": "Vorgeschlagen",
"active": "Gespeichert",
@@ -6033,21 +6250,21 @@
"responseTimeoutInvalid": "Gib eine Zahl zwischen 10 und 600 Sekunden ein.",
"autoRead": {
"title": "Dokumente automatisch mit KI lesen",
- "subLabel": "Dokumente werden von deinem konfigurierten KI-Anbieter gelesen und indiziert, ohne dass du jedes Dokument einzeln bestätigen musst.",
- "honesty": "Wenn aktiviert, wird jedes hochgeladene Dokument an deinen konfigurierten KI-Anbieter gesendet, um gelesen, beschrieben und für die Suche indiziert zu werden — ohne Bestätigung pro Dokument. Das Einschalten wirkt außerdem rückwirkend: Bereits hochgeladene Dokumente ohne Zusammenfassung werden ebenfalls gesendet, bis zu 200 pro Einschaltvorgang. Das geschieht auf dem Server, und der Inhalt des Dokuments verlässt dabei diese Maschine in Richtung dieses Anbieters. Wenn dein konfigurierter Anbieter eine Abo-Verbindung ist (ein angemeldetes KI-Konto statt eines API-Schlüssels), gelten dessen eigene Datenschutzeinstellungen — Abo-Anbieter dürfen den Inhalt zur Verbesserung ihrer Modelle verwenden, und kein Auftragsverarbeitungsvertrag deckt das ab. Dokumente, die von einem selbst gehosteten lokalen Modell gelesen werden, verlassen deinen Server nie. Standardmäßig aus; aktiviere es nur, wenn du diesen Kompromiss für deine Dokumente akzeptierst.",
- "confirm": "Verstanden — aktivieren",
+ "subLabel": "Dein KI-Anbieter liest und indiziert neue Dokumente, ohne dass du jedes einzeln bestätigst.",
+ "honesty": "Wenn aktiviert, wird jedes hochgeladene Dokument an deinen konfigurierten KI-Anbieter gesendet, um gelesen, beschrieben und für die Suche indiziert zu werden, ohne Bestätigung pro Dokument. Das Einschalten wirkt außerdem rückwirkend: Bereits hochgeladene Dokumente ohne Zusammenfassung werden ebenfalls gesendet, bis zu 200 pro Einschaltvorgang. Das geschieht auf dem Server, und der Inhalt des Dokuments verlässt dabei diese Maschine in Richtung dieses Anbieters. Wenn dein konfigurierter Anbieter eine Abo-Verbindung ist (ein angemeldetes KI-Konto statt eines API-Schlüssels), gelten dessen eigene Datenschutzeinstellungen: Abo-Anbieter dürfen den Inhalt zur Verbesserung ihrer Modelle verwenden, und kein Auftragsverarbeitungsvertrag deckt das ab. Dokumente, die von einem selbst gehosteten lokalen Modell gelesen werden, verlassen deinen Server nie. Standardmäßig aus; aktiviere es nur, wenn du diesen Kompromiss für deine Dokumente akzeptierst.",
+ "confirm": "Verstanden, aktivieren",
"cancel": "Abbrechen"
},
"centralCodex": {
"title": "Gemeinsamen KI-Zugang des Servers nutzen",
- "subLabel": "Leite KI-Anfragen über die vom Betreiber für diesen Server eingerichtete Verbindung — zusätzlich zu deinen eigenen Anbietern als Rückfallebene.",
- "honesty": "Der gemeinsame Zugang ist ein einzelnes angemeldetes KI-Konto, das der Betreiber für alle auf diesem Server verbunden hat — nicht dein eigener Schlüssel. Anfragen darüber werden vom Anbieter standardmäßig zum Training genutzt und von keiner Auftragsverarbeitungsvereinbarung gedeckt, die Stunden- und Wochenlimits des Kontos teilst du dir mit allen anderen, die zugestimmt haben, und deine eigenen Anbieter werden weiterhin zuerst versucht. Aktiviere es nur, wenn du diesen Kompromiss akzeptierst.",
- "confirm": "Verstanden — Verbindung nutzen",
+ "subLabel": "KI-Anfragen laufen über die gemeinsame Verbindung des Servers, deine eigenen Anbieter dienen als Rückfall.",
+ "honesty": "Der gemeinsame Zugang ist ein einzelnes angemeldetes KI-Konto, das der Betreiber für alle auf diesem Server verbunden hat, nicht dein eigener Schlüssel. Anfragen darüber werden vom Anbieter standardmäßig zum Training genutzt und von keiner Auftragsverarbeitungsvereinbarung gedeckt, die Stunden- und Wochenlimits des Kontos teilst du dir mit allen anderen, die zugestimmt haben, und deine eigenen Anbieter werden weiterhin zuerst versucht. Aktiviere es nur, wenn du diesen Kompromiss akzeptierst.",
+ "confirm": "Verstanden, Verbindung nutzen",
"cancel": "Abbrechen"
},
"consent": {
"title": "KI-Einwilligung",
- "activeSince": "Erteilt am {date}. Deine Gesundheitsdaten dürfen für die KI-Funktionen an den eingestellten Anbieter gesendet werden.",
+ "activeSince": "Erteilt am {date}: Deine Gesundheitsdaten dürfen für die KI-Funktionen an den eingestellten Anbieter gehen.",
"withdrawn": "Widerrufen. Sofern keine andere Einwilligung sie abdeckt, nutzen Coach und Insights den KI-Zugang dieses Servers nicht mehr, und kein Online-Modell liest deine Dokumente. Mit deinem eigenen Schlüssel oder einem lokalen Modell laufen Coach und Insights weiter.",
"withdraw": "Einwilligung widerrufen",
"confirmBody": "Sofern keine andere Einwilligung sie noch abdeckt, endet sofort: der Coach und die Insights, wo sie den KI-Zugang dieses Servers nutzen, und das Lesen von Dokumenten mit einem Online-Modell. Mit deinem eigenen Schlüssel oder einem lokalen Modell laufen Coach und Insights weiter. Sofern keine andere Einwilligung die Insights noch abdeckt, werden die Notizen gelöscht, die ein Modell für dich geschrieben hat: Statusnotizen, das tägliche Briefing, von einem Modell geschriebene Zeitraum-Zusammenfassungen, Ankunftszeilen und Trainingsnotizen. Deine Coach-Gespräche, was sich der Coach gemerkt hat, deine Pläne und deine Dokument-Zusammenfassungen bleiben, und du kannst sie selbst lesen oder löschen.",
@@ -6058,7 +6275,7 @@
"error": "Der Widerruf konnte nicht gespeichert werden. Bitte versuche es erneut.",
"documentReading": {
"title": "Einwilligung zum Lesen von Dokumenten",
- "activeSince": "Erteilt am {date}. Dokumente, gescannte Laborbefunde und Medikamentenbeschreibungen dürfen zum Lesen an deinen KI-Dienst gesendet werden.",
+ "activeSince": "Erteilt am {date}: Dokumente, Laborscans und Medikamententexte dürfen zum Lesen an deinen KI-Dienst gehen.",
"confirmBody": "Dokumente, gescannte Laborbefunde und Medikamentenbeschreibungen werden nicht mehr zum Lesen gesendet. Nichts, was du bereits hast, wird gelöscht."
}
},
@@ -6080,7 +6297,21 @@
"title": "Coach-Pläne",
"description": "Die Pläne, die du mit dem Coach vereinbart hast, zum Lesen oder Entfernen.",
"open": "Pläne öffnen"
- }
+ },
+ "reasoning": {
+ "label": "Nachdenken",
+ "hint": "Reasoning-Modelle wie Gemma oder Qwen denken nach, bevor sie antworten. Schaltest du es aus, bleibt das ganze Antwortbudget für die Antwort selbst.",
+ "options": {
+ "default": "Standard",
+ "none": "Aus",
+ "low": "Niedrig",
+ "medium": "Mittel",
+ "high": "Hoch"
+ },
+ "coachNote": "Im Coach gilt die Denktiefe aus den Coach-Einstellungen."
+ },
+ "providerCardDescription": "Der Anbieter, den HealthLog zuerst fragt, mit Schlüssel und Modell.",
+ "runtimeTitle": "Prüfen und aktualisieren"
},
"dashboard": {
"units": {
@@ -6104,8 +6335,8 @@
"withingsClientId": "Client-ID",
"withingsClientSecret": "Client-Secret",
"withingsCredentialsSaved": "Zugangsdaten gespeichert",
- "withingsCredentialsSavedPlaceholder": "Gespeichert — neue eingeben zum Ersetzen",
- "withingsCredentialsSavedPlaceholderSecret": "Gespeichert — neues eingeben zum Ersetzen",
+ "withingsCredentialsSavedPlaceholder": "Gespeichert. Neue eingeben zum Ersetzen",
+ "withingsCredentialsSavedPlaceholderSecret": "Gespeichert. Neues eingeben zum Ersetzen",
"withingsSaveCredentials": "Speichern",
"configured": "Konfiguriert",
"withingsSync": "Jetzt synchronisieren",
@@ -6120,7 +6351,7 @@
},
"withingsFullSyncResult": "{count} Messwerte vollständig synchronisiert",
"withingsSyncFailed": "Sync fehlgeschlagen",
- "withingsOauthConnected": "Withings verbunden — der erste Sync startet im Hintergrund.",
+ "withingsOauthConnected": "Withings verbunden. Der erste Sync startet im Hintergrund.",
"withingsOauthFailed": "Withings-Verbindung fehlgeschlagen",
"withingsOauthError": {
"csrf1": "Die Sicherheitsprüfung ist fehlgeschlagen, weil das Browser-Cookie fehlte. Starte die Verbindung bitte erneut im selben Browser.",
@@ -6142,11 +6373,11 @@
"whoop": "WHOOP",
"whoopDescription": "Verbinde dein WHOOP-Band, um Erholung, Schlaf, Belastung und Workouts direkt in HealthLog zu synchronisieren.",
"whoopCredentials": "API-Zugangsdaten",
- "whoopClientId": "Client ID",
- "whoopClientSecret": "Client Secret",
+ "whoopClientId": "Client-ID",
+ "whoopClientSecret": "Client-Secret",
"whoopCredentialsSaved": "Zugangsdaten gespeichert",
- "whoopCredentialsSavedPlaceholder": "Gespeichert — neu eingeben zum Ersetzen",
- "whoopCredentialsSavedPlaceholderSecret": "Gespeichert — neu eingeben zum Ersetzen",
+ "whoopCredentialsSavedPlaceholder": "Gespeichert. Neu eingeben zum Ersetzen",
+ "whoopCredentialsSavedPlaceholderSecret": "Gespeichert. Neu eingeben zum Ersetzen",
"whoopSaveCredentials": "Speichern",
"whoopSync": "Jetzt synchronisieren",
"whoopFullSync": "Alle Daten synchronisieren",
@@ -6161,7 +6392,7 @@
"whoopConnect": "Mit WHOOP verbinden",
"whoopNoCredentials": "Bitte gib oben deine API-Zugangsdaten ein, um WHOOP zu verbinden.",
"whoopBackfillInProgress": "Dein WHOOP-Verlauf wird im Hintergrund importiert…",
- "whoopOauthConnected": "WHOOP verbunden — die erste Synchronisierung startet im Hintergrund.",
+ "whoopOauthConnected": "WHOOP verbunden. Die erste Synchronisierung startet im Hintergrund.",
"whoopOauthFailed": "WHOOP-Verbindung fehlgeschlagen",
"whoopOauthError": {
"csrf1": "Die Sicherheitsprüfung ist fehlgeschlagen, weil das Browser-Cookie fehlte. Bitte starte die Verbindung im selben Browser erneut.",
@@ -6175,14 +6406,14 @@
},
"fitbit": "Fitbit",
"fitbitTag": "Fitbit & Pixel",
- "fitbitDescription": "Verbinde Fitbit, um deine Fitbit- und Pixel-Watch-Werte zu synchronisieren — Gewicht, Herzfrequenz und mehr.",
+ "fitbitDescription": "Verbinde Fitbit, um deine Fitbit- und Pixel-Watch-Werte zu synchronisieren: Gewicht, Herzfrequenz und mehr.",
"fitbitExperimentalBadge": "Experimentell",
"fitbitCredentials": "API-Zugangsdaten",
"fitbitClientId": "Client-ID",
"fitbitClientSecret": "Client-Secret",
"fitbitCredentialsSaved": "Zugangsdaten gespeichert",
- "fitbitCredentialsSavedPlaceholder": "Gespeichert — neu eingeben zum Ersetzen",
- "fitbitCredentialsSavedPlaceholderSecret": "Gespeichert — neu eingeben zum Ersetzen",
+ "fitbitCredentialsSavedPlaceholder": "Gespeichert. Neu eingeben zum Ersetzen",
+ "fitbitCredentialsSavedPlaceholderSecret": "Gespeichert. Neu eingeben zum Ersetzen",
"fitbitSaveCredentials": "Speichern",
"fitbitSync": "Jetzt synchronisieren",
"fitbitFullSync": "Alle Daten synchronisieren",
@@ -6197,18 +6428,18 @@
"fitbitConnect": "Mit Fitbit verbinden",
"fitbitNoCredentials": "Bitte gib oben deine API-Zugangsdaten ein, um Fitbit zu verbinden.",
"fitbitBackfillInProgress": "Dein Fitbit-Verlauf wird im Hintergrund importiert…",
- "fitbitOauthConnected": "Fitbit verbunden — die erste Synchronisierung startet im Hintergrund.",
+ "fitbitOauthConnected": "Fitbit verbunden. Die erste Synchronisierung startet im Hintergrund.",
"fitbitOauthFailed": "Fitbit-Verbindung fehlgeschlagen",
"googleHealth": "Google Health",
"googleHealthTag": "Fitbit & Pixel",
"googleHealthBetaBadge": "Beta",
- "googleHealthDescription": "Verbinde Fitbit, Pixel Watch und Fitbit Air über dein Google-Konto — Gewicht, Herzfrequenz, Schlaf und Aktivität. Das ersetzt die klassische Fitbit-Verbindung, die Google im September 2026 abschaltet.",
+ "googleHealthDescription": "Verbinde Fitbit, Pixel Watch und Fitbit Air über dein Google-Konto: Gewicht, Herzfrequenz, Schlaf und Aktivität. Das ersetzt die klassische Fitbit-Verbindung, die Google im September 2026 abschaltet.",
"googleHealthCredentials": "API-Zugangsdaten",
"googleHealthClientId": "Client-ID",
"googleHealthClientSecret": "Client-Secret",
"googleHealthCredentialsSaved": "Zugangsdaten gespeichert",
- "googleHealthCredentialsSavedPlaceholder": "Gespeichert — zum Ersetzen neu eingeben",
- "googleHealthCredentialsSavedPlaceholderSecret": "Gespeichert — zum Ersetzen neu eingeben",
+ "googleHealthCredentialsSavedPlaceholder": "Gespeichert. Zum Ersetzen neu eingeben",
+ "googleHealthCredentialsSavedPlaceholderSecret": "Gespeichert. Zum Ersetzen neu eingeben",
"googleHealthSaveCredentials": "Speichern",
"googleHealthSync": "Jetzt synchronisieren",
"googleHealthSyncCheckStatus": "Status aktualisieren",
@@ -6242,9 +6473,9 @@
"googleHealthConnect": "Mit Google verbinden",
"googleHealthNoCredentials": "Bitte gib oben deine API-Zugangsdaten ein, um Google Health zu verbinden.",
"googleHealthBackfillInProgress": "Dein Google-Health-Verlauf wird im Hintergrund importiert…",
- "googleHealthReauthBanner": "Google hat die Verbindung ablaufen lassen — das passiert regelmäßig, solange deine OAuth-App im Testmodus bleibt. Verbinde neu, um die Synchronisierung fortzusetzen.",
+ "googleHealthReauthBanner": "Google hat die Verbindung ablaufen lassen. Das passiert regelmäßig, solange deine OAuth-App im Testmodus bleibt. Verbinde neu, um die Synchronisierung fortzusetzen.",
"googleHealthReconnect": "Neu verbinden",
- "googleHealthOauthConnected": "Google Health verbunden — die erste Synchronisierung startet im Hintergrund.",
+ "googleHealthOauthConnected": "Google Health verbunden. Die erste Synchronisierung startet im Hintergrund.",
"googleHealthOauthFailed": "Verbindung zu Google Health fehlgeschlagen",
"googleHealthOauthError": {
"csrf1": "Die Sicherheitsprüfung ist fehlgeschlagen, weil das Browser-Cookie fehlte. Bitte starte die Verbindung im selben Browser erneut.",
@@ -6279,10 +6510,10 @@
},
"integrationPill": {
"connected": "Verbunden",
- "errorReconnect": "Fehler — neu verbinden",
+ "errorReconnect": "Fehler: neu verbinden",
"warningServerError": "Sync fehlgeschlagen",
"failureCount": "{count}/{threshold}",
- "parkedReconnect": "Pausiert — neu verbinden",
+ "parkedReconnect": "Pausiert: neu verbinden",
"notConnected": "Nicht verbunden",
"justNow": "gerade eben",
"minutesAgo": "vor {count} min",
@@ -6297,7 +6528,7 @@
},
"integrationFreshness": {
"title": "Empfangene Daten",
- "summary": "{count} Datentypen · neuestes {relative}",
+ "summary": "{count} Datentypen, neuestes {relative}",
"quiet": "{count} still",
"workouts": "Workouts"
},
@@ -6311,31 +6542,33 @@
"fitbitViewData": "Schlaf ansehen",
"googleHealthViewData": "Schritte ansehen",
"apiTokens": "API-Tokens",
- "apiTokensDescription": "API-Tokens für die externe Medikamenten-Einnahme prüfen und widerrufen.",
+ "apiTokensDescription": "Tokens für Messwerte, Trainings, Dokumente und Medikamenten-Einnahme prüfen und widerrufen.",
"measurementsToken": {
"title": "Messwert-Tokens",
"description": "Erzeuge ein Token, damit eine Waage, eine Uhr oder deine Hausautomatisierung Werte eintragen kann.",
"detail": "Das Token wird nur einmal angezeigt.",
- "scopeNote": "Es kann ausschließlich neue Werte in deine eigene Akte eintragen — deine Daten nicht lesen, nichts ändern oder löschen und kein weiteres Token erstellen. Werte werden als von einem externen Gerät gekennzeichnet, damit du sie später erkennst, und lassen sich jederzeit korrigieren.",
- "createFailed": "Das Token konnte nicht erstellt werden. Bitte versuche es erneut."
+ "scopeNote": "Es kann ausschließlich neue Werte in deine eigene Akte eintragen. Es kann deine Daten nicht lesen, nichts ändern oder löschen und kein weiteres Token erstellen. Werte werden als von einem externen Gerät gekennzeichnet, damit du sie später erkennst, und lassen sich jederzeit korrigieren.",
+ "createFailed": "Das Token konnte nicht erstellt werden. Bitte versuche es erneut.",
+ "namePlaceholder": "Name, z. B. Home Assistant"
},
"workoutsToken": {
"title": "Training-Tokens",
"description": "Erzeuge ein Token, damit eine Uhren-Brücke oder ein Sync-Skript deine Trainings eintragen kann.",
"detail": "Das Token wird nur einmal angezeigt.",
"scopeNote": "Es kann ausschließlich neue Trainings in deine eigene Akte eintragen. Es kann deine Daten nicht lesen, nichts ändern oder löschen und kein weiteres Token erstellen. Trainings, die darüber kommen, werden als von einem externen Gerät gekennzeichnet. Hat dein Telefon dieselbe Einheit aufgezeichnet, siehst du die Aufzeichnung des Telefons.",
- "createFailed": "Das Token konnte nicht erstellt werden. Bitte versuche es erneut."
+ "createFailed": "Das Token konnte nicht erstellt werden. Bitte versuche es erneut.",
+ "namePlaceholder": "Name, z. B. Garmin"
},
"documentsToken": {
"title": "Dokument-Tokens",
"description": "Erzeuge ein Token, damit Paperless-ngx, Papra oder das Import-Skript Dokumente in deine Ablage legen können.",
"detail": "Das Token wird nur einmal angezeigt.",
"scopeNote": "Es kann ausschließlich neue Dokumente in deine eigene Ablage legen. Es kann nichts auflisten, öffnen, herunterladen, ändern oder löschen und kein weiteres Token erstellen. Zurück bekommt es nur, ob eine Datei gespeichert wurde, nicht wie du sie abgelegt hast.",
- "createFailed": "Das Token konnte nicht erstellt werden. Bitte versuche es erneut."
+ "createFailed": "Das Token konnte nicht erstellt werden. Bitte versuche es erneut.",
+ "namePlaceholder": "Name, z. B. Paperless-ngx"
},
- "tokenMintMovedDescription": "Tokens werden dort ausgestellt, wo sie gelten: oben für Messwerte und Dokumente, oder im API-Endpoint eines Medikaments für genau dieses Medikament.",
- "tokenNamePlaceholder": "Token-Name (z.B. iPhone Shortcut)",
- "tokenCreated": "Token erstellt — kopiere ihn jetzt! Er wird nicht erneut angezeigt.",
+ "tokenMintMovedDescription": "Tokens werden dort ausgestellt, wo sie gelten: oben für Messwerte, Trainings und Dokumente, oder im API-Endpoint eines Medikaments für genau dieses Medikament.",
+ "tokenCreated": "Token erstellt. Kopiere ihn jetzt! Er wird nicht erneut angezeigt.",
"tokenCopy": "Token kopieren",
"tokenCopied": "Token in die Zwischenablage kopiert",
"tokenCopyFailed": "Token konnte nicht kopiert werden",
@@ -6374,7 +6607,7 @@
"deleteAccountCta": "Konto löschen",
"deleteAccountConfirmTitle": "Konto endgültig löschen?",
"deleteAccountConfirmDescription": "Dein Konto, alle Gesundheitsdaten, Passkeys, Sessions und Audit-Einträge werden unwiderruflich gelöscht. Du wirst danach automatisch abgemeldet.",
- "deleteAccountSuccess": "Konto wurde gelöscht — du wirst abgemeldet.",
+ "deleteAccountSuccess": "Konto wurde gelöscht. Du wirst abgemeldet.",
"deleteAccountFailed": "Konto konnte nicht gelöscht werden",
"deleteAccountFinal": "Endgültig löschen",
"saved": "Gespeichert",
@@ -6397,7 +6630,7 @@
"testing": "Teste…",
"ok": "Verbunden (Latenz {latency} ms)",
"errors": {
- "credentials_rejected": "Zugangsdaten abgelehnt — Token prüfen",
+ "credentials_rejected": "Zugangsdaten abgelehnt. Token prüfen",
"rate_limited": "Vom Upstream rate-limitiert",
"timeout": "Request hat das Zeitlimit überschritten",
"upstream_error": "Upstream hat einen Fehler gemeldet",
@@ -6408,7 +6641,7 @@
"private_origin_not_approved": "Ziel im privaten Netz. Der Betreiber muss diesen Origin in NOTIFICATION_PRIVATE_ORIGINS eintragen",
"private_origin_not_grantable": "Diese Adresse ist Link-local, eine Metadaten- oder eine unspezifizierte Adresse; keine Betreiber-Freigabe kann sie öffnen. Bitte die Adresse verwenden, auf der das Relay lauscht",
"url_invalid": "URL ist ungültig",
- "redirected": "Endpunkt leitet weiter — URL prüfen",
+ "redirected": "Endpunkt leitet weiter. URL prüfen",
"endpoint_not_found": "Endpunkt unter der URL nicht gefunden",
"credentials_unreadable": "Zugangsdaten nicht entschlüsselbar",
"upstream_invalid_json": "Upstream-Antwort ist kein JSON",
@@ -6419,7 +6652,7 @@
},
"notificationStatus": {
"title": "Zustellstatus",
- "description": "Status je Kanal — „Automatisch deaktiviert“ heißt: nach dauerhaften Fehlern gestoppt.",
+ "description": "Status je Kanal; „Automatisch deaktiviert“ heißt, dass nach dauerhaften Fehlern gestoppt wurde.",
"emptyDescription": "Richte unter Einstellungen › Integrationen › Zustellkanäle einen Kanal ein.",
"stateActive": "Aktiv",
"stateAutoDisabled": "Automatisch deaktiviert",
@@ -6445,7 +6678,7 @@
},
"healthRecord": {
"title": "Gesundheitsakte exportieren",
- "description": "Eine vollständige Akte für die ärztliche Vorlage — als PDF, als FHIR oder beides.",
+ "description": "Eine vollständige Akte für die ärztliche Vorlage, als PDF, als FHIR oder beides.",
"format": "Format",
"format_pdf": "PDF",
"format_fhir": "Strukturiert (FHIR)",
@@ -6477,7 +6710,7 @@
"createTitle": "Neuer Freigabe-Link",
"createDescription": "Der Link ist schreibgeschützt und zeitlich begrenzt.",
"label": "Bezeichnung",
- "labelPlaceholder": "z. B. Dr. Schmidt — Jahres-Check-up",
+ "labelPlaceholder": "z. B. Dr. Schmidt, Jahres-Check-up",
"range": "Tage Verlauf",
"rangeHint": "Wie weit das geteilte Zeitfenster zurückreicht.",
"expiry": "Läuft ab in (Tagen)",
@@ -6497,7 +6730,7 @@
"statusRevoked": "Widerrufen",
"statusExpired": "Abgelaufen",
"inactiveTitle": "Widerrufen und abgelaufen ({count})",
- "shownOnce": "Nur einmal sichtbar. Erfasse QR-Code und Passphrase jetzt — beide lassen sich später nicht wiederherstellen.",
+ "shownOnce": "Nur einmal sichtbar. Erfasse QR-Code und Passphrase jetzt. Beide lassen sich später nicht wiederherstellen.",
"qrAlt": "QR-Code mit Freigabelink und Passphrase",
"linkLabel": "Link",
"passphraseLabel": "Passphrase",
@@ -6508,12 +6741,12 @@
"attachTitle": "Dokumente anhängen",
"attachButton": "Dokumente auswählen",
"attachCount": "{count} von {max} ausgewählt",
- "attachFrozen": "Die Dokumentenauswahl ist nach dem Erstellen des Links fest — Dokumente lassen sich danach weder hinzufügen noch entfernen.",
+ "attachFrozen": "Die Dokumentenauswahl ist nach dem Erstellen des Links fest. Dokumente lassen sich danach weder hinzufügen noch entfernen.",
"attachRemove": "{title} entfernen",
"exifNote": "Kamera-Metadaten (EXIF/GPS) werden beim Teilen aus JPEG-, PNG- und WebP-Bildern entfernt. PDF-, TIFF-, HEIC- und Office-Dateien werden unverändert übernommen.",
"documentCount": "Angehängte Dokumente: {count}",
"qrScanTitle": "Zum Öffnen auf einem anderen Gerät scannen",
- "qrScanHint": "Richte eine Handykamera auf den Code, um die geteilte Akte zu öffnen — die Passphrase ist bereits enthalten.",
+ "qrScanHint": "Richte eine Handykamera auf den Code, um die geteilte Akte zu öffnen. Die Passphrase ist bereits enthalten.",
"pickerTitle": "Dokumente anhängen",
"pickerDescription": "Durchsuche deinen Tresor und wähle Dokumente zum Teilen. Bis zu {max}.",
"pickerSearch": "Dokumente suchen",
@@ -6526,7 +6759,8 @@
"statusNeedsReselect": "Inhalt neu wählen",
"reselectReason": "Dieser Link lieferte einen Umfang, den niemand gewählt hatte, und wurde deshalb geschlossen. Lege einen neuen an und wähle den Inhalt.",
"reselectAction": "Ersatz anlegen",
- "createDetail": "Der Token wird nur einmal bei der Erstellung angezeigt — sichere ihn dann; er lässt sich nicht wiederherstellen."
+ "createDetail": "Der Token wird nur einmal bei der Erstellung angezeigt. Sichere ihn dann; er lässt sich nicht wiederherstellen.",
+ "activeDescription": "Deine geteilten Links, wann sie ablaufen und wie oft sie geöffnet wurden."
},
"genderOther": "Divers",
"nightscout": "Nightscout",
@@ -6534,10 +6768,10 @@
"nightscoutDescription": "Verbinde deine eigene Nightscout-Seite, um kontinuierliche Glukosewerte ins Glukose-Panel zu synchronisieren.",
"nightscoutViewData": "Glukose ansehen",
"nightscoutUrl": "Nightscout-URL",
- "nightscoutUrlSavedPlaceholder": "Gespeichert — neue URL eingeben zum Ersetzen",
+ "nightscoutUrlSavedPlaceholder": "Gespeichert. Neue URL eingeben zum Ersetzen",
"nightscoutToken": "API-Token",
"nightscoutTokenOptional": "Optional bei einer öffentlichen Seite",
- "nightscoutTokenSavedPlaceholder": "Gespeichert — neuen Token eingeben zum Ersetzen",
+ "nightscoutTokenSavedPlaceholder": "Gespeichert. Neuen Token eingeben zum Ersetzen",
"nightscoutTokenHelp": "Füge einen Zugriffstoken aus deinen Nightscout-Admin-Tools ein. Leer lassen, wenn deine Seite öffentlich lesbar ist.",
"nightscoutPrivateOperatorTitle": "Der Zugriff auf ein privates Nightscout benötigt die Freigabe der Serveradministration.",
"nightscoutPrivateOperatorDescription": "Bitte die Serveradministration, genau diesen Nightscout-Ursprung freizugeben.",
@@ -6555,10 +6789,10 @@
"polarDescription": "Verbinde dein Polar-Konto, um Nightly-Recharge-Erholung, Schlafphasen, tägliche Aktivität und Trainingseinheiten zu synchronisieren.",
"polarViewData": "Schlaf & Erholung ansehen",
"polarCredentials": "API-Zugangsdaten",
- "polarClientId": "Client ID",
- "polarClientSecret": "Client Secret",
+ "polarClientId": "Client-ID",
+ "polarClientSecret": "Client-Secret",
"polarCredentialsSaved": "Zugangsdaten gespeichert",
- "polarCredentialsSavedPlaceholder": "Gespeichert — neu eingeben zum Ersetzen",
+ "polarCredentialsSavedPlaceholder": "Gespeichert. Neu eingeben zum Ersetzen",
"polarSaveCredentials": "Speichern",
"polarUnavailable": "Polar ist auf diesem Server nicht konfiguriert. Füge oben deine eigenen Polar-OAuth-Zugangsdaten hinzu oder bitte deinen Administrator, die gemeinsame App zu konfigurieren.",
"polarConnect": "Polar verbinden",
@@ -6568,7 +6802,7 @@
"polarDisconnect": "Trennen",
"polarDisconnectTitle": "Polar trennen?",
"polarDisconnectDescription": "Künftige Synchronisierungen stoppen. Bereits importierte Werte bleiben in deinem Verlauf.",
- "polarOauthConnected": "Polar verbunden — die erste Synchronisierung startet im Hintergrund.",
+ "polarOauthConnected": "Polar verbunden. Die erste Synchronisierung startet im Hintergrund.",
"polarOauthFailed": "Polar-Verbindung fehlgeschlagen",
"polarOauthError": {
"csrf1": "Die Sicherheitsprüfung ist fehlgeschlagen, weil das Browser-Cookie fehlte. Bitte starte die Verbindung im selben Browser erneut.",
@@ -6584,10 +6818,10 @@
"ouraDescription": "Verbinde dein Oura-Konto, um Readiness, Schlafphasen, Herzfrequenzvariabilität und tägliche Aktivität zu synchronisieren.",
"ouraViewData": "Schlaf & Erholung ansehen",
"ouraCredentials": "API-Zugangsdaten",
- "ouraClientId": "Client ID",
- "ouraClientSecret": "Client Secret",
+ "ouraClientId": "Client-ID",
+ "ouraClientSecret": "Client-Secret",
"ouraCredentialsSaved": "Zugangsdaten gespeichert",
- "ouraCredentialsSavedPlaceholder": "Gespeichert — neu eingeben zum Ersetzen",
+ "ouraCredentialsSavedPlaceholder": "Gespeichert. Neu eingeben zum Ersetzen",
"ouraSaveCredentials": "Speichern",
"ouraUnavailable": "Oura ist auf diesem Server nicht konfiguriert. Füge oben deine eigenen Oura-OAuth-Zugangsdaten hinzu oder bitte deinen Administrator, die gemeinsame App zu konfigurieren.",
"ouraConnect": "Oura verbinden",
@@ -6597,7 +6831,7 @@
"ouraDisconnect": "Trennen",
"ouraDisconnectTitle": "Oura trennen?",
"ouraDisconnectDescription": "Künftige Synchronisierungen stoppen. Bereits importierte Werte bleiben in deinem Verlauf.",
- "ouraOauthConnected": "Oura verbunden — die erste Synchronisierung startet im Hintergrund.",
+ "ouraOauthConnected": "Oura verbunden. Die erste Synchronisierung startet im Hintergrund.",
"ouraOauthFailed": "Oura-Verbindung fehlgeschlagen",
"ouraOauthError": {
"csrf1": "Die Sicherheitsprüfung ist fehlgeschlagen, weil das Browser-Cookie fehlte. Bitte starte die Verbindung im selben Browser erneut.",
@@ -6607,6 +6841,7 @@
"nocreds": "Oura ist auf diesem Server nicht konfiguriert. Bitte deinen Administrator, die Oura-OAuth-Zugangsdaten zu hinterlegen.",
"token": "Der Token-Austausch mit Oura ist fehlgeschlagen. Bitte starte die Verbindung erneut.",
"rate_limited": "Zu viele Verbindungsversuche. Warte einen Moment und versuche es erneut.",
+ "issuer": "Die Anmeldeantwort kam nicht vom Autorisierungsserver von Oura, deshalb wurde die Verbindung zu deiner Sicherheit abgebrochen. Bitte starte die Verbindung erneut. Wenn das wieder passiert, sag deinem Administrator Bescheid.",
"generic": "Unbekannter Fehler bei der Verbindung. Bitte starte die Verbindung erneut."
},
"integrationSetupGuide": "Einrichtungsanleitung",
@@ -6618,10 +6853,17 @@
"savedOn": "Coach ist aktiviert.",
"savedOff": "Coach ist ausgeblendet.",
"saveError": "Speichern fehlgeschlagen. Bitte erneut versuchen."
+ },
+ "memory": {
+ "edit": "Bearbeiten",
+ "source": {
+ "user": "von dir",
+ "coach": "vom Coach"
+ }
}
},
"integrationRedirectGuide": {
- "intro": "Die Verbindung läuft über deine eigene {provider}-Entwickler-App — etwa 5 Minuten Einrichtung:",
+ "intro": "Die Verbindung läuft über deine eigene {provider}-Entwickler-App. Die Einrichtung dauert etwa 5 Minuten:",
"step1": "Erstelle eine App im {provider}-Entwicklerportal.",
"step2": "Setze deren Weiterleitungs-/Callback-URL auf:",
"step3": "Trage die dort erhaltene Client-ID und das Client-Secret oben ein."
@@ -6681,7 +6923,7 @@
},
"recovery": {
"saveNow": "Speichere diese Wiederherstellungscodes jetzt",
- "saveNowHint": "Jeder Code funktioniert einmal. Sie werden nur dieses eine Mal angezeigt — bewahre sie sicher auf. Wenn du deine Authenticator-App verlierst, ist ein Wiederherstellungscode der einzige Weg zurück.",
+ "saveNowHint": "Jeder Code funktioniert einmal. Sie werden nur dieses eine Mal angezeigt, bewahre sie sicher auf. Wenn du deine Authenticator-App verlierst, ist ein Wiederherstellungscode der einzige Weg zurück.",
"download": "Herunterladen",
"remainingOne": "{count} Wiederherstellungscode übrig.",
"remainingOther": "{count} Wiederherstellungscodes übrig.",
@@ -6709,7 +6951,7 @@
},
"passkeyNudge": {
"title": "Füge einen Passkey für schnellere Anmeldung hinzu",
- "body": "Mit Passkeys meldest du dich per Fingerabdruck, Gesicht oder Geräte-PIN an — ohne Passwort zum Merken."
+ "body": "Mit Passkeys meldest du dich per Fingerabdruck, Gesicht oder Geräte-PIN an, ohne Passwort zum Merken."
},
"trustedDevices": {
"title": "Vertrauenswürdige Geräte",
@@ -6740,17 +6982,16 @@
"privacy": {
"encryption": {
"title": "Verschlüsselt gespeichert",
- "summary": "Deine sensiblen Daten werden mit {algorithm} verschlüsselt gespeichert — über {columns} Felder in {models} Datensatztypen.",
+ "summary": "Deine sensiblen Daten werden mit {algorithm} verschlüsselt gespeichert: über {columns} Felder in {models} Datensatztypen.",
"statement": "Deine sensiblen Daten werden mit AES-256-GCM verschlüsselt gespeichert."
},
"stored": {
"title": "Was gespeichert wird",
- "measurements": "Messwerte (Gewicht, Blutdruck, Puls, Glukose, Schlaf, Körperzusammensetzung)",
- "medications": "Medikamente und Einnahmeverlauf",
- "moodLabs": "Stimmungseinträge und Laborwerte",
- "coach": "Coach-Unterhaltungen",
+ "measurements": "Messwerte: Gewicht, Blutdruck, Puls und jeder weitere Wert, den du oder ein Gerät erfasst",
"integrations": "Integrations-Tokens (verschlüsselt)",
- "security": "Konto- und Sicherheitsaktivität (Sitzungen, Anmeldeprotokoll)"
+ "security": "Konto- und Sicherheitsaktivität (Sitzungen, Anmeldeprotokoll)",
+ "profile": "Dein Profil, deine Anamnese und die Angaben für die Gesundheitsakte",
+ "modules": "Einträge der Module: {list}"
},
"retention": {
"title": "Aufbewahrung",
@@ -6762,7 +7003,7 @@
},
"export": {
"title": "Meine Daten herunterladen",
- "description": "Deine Datensätze als CSV oder JSON — oder eine vollständige Akte für die Praxis.",
+ "description": "Deine Datensätze als CSV oder JSON oder eine vollständige Akte für die Praxis.",
"openExport": "Export & Backup",
"openHealthRecord": "Gesundheitsakte"
},
@@ -6781,13 +7022,11 @@
"mcp": {
"enableTitle": "Remote-MCP-Endpunkt",
"enableDescription": "Erlaube einem MCP-fähigen Assistenten (Claude.ai, ChatGPT), deine Gesundheitsdaten über ein begrenztes Token zu lesen.",
- "enabled": "Aktiv",
- "disabled": "Inaktiv",
"enableToggleLabel": "MCP-Endpunkt aktivieren",
"endpointLabel": "Connector-URL",
"connectDescription": "Füge diese URL in die Einrichtung des eigenen Connectors deines Assistenten ein und autorisiere dich mit dem Token unten oder melde dich bei Aufforderung an.",
"tokensTitle": "Connector-Tokens",
- "tokensDescription": "Erzeuge ein Token für einen Assistenten — nur Lesezugriff, außer du erlaubst unten das Eintragen.",
+ "tokensDescription": "Erzeuge ein Token für einen Assistenten, nur mit Lesezugriff, außer du erlaubst unten das Eintragen.",
"scopeNote": "Neue Tokens können nur deine eigenen Daten lesen. Schreibzugriff (health:write) vergibst du pro Token über den Schalter unten. Admin-Funktionen erreicht ein Token nie.",
"writeScopeToggleLabel": "Eintragen erlauben (health:write)",
"writeScopeNote": "Fügt den Scope health:write hinzu, damit der Assistent neue Messwerte und Stimmungseinträge in dein Konto eintragen kann. Jeder Schreibvorgang wird zuvor bestätigt. Das Token kann weiterhin keine vorhandenen Einträge löschen oder ändern, keine Medikamente bearbeiten und keine REST- oder Admin-Funktionen erreichen. Für reinen Lesezugriff ausgeschaltet lassen.",
@@ -6801,7 +7040,8 @@
"connectionRevokeDescription": "Dies beendet den Zugriff des Assistenten sofort und verhindert eine Erneuerung. Für eine erneute Verbindung ist eine neue Autorisierung nötig.",
"connectionRevokeFailed": "Assistent konnte nicht getrennt werden.",
"tokensDetail": "Das Token wird nur einmal angezeigt.",
- "connectionsDetail": "Das Widerrufen einer Verbindung beendet den Zugriff sofort, einschließlich der Möglichkeit zur Erneuerung."
+ "connectionsDetail": "Das Widerrufen einer Verbindung beendet den Zugriff sofort, einschließlich der Möglichkeit zur Erneuerung.",
+ "tokenNamePlaceholder": "Name, z. B. Claude Desktop"
},
"strava": "Strava",
"stravaDescription": "Verbinde dein Strava-Konto, um deine Aktivitäten in deinen Trainingsverlauf zu übernehmen.",
@@ -6810,7 +7050,7 @@
"stravaClientId": "Client-ID",
"stravaClientSecret": "Client-Secret",
"stravaCredentialsSaved": "Zugangsdaten gespeichert",
- "stravaCredentialsSavedPlaceholder": "Gespeichert — neue eingeben zum Ersetzen",
+ "stravaCredentialsSavedPlaceholder": "Gespeichert. Neue eingeben zum Ersetzen",
"stravaSaveCredentials": "Speichern",
"stravaUnavailable": "Strava ist auf diesem Server nicht konfiguriert. Füge oben deine eigenen Strava-API-Zugangsdaten hinzu oder bitte deinen Administrator, die gemeinsame App zu konfigurieren.",
"stravaConnect": "Strava verbinden",
@@ -6820,7 +7060,7 @@
"stravaDisconnect": "Trennen",
"stravaDisconnectTitle": "Strava trennen?",
"stravaDisconnectDescription": "Künftige Synchronisierungen stoppen. Bereits importierte Aktivitäten bleiben in deinem Verlauf.",
- "stravaOauthConnected": "Strava verbunden — die erste Synchronisierung startet im Hintergrund.",
+ "stravaOauthConnected": "Strava verbunden. Die erste Synchronisierung startet im Hintergrund.",
"stravaOauthFailed": "Strava-Verbindung fehlgeschlagen",
"stravaOauthError": {
"csrf1": "Die Sicherheitsprüfung ist fehlgeschlagen, weil das Browser-Cookie fehlte. Bitte starte die Verbindung im selben Browser erneut.",
@@ -6834,7 +7074,7 @@
},
"appleHealth": {
"title": "Apple Health",
- "description": "Die Live-Synchronisierung erfolgt über die HealthLog-iOS-App — nicht über eine Web- oder OAuth-Verbindung.",
+ "description": "Die Live-Synchronisierung erfolgt über die HealthLog-iOS-App, nicht über eine Web- oder OAuth-Verbindung.",
"status": {
"unavailable": "Apple-Health-Status nicht verfügbar"
},
@@ -6848,26 +7088,33 @@
"title": "Sync-Fortschritt",
"receivedLabel": "Empfangene Datensätze",
"oldestLabel": "Älteste erreichte Messung",
- "flowing": "In der letzten Stunde sind Daten angekommen — die iPhone-App liefert weiter.",
- "waiting": "Warten auf weitere Daten von der iPhone-App. Die Übertragung läuft weiter, wenn die App geöffnet ist oder iOS ihr Hintergrundzeit gewährt — eine ruhige Phase während des ersten großen Abgleichs ist normal, kein Fehler."
+ "flowing": "In der letzten Stunde sind Daten angekommen. Die iPhone-App liefert weiter.",
+ "waiting": "Warten auf weitere Daten von der iPhone-App. Die Übertragung läuft weiter, wenn die App geöffnet ist oder iOS ihr Hintergrundzeit gewährt. Eine ruhige Phase während des ersten großen Abgleichs ist normal, kein Fehler."
},
"delivery": {
"title": "Übermittlung",
"lastTriggerLabel": "Letzte Synchronisierung kam",
"lastBackgroundLabel": "Letzte Hintergrund-Synchronisierung",
- "backgroundNever": "Nie — bisher kam jede Synchronisierung bei geöffneter App an",
+ "backgroundNever": "Nie: bisher kam jede Synchronisierung bei geöffneter App an",
"noSyncYet": "Es ist noch keine Synchronisierung angekommen, daher gibt es hier nichts zu berichten.",
"trigger": {
"foreground": "bei geöffneter App",
"background": "im Hintergrund",
"push": "durch einen Push-Weckruf",
- "unknown": "Auslöser von der App nicht gemeldet"
+ "unknown": "Auslöser von der App nicht gemeldet",
+ "manual": "über „Alles synchronisieren“"
}
+ },
+ "freshness": {
+ "title": "Nach Datentyp",
+ "lastReceived": "Zuletzt empfangen {relative}",
+ "lastNewValue": "Letzter neuer Wert {relative}",
+ "never": "Noch nicht empfangen"
}
},
"garminInfo": {
"title": "Garmin",
- "body": "Garmin hat hier keine direkte Verbindung. Verbinde über Apple Health (iOS) oder Google Health Connect (Android). Garmins proprietäre Werte — Body Battery, Training Load, HRV-Status, VO₂max — werden von Garmin an keine der beiden Plattformen weitergegeben.",
+ "body": "Garmin hat hier keine direkte Verbindung. Verbinde über Apple Health (iOS) oder Google Health Connect (Android). Garmins proprietäre Werte (Body Battery, Training Load, HRV-Status, VO₂max) werden von Garmin an keine der beiden Plattformen weitergegeben.",
"docsLink": "Zur Anleitung"
},
"glucoseReference": {
@@ -6923,6 +7170,12 @@
"description": "Damit werden deine Gesundheitsdaten weitergegeben oder exportiert. Bestätige, dass du es bist, um fortzufahren.",
"signInAgain": "Melde dich neu an und versuche es dann noch einmal. Dein Konto hat kein Passwort und keinen Passkey, den du hier bestätigen könntest.",
"failed": "Das hat nicht geklappt. Prüfe deine Eingabe und versuche es noch einmal."
+ },
+ "passwordTitle": "Passwort",
+ "tokenNameLabel": "Token-Name",
+ "displayCard": {
+ "title": "Darstellung",
+ "description": "Sprache, Einheiten und Formate, gespeichert, sobald du sie änderst."
}
},
"admin": {
@@ -6942,33 +7195,33 @@
"loadingStatus": "Status wird geladen…",
"systemStatusLoadError": "System-Status konnte nicht geladen werden.",
"systemStatusRetry": "Erneut versuchen",
- "adminSettingsLoadError": "Admin-Einstellungen konnten nicht geladen werden. Schalter unten zeigen evtl. nicht den Live-Zustand — Seite neu laden.",
+ "adminSettingsLoadError": "Admin-Einstellungen konnten nicht geladen werden. Schalter unten zeigen evtl. nicht den Live-Zustand. Seite neu laden.",
"hostMetrics": {
"title": "Host-Auslastung",
"load1": "Last (1m)",
"memUsedPercent": "Speicher",
"diskBusyPercent": "Disk-I/O",
"last2hours": "Letzte 2 Stunden",
- "empty": "Erfassung läuft — Daten erscheinen in 1–2 Minuten.",
+ "empty": "Erfassung läuft. Daten erscheinen in 1–2 Minuten.",
"loadError": "Host-Metriken konnten nicht geladen werden. Bitte die Seite neu laden."
},
"aiServerKey": {
"title": "KI-Server-Schlüssel",
"description": "Ein Betreiber-Schlüssel für Konten, die keinen eigenen hinterlegt haben.",
- "byokHint": "Der eigene Schlüssel (BYOK, ChatGPT-Login oder lokaler Provider) hat immer Vorrang — der Server-Schlüssel ist die letzte Stufe der Provider-Kette.",
+ "byokHint": "Der eigene Schlüssel (BYOK, ChatGPT-Login oder lokaler Provider) hat immer Vorrang. Der Server-Schlüssel ist die letzte Stufe der Provider-Kette.",
"consentHint": "Anfragen über den Server-Schlüssel erfordern die aktive KI-Einwilligung der Person; ohne Einwilligungsbeleg wird nichts gesendet.",
"baseUrlHint": "Die Basis-URL bestimmt die API: eine Anthropic-URL wie https://api.anthropic.com/v1 wird erkannt, jeder andere Host wird über die OpenAI-kompatible API angesprochen.",
"statusConfigured": "Eingerichtet",
"statusNotConfigured": "Nicht eingerichtet",
"keyLabel": "API-Schlüssel",
"keyPlaceholder": "sk-...",
- "keyPlaceholderSet": "Hinterlegt ({preview}) — neuen Schlüssel eingeben, um ihn zu ersetzen",
+ "keyPlaceholderSet": "Hinterlegt ({preview}). Neuen Schlüssel eingeben, um ihn zu ersetzen",
"modelLabel": "Modell",
"baseUrlLabel": "Basis-URL",
"save": "Speichern",
"remove": "Schlüssel entfernen",
"removeTitle": "Server-Schlüssel entfernen?",
- "removeBody": "Konten ohne eigenen Schlüssel verlieren die KI-Funktionen sofort. Der Schlüssel wird nach dem Speichern nirgends mehr angezeigt — zum Wiederherstellen brauchst du das Original.",
+ "removeBody": "Konten ohne eigenen Schlüssel verlieren die KI-Funktionen sofort. Der Schlüssel wird nach dem Speichern nirgends mehr angezeigt. Zum Wiederherstellen brauchst du das Original.",
"removeConfirm": "Schlüssel entfernen",
"saved": "KI-Server-Schlüssel gespeichert",
"error": "Speichern des KI-Server-Schlüssels fehlgeschlagen",
@@ -6996,13 +7249,16 @@
"typeCodex": "ChatGPT-Anmeldung (persönlich)",
"typeLocal": "Lokales Modell (persönlich)",
"typeGateway": "OpenAI-kompatibles Gateway (persönlich)",
- "spendToday": "Heute ({date}): {total} Tokens, davon {operator} auf dem Schlüssel des Betreibers."
+ "spendToday": "Heute, jeweils am eigenen Kalendertag jedes Kontos: {total} Tokens, davon {operator} auf dem Schlüssel des Betreibers (Konten mit Verbrauch: {accounts}).",
+ "statusPaused": "Pausiert",
+ "pausedHint": "Nach wiederholten Fehlern für {count} Nutzer pausiert bis {at}. Danach wird er automatisch wieder versucht.",
+ "modelNotServed": "Der Endpunkt bietet das Modell {model} nicht mehr an. Wähle in den KI-Einstellungen ein Modell, das er anbietet."
},
"aiQuality": {
"title": "Auswertungs-Qualität",
"loading": "Feedback-Zusammenfassung wird geladen…",
"loadError": "Qualitätszusammenfassung konnte nicht geladen werden. Bitte die Seite neu laden.",
- "empty": "Noch kein Feedback. Der Aggregator läuft täglich um 04:00 — sobald Nutzer Empfehlungen bewerten, erscheinen hier die Hilfreich-Quoten.",
+ "empty": "Noch kein Feedback. Der Aggregator läuft täglich um 04:00. Sobald Nutzer Empfehlungen bewerten, erscheinen hier die Hilfreich-Quoten.",
"windowLabel": "Letzte {days} Tage",
"generatedAtLabel": "Erstellt",
"colSeverity": "Schweregrad",
@@ -7017,7 +7273,7 @@
"title": "Coach-Feedback",
"loading": "Coach-Feedback-Zusammenfassung wird geladen…",
"loadError": "Coach-Feedback-Zusammenfassung konnte nicht geladen werden. Bitte die Seite neu laden.",
- "empty": "Noch kein Coach-Feedback. Der Aggregator läuft täglich um 04:00 — sobald Nutzer Coach-Antworten bewerten, erscheinen hier die Hilfreich-Quoten.",
+ "empty": "Noch kein Coach-Feedback. Der Aggregator läuft täglich um 04:00. Sobald Nutzer Coach-Antworten bewerten, erscheinen hier die Hilfreich-Quoten.",
"windowLabel": "Letzte {days} Tage",
"generatedAtLabel": "Erstellt",
"colPromptVersion": "Prompt-Version",
@@ -7036,13 +7292,13 @@
"appSettingsDescription": "Globale Standardwerte für Registrierung, Sprache und Zeitzone neuer Konten.",
"registrationEnabled": "Registrierung erlauben",
"registrationEnabledDescription": "Wenn deaktiviert, können sich keine neuen Nutzer registrieren.",
- "registrationOpenHint": "Die Registrierung ist offen — jeder, der diese Instanz erreichen kann, kann ein Konto anlegen. Schließe sie hier, sobald alle ein Konto haben.",
+ "registrationOpenHint": "Die Registrierung ist offen: Jeder, der diese Instanz erreichen kann, kann ein Konto anlegen. Schließe sie hier, sobald alle ein Konto haben.",
"defaultLanguage": "Standard-Sprache",
"defaultLanguageDescription": "Wird für neue Nutzer und als Fallback verwendet.",
"defaultUserTimezone": "Standard-Zeitzone für neue Konten",
- "defaultUserTimezoneDescription": "Wird für neue Konten ohne erkannte Browser-Zeitzone verwendet. Bestehende Nutzer sind nicht betroffen.",
+ "defaultUserTimezoneDescription": "Gilt für neue Konten ohne erkannte Browser-Zeitzone; bestehende Konten behalten ihre.",
"defaultUserTimezoneFallback": "Europe/Berlin (Standard)",
- "servicesGlobal": "Dienste (global)",
+ "servicesGlobal": "Kanäle und API",
"servicesGlobalDescription": "Deaktivierte Dienste stehen keinem Nutzer zur Verfügung.",
"telegramGlobal": "Telegram-Benachrichtigungen erlauben.",
"ntfyGlobal": "ntfy-Benachrichtigungen erlauben.",
@@ -7050,19 +7306,21 @@
"integrationWebPush": "Web Push",
"offlineGeoLabel": "Geodaten",
"offlineGeoEnabled": "Offline (GeoLite2)",
- "offlineGeoFallback": "Online ({host}) — GeoLite2-Datenbanken einbinden und GEOLITE2_DIR setzen",
+ "offlineGeoFallback": "Online ({host}). GeoLite2-Datenbanken einbinden und GEOLITE2_DIR setzen",
+ "offlineGeoOff": "Aus",
+ "offlineGeoChosen": "Online ({host})",
"apiGlobal": "API-Endpunkte und API-Tokens erlauben.",
"umamiTitle": "Umami",
- "umamiDescription": "Web-Analyse mit Umami",
+ "umamiDescription": "Web-Analyse mit Umami.",
"glitchtipTitle": "GlitchTip",
- "glitchtipDescription": "Fehler-Tracking mit GlitchTip",
+ "glitchtipDescription": "Fehler-Tracking mit GlitchTip.",
"webPushVapidTitle": "Web Push VAPID",
- "umamiEnabled": "Umami-Tracking aktivieren.",
+ "umamiEnabled": "Umami-Tracking aktivieren",
"umamiScriptUrl": "Umami Script-URL",
"umamiScriptUrlPlaceholder": "https://analytics.example.com/script.js",
"umamiWebsiteId": "Umami Website-ID",
"umamiWebsiteIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "glitchtipEnabled": "GlitchTip Fehler-Tracking aktivieren.",
+ "glitchtipEnabled": "GlitchTip-Fehler-Tracking aktivieren",
"glitchtipDsn": "GlitchTip DSN",
"glitchtipDsnPlaceholder": "https://@glitchtip.example.com/",
"glitchtipEnvironment": "GlitchTip Environment",
@@ -7085,7 +7343,7 @@
"webPushVapidGenerating": "Wird erzeugt…",
"webPushVapidGenerated": "VAPID-Schlüssel erzeugt. Betreff speichern, um abzuschließen.",
"webPushVapidGenerateFailed": "VAPID-Schlüssel konnten nicht erzeugt werden.",
- "webPushVapidGenerateConfirm": "VAPID-Schlüssel sind bereits konfiguriert. Ein neues Schlüsselpaar macht alle Browser-Push-Abos ungültig — jedes Gerät muss sich neu anmelden. Fortfahren?",
+ "webPushVapidGenerateConfirm": "VAPID-Schlüssel sind bereits konfiguriert. Ein neues Schlüsselpaar macht alle Browser-Push-Abos ungültig, jedes Gerät muss sich neu anmelden. Fortfahren?",
"webPushVapidGenerateConfirmTitle": "VAPID-Schlüssel neu erzeugen?",
"webPushVapidRegenerate": "Neu erzeugen",
"configured": "Konfiguriert",
@@ -7113,18 +7371,23 @@
"documentAi": {
"title": "Dokumente lesen",
"description": "KI-Lesen gespeicherter Dokumente, Scans von Laborbefunden und das Erkennen von Medikamenten aus Text. Der größte Anteil der Daten, die den Server verlassen."
+ },
+ "reasoning": {
+ "enabled": "Gründliches Denken erlauben",
+ "max": "Höchste Denktiefe",
+ "description": "Modelle denken vor der Antwort nach: im Coach, im Tagesbriefing und in den Zusammenfassungen. Genauer, braucht mehr Tokens."
}
},
"modules": {
"title": "Modulverfügbarkeit",
"description": "Schalte ein Modul für den gesamten Server ab.",
"saveError": "Modulverfügbarkeit konnte nicht aktualisiert werden.",
- "detail": "Ein hier deaktiviertes Modul ist für jedes Konto aus — unabhängig von den persönlichen Einstellungen. Kerndaten (Gewicht, Blutdruck, Puls, Medikamente) sind immer aktiv und lassen sich nicht abschalten.",
- "coachFollowsSwitch": "Folgt dem Coach-Schalter unter Assistenz."
+ "detail": "Gewicht, Blutdruck und Puls sind Kerndaten und bleiben immer aktiv.",
+ "coachFollowsSwitch": "Folgt dem Coach-Schalter im Bereich Coach."
},
"invites": {
- "title": "Einladungen",
- "description": "Einladungslinks für die Registrierung — funktionieren auch bei geschlossener Registrierung.",
+ "title": "Einladungslinks",
+ "description": "Jeder Link lässt eine Registrierung zu, auch wenn sie geschlossen ist.",
"create": "Einladung erstellen",
"createTitle": "Einladung erstellen",
"createDescription": "Der Link erlaubt eine Registrierung auch bei geschlossener Registrierung.",
@@ -7136,8 +7399,8 @@
"createdToast": "Einladung erstellt",
"createError": "Einladung konnte nicht erstellt werden.",
"tokenTitle": "Einladungslink",
- "tokenOnceHint": "Nur jetzt kopierbar — kopiere den Link oder lass den QR-Code scannen.",
- "hashExplainer": "Gespeichert wird nur ein Hash des Links — er kann später nicht erneut angezeigt werden. Geht er verloren: Einladung widerrufen und neu erstellen.",
+ "tokenOnceHint": "Nur jetzt kopierbar. Kopiere den Link oder lass den QR-Code scannen.",
+ "hashExplainer": "Gespeichert wird nur ein Hash des Links, er kann später nicht erneut angezeigt werden. Geht er verloren: Einladung widerrufen und neu erstellen.",
"copyLink": "Link kopieren",
"copiedToast": "Link kopiert",
"copyError": "Kopieren fehlgeschlagen.",
@@ -7170,8 +7433,8 @@
"revokedToast": "Einladung widerrufen.",
"revokeError": "Einladung konnte nicht widerrufen werden."
},
- "userManagement": "Nutzerverwaltung",
- "userManagementDescription": "Konten verwalten: Rollen, Passwort-Resets und erzwungene Abmeldungen.",
+ "userManagement": "Konten",
+ "userManagementDescription": "Rollen, Passwort-Resets und erzwungene Abmeldungen für jedes Konto.",
"userName": "Name",
"userEmail": "E-Mail",
"userRole": "Rolle",
@@ -7198,14 +7461,13 @@
"tokenName": "Name",
"tokenPermissions": "Berechtigungen",
"tokenPermissionAll": "Alle",
- "tokenPermissionAllTooltip": "Wildcard-Berechtigung — dieses Token kann jede API-Route ansprechen.",
+ "tokenPermissionAllTooltip": "Wildcard-Berechtigung: Dieses Token kann jede API-Route ansprechen.",
"tokenStatus": "Status",
"tokenLastUsed": "Zuletzt benutzt",
"tokenCreated": "Erstellt",
"tokenNeverUsed": "Nie",
"tokensEmptyTitle": "Noch keine API-Tokens",
"tokensEmptyDescription": "Tokens werden bei der Anmeldung nativer Clients oder über Einstellungen → Account ausgestellt.",
- "loginOverview": "Anmeldeübersicht",
"loginOverviewDescription": "Anmelde- und Auth-Ereignisse aller Konten, filterbar und als CSV exportierbar.",
"allAuthEvents": "Alle Auth-Ereignisse",
"failedOnly": "Nur fehlgeschlagene",
@@ -7238,6 +7500,7 @@
"finalDelete": "Endgültig löschen",
"deletedResult": "Gelöscht: {rows} Zeilen in {tables} Tabellen. Nutzerkonten und Anmelde-Zugangsdaten bleiben erhalten.",
"authRegister": "Registrierung",
+ "authClaim": "Profil übernommen",
"authLogin": "Anmeldung",
"authLoginPasskey": "Passkey-Anmeldung",
"authLoginPassword": "Passwort-Anmeldung",
@@ -7312,7 +7575,7 @@
},
"services": {
"title": "Dienste",
- "subtitle": "Benachrichtigungskanäle und Integrationen systemweit aktivieren oder deaktivieren."
+ "subtitle": "Benachrichtigungskanäle und die API für den ganzen Server ein- oder ausschalten."
},
"integrations": {
"title": "Integrationen",
@@ -7376,7 +7639,7 @@
"app-logs": {
"title": "App-Logs",
"subtitle": "Letzte strukturierte Wide-Events dieses Prozesses anzeigen.",
- "processNote": "Es werden nur App-Prozess-Logs angezeigt — Worker-Logs landen in Loki, sofern die Integration konfiguriert ist.",
+ "processNote": "Es werden nur App-Prozess-Logs angezeigt; Worker-Logs landen in Loki, sofern die Integration konfiguriert ist.",
"filterTraceId": "Trace-ID",
"filterAction": "Aktion enthält…",
"filterLevel": "Level",
@@ -7398,7 +7661,8 @@
"refresh": "Aktualisieren",
"eventDetails": "Event-Details",
"closeDetails": "Details schließen",
- "processDetail": "Der Puffer ist flüchtig und auf 500 Einträge begrenzt."
+ "processDetail": "Der Puffer ist flüchtig und auf 500 Einträge begrenzt.",
+ "cardTitle": "Letzte Ereignisse"
},
"backups": {
"title": "Backups",
@@ -7410,7 +7674,7 @@
"loading": "Backups werden geladen…",
"loadError": "Backups konnten nicht geladen werden.",
"emptyTitle": "Noch keine Backups",
- "emptyDescription": "Backups laufen automatisch jeden Sonntag um 03:00 Uhr — oder erstelle jetzt einen ersten Snapshot.",
+ "emptyDescription": "Backups laufen automatisch jeden Sonntag um 03:00 Uhr, oder erstelle jetzt einen ersten Snapshot.",
"colUser": "Nutzer",
"colType": "Typ",
"typeWeeklyAuto": "Wöchentlich (auto)",
@@ -7425,6 +7689,12 @@
"scheduleFailedDescription": "{when}: {reason}",
"scheduleFailedNoReason": "kein Grund aufgezeichnet",
"scheduleRemedy": "Starte jetzt eine Sicherung und prüfe, ob eine frische Zeile erscheint. Schlägt sie erneut fehl, nennt das Worker-Protokoll das Konto, an dem sie stehen geblieben ist.",
+ "activity": {
+ "running": "Läuft seit {when}.",
+ "interruptedTitle": "Ein Lauf ist mittendrin abgebrochen und nicht zurückgekehrt",
+ "interruptedAccount": "{username}: begonnen {when}, nie beendet",
+ "interruptedRemedy": "Wahrscheinlich wurde die App neu gestartet oder ihr ging beim Schreiben dieser Kopie der Speicher aus. Der nächste Lauf nimmt diese Konten zuletzt, damit alle anderen zuerst ihre Kopie bekommen. Passiert es wieder, gib dem Container mehr Speicher."
+ },
"download": "Herunterladen",
"downloadAria": "Backup für {username} herunterladen",
"downloadFailed": "Download fehlgeschlagen",
@@ -7440,11 +7710,11 @@
"restoreTitle": "Aus Backup wiederherstellen",
"restoreDescription": "Dies ersetzt ALLE aktuellen Daten von {username} mit dem Snapshot von {when}. Vorhandene Messungen, Medikamente, Einnahmen, Stimmungseinträge, Benachrichtigungs-Kanäle, Push-Abos und Telegram-Lösch-Aufträge werden entfernt.",
"restoreInstanceSettingsLabel": "Instanzweite Einstellungen mitwiederherstellen",
- "restoreInstanceSettingsHint": "Registrierung, MFA-Pflicht, Standardsprache, Modulverfügbarkeit, Benachrichtigungs- und KI-Konfiguration — aus dem Snapshot, gültig für alle auf dieser Installation. Leer lassen, um nur dieses Konto wiederherzustellen.",
+ "restoreInstanceSettingsHint": "Registrierung, MFA-Pflicht, Standardsprache, Modulverfügbarkeit, Benachrichtigungs- und KI-Konfiguration aus dem Snapshot, gültig für alle auf dieser Installation. Leer lassen, um nur dieses Konto wiederherzustellen.",
"restorePromptLabel": "Tippe RESTORE zum Fortfahren",
"previewTitle": "Inhalt der Sicherung",
"previewLoading": "Inhalt wird gelesen…",
- "previewUnavailable": "Inhalt konnte nicht gelesen werden — die Wiederherstellung bleibt möglich.",
+ "previewUnavailable": "Inhalt konnte nicht gelesen werden, die Wiederherstellung bleibt möglich.",
"previewStillReading": "Diese Sicherung stammt aus einer früheren Version, deshalb wird ihr Inhalt vollständig gelesen. Bei einem großen Datenbestand dauert das eine Weile. Öffne diesen Dialog in ein paar Minuten noch einmal, dann steht er hier. Die Wiederherstellung hängt nicht davon ab.",
"previewEmpty": "Die Datei enthält keine Datensätze.",
"previewOther": "Weitere Datensätze",
@@ -7487,7 +7757,7 @@
"restoreSkippedMedicationTarget": "Zielwert-Analyt einer Medikation",
"restoreSkippedScheduleRevisionLink": "Abfolge der Schema-Zeiträume",
"restoreSkippedAccountSetting": "Kontoeinstellung",
- "restoreMissingTitle": "Wiederherstellung abgelehnt — die Datei ist unvollständig",
+ "restoreMissingTitle": "Wiederherstellung abgelehnt: Die Datei ist unvollständig",
"restoreMissingDescription": "Das Manifest der Datei sagt, sie enthalte den folgenden Abschnitt, und sie enthält ihn nicht. Es wurde nichts geändert: Eine Wiederherstellung ersetzt eine ganze Datenklasse, ein fehlender Abschnitt hätte sie also geleert.",
"restoreMissingRemedy": "Nimm eine Notfall-Sicherung oder exportiere das Konto erneut und lade die neue Datei hoch. Ein Abschnitt, den das Manifest ausdrücklich als weggelassen ausweist, ist etwas anderes und wird normal wiederhergestellt.",
"restoreMissingDocuments": "Dokumente",
@@ -7555,7 +7825,12 @@
"restoreFailureKeyMissing": "Die Sicherung braucht den Verschlüsselungsschlüssel {keys}, mit dem dieser Server sie nicht öffnen kann. Nimm den Schlüssel wieder in ENCRYPTION_KEYS auf, starte neu und starte die Wiederherstellung erneut.",
"restoreFailureForeignReference": "Die Datei verweist auf Einträge, die zu einem anderen Konto gehören. Es wurde nichts geschrieben.",
"previewKeyMissing": "Diese Sicherung lässt sich auf diesem Server nicht wiederherstellen: Sie braucht den Verschlüsselungsschlüssel {keys}. Nimm den Schlüssel zuerst wieder in ENCRYPTION_KEYS auf und starte neu.",
- "restoreSkippedSymptomEpisodeReference": "Symptom-Eintrag mit Verknüpfung zu einer Erkrankung, die die Datei nicht enthält (ohne Verknüpfung behalten)"
+ "restoreSkippedSymptomEpisodeReference": "Symptom-Eintrag mit Verknüpfung zu einer Erkrankung, die die Datei nicht enthält (ohne Verknüpfung behalten)",
+ "restoreSkippedCycleCiphertext": "Zyklus-Notiz oder -Bezeichnung mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (Tag oder Symptom ohne sie behalten)",
+ "restoreSkippedMoodLabelCiphertext": "Name eines Stimmungs-Tags oder einer Gruppe mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (ohne Namen behalten)",
+ "restoreSkippedVisitCiphertext": "Text zu Arztbesuch oder Praxis mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (ohne ihn behalten)",
+ "restoreSkippedVaccinationCiphertext": "Impf-Notiz mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (Impfung ohne sie behalten)",
+ "restoreSkippedEnvironmentLocationCiphertext": "Ort eines Umweltzeitraums oder -tags mit einem Schlüssel verschlüsselt, den dieser Server nicht hat (verschlüsselt behalten, diese Tage werden nicht neu abgerufen)"
},
"danger-zone": {
"title": "Gefahrenzone",
@@ -7572,7 +7847,6 @@
"encryption": {
"title": "Verschlüsselung",
"subtitle": "Verschlüsselungsabdeckung und Schlüsselrotations-Status für ruhende Daten.",
- "loading": "Verschlüsselungsstatus wird geladen…",
"loadError": "Verschlüsselungsstatus konnte nicht geladen werden.",
"coverageTitle": "Abdeckung",
"coverageDescription": "Wie viele verschlüsselte Zeilen unter welcher Schlüssel-ID liegen.",
@@ -7607,7 +7881,7 @@
"completed": "Abgeschlossen",
"failed": "Fehlgeschlagen"
},
- "coverageDetail": "Es werden nur Schlüssel-IDs und Anzahlen gezeigt — niemals Schlüsselmaterial.",
+ "coverageDetail": "Es werden nur Schlüssel-IDs und Anzahlen gezeigt, niemals Schlüsselmaterial.",
"backupsTitle": "Schlüssel, die die Sicherungen noch brauchen",
"backupsDescription": "Ein alter Schlüssel bleibt nötig, solange eine Sicherung unten ihn noch aufführt.",
"backupsStoredLine": "Schlüssel {key}: {count} gespeicherte Kopien, die älteste vom {when}",
@@ -7617,12 +7891,45 @@
"backupsNone": "Noch keine Sicherung hat die Schlüssel festgehalten, die sie braucht.",
"backupsStillNeed": "Alle Zeilen sind rotiert, aber Sicherungen brauchen noch {keys}",
"backupsUnrecordedBadge": "Alle Zeilen sind rotiert, aber ältere Sicherungen können noch einen ausgemusterten Schlüssel brauchen",
- "backupsDetail": "Die Rotation verschlüsselt jede Zeile neu, aber eine Sicherung behält den Schlüssel, unter dem ihr Inhalt geschrieben wurde, und ihre Wiederherstellung braucht ihn. Behalte einen ausgemusterten Schlüssel in ENCRYPTION_KEYS, bis er unten nicht mehr aufgeführt ist."
+ "backupsDetail": "Die Rotation verschlüsselt jede Zeile neu, aber eine Sicherung behält den Schlüssel, unter dem ihr Inhalt geschrieben wurde, und ihre Wiederherstellung braucht ihn. Behalte einen ausgemusterten Schlüssel in ENCRYPTION_KEYS, bis er unten nicht mehr aufgeführt ist.",
+ "columnsShow": "Tabelle anzeigen ({count} mit Zeilen)",
+ "columnsHide": "Tabelle ausblenden",
+ "columnsShowAll": "Alle {count} Spalten anzeigen",
+ "columnsShowWithRows": "Nur Spalten mit Zeilen",
+ "columnsNoneWithRows": "Noch keine verschlüsselte Spalte enthält Zeilen."
+ },
+ "maintenance": {
+ "title": "Pflege der Messwert-Tabelle",
+ "description": "Gibt den Platz frei, den alte Sync-Reste belegt haben, sobald die Bereinigung gelaufen ist.",
+ "body": "Sie räumt die Messwert-Tabelle auf und baut danach ihre Indizes neu auf, einen nach dem anderen. Lesen und Speichern funktionieren währenddessen weiter. Wähle eine ruhige Stunde und halte freien Speicher für eine weitere Kopie des größten Index bereit.",
+ "tableSize": "Tabelle mit Indizes",
+ "indexSize": "Indizes",
+ "loading": "Stand der Pflege wird geprüft…",
+ "loadError": "Der Stand der Pflege konnte nicht gelesen werden.",
+ "unavailable": "Es ist kein Hintergrunddienst verbunden, deshalb kann die Pflege hier nicht laufen.",
+ "purgePending": "Die Bereinigung alter Sync-Reste läuft noch. Die Pflege kann starten, sobald sie fertig ist.",
+ "state": {
+ "idle": "Noch nicht gelaufen.",
+ "queued": "Wartet auf den Hintergrunddienst.",
+ "running": "Läuft seit {since}. Auf einer großen Instanz kann das eine Weile dauern.",
+ "completed": "Fertig am {at}.",
+ "refusedPurge": "Nicht gestartet, weil die Bereinigung noch lief. Starte sie erneut, sobald die Bereinigung fertig ist.",
+ "alreadyRunning": "Nicht gestartet, weil bereits ein anderer Lauf arbeitete.",
+ "failed": "Angehalten am {at}. Das App-Protokoll nennt den Schritt, das Runbook erklärt, was zu prüfen ist."
+ },
+ "start": "Pflege starten",
+ "confirmTitle": "Pflege jetzt starten?",
+ "confirmBody": "Sie läuft im Hintergrund, Schritt für Schritt, und kann eine Weile dauern. Wenn du heute noch keine Sicherung erstellt hast, erstelle zuerst eine.",
+ "confirm": "Starten",
+ "cancel": "Abbrechen",
+ "started": "Pflege gestartet.",
+ "alreadyQueued": "Ein Pflegelauf ist bereits eingeplant oder läuft.",
+ "startFailed": "Die Pflege konnte nicht gestartet werden."
}
},
"carrier": "Netzbetreiber",
"mfaRequired": "Zweiten Faktor verlangen",
- "mfaRequiredDescription": "Jedes Konto ohne aktiven zweiten Faktor wird nach der Anmeldung zur Einrichtung geleitet. Das fordert Web-Nutzer zur Einrichtung auf — es blockiert jedoch keinen direkten API- oder nativen (Bearer-)Zugriff für noch nicht eingerichtete Konten.",
+ "mfaRequiredDescription": "Konten ohne zweiten Faktor werden nach der Web-Anmeldung zur Einrichtung geleitet; API- und nativer (Bearer-)Zugriff bleiben offen.",
"mfaEnforcedOn": "MFA an",
"mfaEnforcedOff": "MFA aus",
"mfaEnforcedOnHint": "Für diesen Nutzer ist ein zweiter Faktor erforderlich. Klicken, um die Anforderung aufzuheben.",
@@ -7640,7 +7947,7 @@
"centralCodex": {
"title": "Gemeinsame KI-Verbindung",
"description": "Verbinde ein angemeldetes ChatGPT-Konto, das jede Nutzerin und jeder Nutzer dieses Servers für KI-Funktionen aktivieren kann.",
- "honesty": "Dies ist ein einzelnes gemeinsames, angemeldetes KI-Konto — kein API-Schlüssel. Alle, die zustimmen, senden Anfragen darüber, sodass die Stunden- und Wochenlimits des Kontos unter ihnen geteilt werden; dieselben Limits speisen bereits den Coach. Abo-Anbieter dürfen die Inhalte zur Verbesserung ihrer Modelle verwenden, und keine Auftragsverarbeitungsvereinbarung deckt dies ab — daher ist es nie Standard und steht beim Lesen von Dokumenten hinter Anbietern ohne Training. Verbinde nur ein Konto, das du auf diese Weise teilen möchtest.",
+ "honesty": "Dies ist ein einzelnes gemeinsames, angemeldetes KI-Konto, kein API-Schlüssel. Alle, die zustimmen, senden Anfragen darüber, sodass die Stunden- und Wochenlimits des Kontos unter ihnen geteilt werden; dieselben Limits speisen bereits den Coach. Abo-Anbieter dürfen die Inhalte zur Verbesserung ihrer Modelle verwenden, und keine Auftragsverarbeitungsvereinbarung deckt dies ab. Daher ist es nie Standard und steht beim Lesen von Dokumenten hinter Anbietern ohne Training. Verbinde nur ein Konto, das du auf diese Weise teilen möchtest.",
"statusConnected": "Verbunden",
"statusDisconnected": "Nicht verbunden",
"statusExpired": "Abgelaufen",
@@ -7652,13 +7959,13 @@
"connectedSince": "Verbunden seit {when}",
"connected": "Gemeinsame KI-Verbindung verknüpft",
"disconnected": "Gemeinsame KI-Verbindung entfernt",
- "error": "Etwas ist schiefgelaufen — bitte erneut versuchen"
+ "error": "Etwas ist schiefgelaufen. Bitte erneut versuchen"
},
"failingJobs": {
"title": "Hintergrundjobs",
"description": "Queues, deren Jobs nach allen Wiederholungen aufgegeben haben.",
"none": "In den letzten {hours} Stunden ist kein Job fehlgeschlagen.",
- "unavailable": "Keine Job-Queue lesbar — dieses Deployment läuft ohne Hintergrund-Worker.",
+ "unavailable": "Keine Job-Queue lesbar. Dieses Deployment läuft ohne Hintergrund-Worker.",
"countOne": "{count} Fehlschlag",
"countFew": "{count} Fehlschläge",
"countOther": "{count} Fehlschläge",
@@ -7720,7 +8027,9 @@
"restartNote": "Starte HealthLog nach der Änderung neu.",
"lastResort": "Letzter Ausweg, nur wenn du sicher bist, dass der Schlüssel stimmt, und keiner der drei Wege passt: setze {setting} und starte neu. HealthLog liefert dann trotzdem aus und schreibt den Befund nur ins Log. Damit ist eine Sicherheitsprüfung abgeschaltet: Mit einem falschen Schlüssel scheitert jeder verschlüsselte Wert, und neue Daten werden unter diesem Schlüssel geschrieben.",
"codes": "Für den Betrieb: API-Anfragen antworten mit 503 und dem Fehlercode {code}, /api/health meldet den Grund {reason}."
- }
+ },
+ "loginOverviewCardTitle": "Anmeldeereignisse",
+ "userColumn": "Nutzer"
},
"achievements": {
"title": "Erfolge",
@@ -7736,7 +8045,9 @@
"noneUnlockedYet": "Noch keine Erfolge freigeschaltet.",
"pointsValue": "{points} Punkte",
"metricCount": "{count}",
- "metricDays": "{count} Tage",
+ "metricDaysOne": "{count} Tag",
+ "metricDaysFew": "{count} Tage",
+ "metricDaysOther": "{count} Tage",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -7759,14 +8070,6 @@
"title": "Einnahme-Legende",
"description": "Dokumentiere 300 eingenommene Medikamente."
},
- "overIntake1": {
- "title": "Doppelt genommen",
- "description": "Nimm mindestens einmal mehr ein als vorgesehen."
- },
- "skippedIntake1": {
- "title": "Pause eingelegt",
- "description": "Überspringe mindestens eine geplante Einnahme."
- },
"passkeyCreated1": {
"title": "Passkey gesetzt",
"description": "Erstelle deinen ersten Passkey."
@@ -7792,19 +8095,19 @@
"description": "Nimm an einem Tag alle Medikamente im Zeitfenster ein."
},
"compliance801": {
- "title": "Treue 80% · 1T",
+ "title": "80% Treue für 1 Tag",
"description": "Erreiche an einem Tag eine 30-Tage-Therapietreue von mindestens 80%."
},
"bmiGreen1": {
- "title": "BMI im Grün · 1T",
+ "title": "BMI 1 Tag im Grün",
"description": "Halte deinen BMI an einem Tag im Normalbereich."
},
"bpGreen1": {
- "title": "Blutdruck im Grün · 1T",
+ "title": "Blutdruck 1 Tag im Grün",
"description": "Halte deinen Blutdruck an einem Tag im Normalbereich."
},
"pulseGreen1": {
- "title": "Puls im Grün · 1T",
+ "title": "Puls 1 Tag im Grün",
"description": "Halte deinen Ruhepuls an einem Tag im Normalbereich."
},
"onTimePerfect7": {
@@ -7812,19 +8115,19 @@
"description": "Nimm 7 Tage in Folge alle Medikamente im Zeitfenster ein."
},
"compliance807": {
- "title": "Treue 80% · 7T",
+ "title": "80% Treue für 7 Tage",
"description": "Erreiche 7 Tage in Folge eine 30-Tage-Therapietreue von mindestens 80%."
},
"bmiGreen7": {
- "title": "BMI im Grün · 7T",
+ "title": "BMI 7 Tage im Grün",
"description": "Halte deinen BMI 7 Tage in Folge im Normalbereich."
},
"bpGreen7": {
- "title": "Blutdruck im Grün · 7T",
+ "title": "Blutdruck 7 Tage im Grün",
"description": "Halte deinen Blutdruck 7 Tage in Folge im Normalbereich."
},
"pulseGreen7": {
- "title": "Puls im Grün · 7T",
+ "title": "Puls 7 Tage im Grün",
"description": "Halte deinen Ruhepuls 7 Tage in Folge im Normalbereich."
},
"onTimePerfect30": {
@@ -7832,19 +8135,19 @@
"description": "Nimm 30 Tage in Folge alle Medikamente im Zeitfenster ein."
},
"compliance8030": {
- "title": "Treue 80% · 30T",
+ "title": "80% Treue für 30 Tage",
"description": "Erreiche 30 Tage in Folge eine 30-Tage-Therapietreue von mindestens 80%."
},
"bmiGreen30": {
- "title": "BMI im Grün · 30T",
+ "title": "BMI 30 Tage im Grün",
"description": "Halte deinen BMI 30 Tage in Folge im Normalbereich."
},
"bpGreen30": {
- "title": "Blutdruck im Grün · 30T",
+ "title": "Blutdruck 30 Tage im Grün",
"description": "Halte deinen Blutdruck 30 Tage in Folge im Normalbereich."
},
"pulseGreen30": {
- "title": "Puls im Grün · 30T",
+ "title": "Puls 30 Tage im Grün",
"description": "Halte deinen Ruhepuls 30 Tage in Folge im Normalbereich."
},
"onTimePerfect180": {
@@ -7852,19 +8155,19 @@
"description": "Nimm 180 Tage in Folge alle Medikamente im Zeitfenster ein."
},
"compliance80180": {
- "title": "Treue 80% · 180T",
+ "title": "80% Treue für 180 Tage",
"description": "Erreiche 180 Tage in Folge eine 30-Tage-Therapietreue von mindestens 80%."
},
"bmiGreen180": {
- "title": "BMI im Grün · 180T",
+ "title": "BMI 180 Tage im Grün",
"description": "Halte deinen BMI 180 Tage in Folge im Normalbereich."
},
"bpGreen180": {
- "title": "Blutdruck im Grün · 180T",
+ "title": "Blutdruck 180 Tage im Grün",
"description": "Halte deinen Blutdruck 180 Tage in Folge im Normalbereich."
},
"pulseGreen180": {
- "title": "Puls im Grün · 180T",
+ "title": "Puls 180 Tage im Grün",
"description": "Halte deinen Ruhepuls 180 Tage in Folge im Normalbereich."
},
"onTimePerfect360": {
@@ -7872,19 +8175,19 @@
"description": "Nimm 360 Tage in Folge alle Medikamente im Zeitfenster ein."
},
"compliance80360": {
- "title": "Treue 80% · 360T",
+ "title": "80% Treue für 360 Tage",
"description": "Erreiche 360 Tage in Folge eine 30-Tage-Therapietreue von mindestens 80%."
},
"bmiGreen360": {
- "title": "BMI im Grün · 360T",
+ "title": "BMI 360 Tage im Grün",
"description": "Halte deinen BMI 360 Tage in Folge im Normalbereich."
},
"bpGreen360": {
- "title": "Blutdruck im Grün · 360T",
+ "title": "Blutdruck 360 Tage im Grün",
"description": "Halte deinen Blutdruck 360 Tage in Folge im Normalbereich."
},
"pulseGreen360": {
- "title": "Puls im Grün · 360T",
+ "title": "Puls 360 Tage im Grün",
"description": "Halte deinen Ruhepuls 360 Tage in Folge im Normalbereich."
},
"moodFirst": {
@@ -7968,15 +8271,15 @@
"description": "Du hast die App-Sprache mindestens einmal gewechselt."
},
"missFree7": {
- "title": "Lückenlos · 7T",
+ "title": "7 Tage lückenlos",
"description": "7 Tage in Folge ohne eine verpasste Dosis."
},
"missFree30": {
- "title": "Lückenlos · 30T",
+ "title": "30 Tage lückenlos",
"description": "30 Tage in Folge ohne eine verpasste Dosis."
},
"missFree90": {
- "title": "Lückenlos · 90T",
+ "title": "90 Tage lückenlos",
"description": "90 Tage in Folge ohne eine verpasste Dosis."
},
"measurementWeeks4": {
@@ -7985,10 +8288,10 @@
},
"selfContextComplete": {
"title": "Selbstauskunft gepflegt",
- "description": "Alle vier Felder der Selbstauskunft ausgefüllt — der Coach kennt deinen Kontext."
+ "description": "Alle vier Felder der Selbstauskunft ausgefüllt. Der Coach kennt deinen Kontext."
},
"sleepLog7": {
- "title": "Schlafprotokoll · 7T",
+ "title": "7 Tage Schlafprotokoll",
"description": "An 7 Tagen in Folge Schlafdaten erfasst."
}
},
@@ -8007,7 +8310,7 @@
},
"hiddenCard": {
"title": "Versteckter Erfolg",
- "description": "Tracke fleißig weiter — vielleicht stolperst du darüber.",
+ "description": "Tracke fleißig weiter, vielleicht stolperst du darüber.",
"ariaLabel": "Versteckter, gesperrter Erfolg"
},
"hiddenUnlockToast": {
@@ -8016,7 +8319,7 @@
"dashboardCard": {
"title": "Zuletzt freigeschaltet",
"viewAll": "Alle anzeigen",
- "empty": "Noch keine Erfolge — trage weiter ein, um deinen ersten freizuschalten."
+ "empty": "Noch keine Erfolge. Trage weiter ein, um deinen ersten freizuschalten."
}
},
"notifications": {
@@ -8078,9 +8381,6 @@
"enabledToast": "Proaktive Coach-Hinweise aktiviert.",
"disabledToast": "Proaktive Coach-Hinweise deaktiviert.",
"saveError": "Speichern fehlgeschlagen. Bitte später erneut versuchen.",
- "suggestionsLabel": "Beispielvorschläge",
- "suggestionsDesc": "Zeigt auf dem Coach-Bildschirm gelegentlich eine fertige Beispielfrage.",
- "suggestionsAria": "Beispiel-Coach-Vorschläge umschalten",
"aiComposedLabel": "Den Coach den Anstoß formulieren lassen",
"aiComposedDesc": "Wenn ein Anbieter verfügbar ist, formuliert das Modell den proaktiven Anstoß statt der festen Vorlage. Standardmäßig aus.",
"aiComposedAria": "KI-formulierten proaktiven Anstoß umschalten"
@@ -8151,7 +8451,46 @@
"loginRequired": "Bitte melde dich an, um Benachrichtigungen zu verwalten.",
"someChannelsGloballyDisabled": "Ein oder mehrere Kanäle sind in den Einstellungen deaktiviert und erhalten keine Benachrichtigungen.",
"reminderSettingsLink": "Erinnerungseinstellungen",
- "channelsLink": "Kanäle"
+ "channelsLink": "Kanäle",
+ "handover": {
+ "claimedTitle": "{name} hat jetzt ein eigenes Konto",
+ "claimedEnd": "{name} hat die eigene Gesundheitsakte übernommen. Dein Zugriff ist beendet.",
+ "claimedRead": "{name} hat die eigene Gesundheitsakte übernommen. Du kannst sie weiter ansehen, aber nicht mehr ändern.",
+ "claimedManage": "{name} hat die eigene Gesundheitsakte übernommen. Du kannst sie weiter verwalten, bis {name} anders entscheidet.",
+ "changedTitle": "{name} hat deinen Zugriff geändert",
+ "changedEnd": "Dein Zugriff auf die Gesundheitsakte von {name} ist beendet.",
+ "changedRead": "Du kannst die Gesundheitsakte von {name} jetzt ansehen, aber nicht ändern.",
+ "changedManage": "Du kannst die Gesundheitsakte von {name} verwalten.",
+ "unnamed": {
+ "claimedTitle": "Die Person, die du betreut hast, hat jetzt ein eigenes Konto",
+ "claimedEnd": "Die Person, die du betreut hast, hat die eigene Gesundheitsakte übernommen. Dein Zugriff ist beendet.",
+ "claimedRead": "Die Person, die du betreut hast, hat die eigene Gesundheitsakte übernommen. Du kannst sie weiter ansehen, aber nicht mehr ändern.",
+ "claimedManage": "Die Person, die du betreut hast, hat die eigene Gesundheitsakte übernommen. Du kannst sie weiter verwalten, bis sie anders entscheidet.",
+ "changedTitle": "Dein Zugriff auf eine Gesundheitsakte hat sich geändert",
+ "changedEnd": "Dein Zugriff auf die Gesundheitsakte der Person, die du betreut hast, ist beendet.",
+ "changedRead": "Du kannst die Gesundheitsakte der Person, die du betreut hast, jetzt ansehen, aber nicht ändern.",
+ "changedManage": "Du kannst die Gesundheitsakte der Person, die du betreut hast, verwalten."
+ }
+ },
+ "links": {
+ "open": "In HealthLog öffnen"
+ },
+ "quickActions": {
+ "moodGood": "Gut",
+ "moodOkay": "Okay",
+ "moodBad": "Schlecht"
+ },
+ "content": {
+ "title": "Gesundheitswerte in Benachrichtigungen",
+ "description": "Ob eine Benachrichtigung Werte wie deinen Morgenscore zeigen darf.",
+ "detail": "Aus: Eine Benachrichtigung sagt nur, worum es geht, der Sperrbildschirm zeigt keine Gesundheitswerte. An: Sie nennt auch die Werte, etwa den Morgenscore im Überblick. Ein Tipp auf die Benachrichtigung öffnet in beiden Fällen die passende Stelle in der App.",
+ "toggleAria": "Gesundheitswerte in Benachrichtigungen zeigen",
+ "statusDetailed": "Mit Werten",
+ "statusGeneric": "Ohne Werte",
+ "detailedToast": "Benachrichtigungen zeigen jetzt Gesundheitswerte.",
+ "genericToast": "Benachrichtigungen zeigen keine Gesundheitswerte mehr.",
+ "saveError": "Speichern fehlgeschlagen. Bitte später erneut versuchen."
+ }
},
"i18n": {
"maintainershipBanner": {
@@ -8182,7 +8521,7 @@
}
},
"edit": {
- "title": "Zielbereich bearbeiten — {metric}",
+ "title": "Zielbereich bearbeiten: {metric}",
"description": "Passt den Zielbereich an, der in Charts, Insights und im Arztbericht verwendet wird. Werte in {unit}.",
"derivedHint": "Diese Karte wird aus anderen Metriken abgeleitet. Bearbeite stattdessen die zugrundeliegenden Ziele (z. B. Blutdruck oder Gewicht).",
"minLabel": "Min",
@@ -8206,15 +8545,19 @@
"currentValue": "Aktuell: {value} {unit}",
"targetRangeValue": "Zielbereich: {min}–{max} {unit}",
"sourceLabel": "Quelle: {source}",
+ "sourceNames": {
+ "moodEntries": "Stimmungseinträge",
+ "sevenDays": "7 Tage"
+ },
"inTarget": "Im Zielbereich",
"aboveTarget": "{delta} {unit} über Zielbereich",
"belowTarget": "{delta} {unit} unter Zielbereich",
"diastolic": "Diastolisch",
- "glucoseFasting": "Glukose — nüchtern",
- "glucosePostprandial": "Glukose — postprandial",
- "glucoseRandom": "Glukose — beliebig",
- "glucoseBedtime": "Glukose — vor dem Schlafen",
- "glucoseUnspecified": "Glukose — ohne Angabe",
+ "glucoseFasting": "Glukose, nüchtern",
+ "glucosePostprandial": "Glukose, postprandial",
+ "glucoseRandom": "Glukose, beliebig",
+ "glucoseBedtime": "Glukose, vor dem Schlafen",
+ "glucoseUnspecified": "Glukose, ohne Angabe",
"status": {
"underweight": "Untergewicht",
"normal": "Normal",
@@ -8297,10 +8640,10 @@
"noActiveMedications": "Keine aktiven Medikamente gefunden.",
"whichMedication": "Welches Medikament eingenommen?",
"cancelButton": "Abbrechen",
- "askMedicationName": "Bitte Medikament angeben, z.B. genommen Ramipril.",
+ "askMedicationName": "Bitte Medikament angeben, z. B. genommen Ramipril.",
"medicationNotFoundExact": "Medikament nicht gefunden. Bitte Namen exakt wie in HealthLog senden.",
"helpHeader": "Verfügbare Befehle:",
- "helpBody": "/help — Diese Hilfe anzeigen\n/add — Einnahme erfassen\n/start — Bot starten\n\nTextbefehle:\ngenommen <Name> — Einnahme bestätigen\n\nÜber die Buttons in Erinnerungen:\n• Genommen — Einnahme bestätigen\n• 🕐 1h / 🕐 3h — Erinnerung verschieben\n• ⏭ Überspringen — Einnahme überspringen\n• ✓ Bestätigen — Verpasste Einnahme bestätigen",
+ "helpBody": "/help: Diese Hilfe anzeigen\n/add: Einnahme erfassen\n/start: Bot starten\n\nTextbefehle:\ngenommen <Name>: Einnahme bestätigen\n\nÜber die Buttons in Erinnerungen:\n• Genommen: Einnahme bestätigen\n• 🕐 1h / 🕐 3h: Erinnerung verschieben\n• ⏭ Überspringen: Einnahme überspringen\n• ✓ Bestätigen: Verpasste Einnahme bestätigen",
"measureValuePrompt": "Antworte mit dem Wert, um ihn zu speichern (optional).",
"buttonMeasureValue": "Wert…",
"measureValueSaved": "Messwert gespeichert.",
@@ -8326,7 +8669,7 @@
"lowStockReminders": {
"title": "Vorrat niedrig: {medName}",
"bodyToday": "Dein Vorrat an {medName} reicht keinen ganzen Tag mehr ({units} Einheiten übrig).",
- "bodyReorder": "Dein Vorrat an {medName} ({units} Einheiten) ist um den {runsOutOn} aufgebraucht — bestelle bis zum {reorderBy} nach.",
+ "bodyReorder": "Dein Vorrat an {medName} ({units} Einheiten) ist um den {runsOutOn} aufgebraucht. Bestelle bis zum {reorderBy} nach.",
"bodyLastDose": "Dein Vorrat an {medName} ({units} Einheiten) reicht noch etwa für die letzte Dosis. Aufgebraucht um den {runsOutOn}.",
"bodyExpired": "{medName}: kein nutzbarer Bestand mehr. {units} Einheiten sind als abgelaufen markiert.",
"bodyAsNeeded": "Von {medName} sind nur noch {units} Einheiten übrig. Bestelle nach, bevor du die nächste Dosis brauchst."
@@ -8354,11 +8697,11 @@
"questionConditions": "Hast du chronische Erkrankungen, die der Coach kennen sollte?",
"questionFocus": "Gibt es etwas Bestimmtes, worauf der Coach achten soll?",
"questionAllergies": "Hast du Allergien oder Unverträglichkeiten?",
- "questionAboutMe": "Magst du dem Coach etwas über dich erzählen — Alltag, Ziele, Kontext?"
+ "questionAboutMe": "Magst du dem Coach etwas über dich erzählen? Alltag, Ziele, Kontext?"
},
"clinicianView": {
"title": "Geteilte Gesundheitsakte",
- "period": "Berichtszeitraum: {start} – {end}",
+ "period": "Berichtszeitraum: {start}–{end}",
"expires": "Dieser Link läuft am {date} ab.",
"provenance": "Dies ist eine schreibgeschützte Zusammenfassung, die von der betreffenden Person geteilt wurde, exportiert aus ihren selbst erfassten HealthLog-Daten. Die Werte sind selbst erhoben und können geräteübertragene Messwerte enthalten; behandeln Sie sie als von der Patientin/dem Patienten berichtet, nicht als klinische Untersuchung.",
"labs": "Laborwerte",
@@ -8382,7 +8725,7 @@
"strain": "Belastungswert",
"score": "Wellness-Wert"
},
- "footer": "Geteilt aus HealthLog — einem selbst gehosteten Gesundheits-Tracker.",
+ "footer": "Geteilt aus HealthLog, einem selbst gehosteten Gesundheits-Tracker.",
"unlock": {
"title": "Passphrase eingeben",
"description": "Diese geteilte Gesundheitsakte ist geschützt. Geben Sie die erhaltene Passphrase ein, um sie anzuzeigen.",
@@ -8417,7 +8760,11 @@
},
"course": "Phase {n}",
"courseRange": "{start} bis {end}",
- "courseSince": "seit {start}"
+ "courseSince": "seit {start}",
+ "unavailable": {
+ "title": "Link abgelaufen oder ungültig",
+ "description": "Dieser Freigabelink ist abgelaufen, wurde zurückgezogen oder ist unvollständig. Bitte die Person, die ihn geteilt hat, um einen neuen Link."
+ }
},
"cycle": {
"symptomCategory": {
@@ -8466,11 +8813,11 @@
"generic": "Dein/e {metric} liegt in der Lutealphase etwa {delta} {unit} {dir} als in der Follikelphase.",
"dirHigher": "höher",
"dirLower": "niedriger",
- "empty": "Noch nicht genug Zyklen und Messwerte für ein Phasenmuster. Logge weiter — das füllt sich, je länger deine Historie wird."
+ "empty": "Noch nicht genug Zyklen und Messwerte für ein Phasenmuster. Logge weiter, das füllt sich, je länger deine Historie wird."
},
"crosstab": {
- "description": "Wie sich jeder Messwert zwischen deiner Luteal- und Follikelphase vergleicht — auf deinen eigenen Daten.",
- "pairLabel": "{metric} · Luteal vs. Follikel",
+ "description": "Wie sich jeder Messwert zwischen deiner Luteal- und Follikelphase vergleicht, auf deinen eigenen Daten.",
+ "pairLabel": "{metric}: Luteal- vs. Follikelphase",
"detail": "Luteal {lutealAvg} {unit} ({lutealDays} Tage) vs. Follikel {follicularAvg} {unit} ({follicularDays} Tage).",
"metricRestingHeartRate": "Ruhepuls",
"metricHrv": "Herzfrequenzvariabilität",
@@ -8506,7 +8853,7 @@
},
"context": {
"MENSTRUAL": "Die Hormonspiegel sind jetzt am niedrigsten, weshalb sich diese Tage wie ein ruhiger Neustart anfühlen können. Manche bemerken Krämpfe, leichteren Schlaf oder den Wunsch, einen Gang zurückzuschalten; andere kommen fast unverändert durch. All das liegt im normalen Rahmen, und wie es sich anfühlt, kann von Zyklus zu Zyklus wechseln.",
- "FOLLICULAR": "Mit steigendem Östrogen beschreiben viele diese Tage als hell und offen — der Schlaf fällt oft leichter und der Kopf fühlt sich klarer an. Die Basaltemperatur bleibt meist im niedrigeren Bereich, bis kurz vor dem Eisprung. Die Länge dieser Phase schwankt am stärksten — von Person zu Person und von Zyklus zu Zyklus.",
+ "FOLLICULAR": "Mit steigendem Östrogen beschreiben viele diese Tage als hell und offen: Der Schlaf fällt oft leichter und der Kopf fühlt sich klarer an. Die Basaltemperatur bleibt meist im niedrigeren Bereich, bis kurz vor dem Eisprung. Die Länge dieser Phase schwankt am stärksten, von Person zu Person und von Zyklus zu Zyklus.",
"OVULATORY": "Das Östrogen erreicht kurz vor dem Eisprung seinen Höhepunkt, oft begleitet von einem kurzen Stimmungshoch und einem leichten Anstieg der Basaltemperatur in den ein, zwei Tagen danach. Der Zervixschleim wird in diesem Fenster meist klarer und dehnbarer. Ein kurzes Ziehen auf einer Seite bemerken manche; viele spüren gar nichts, und beides ist völlig normal.",
"LUTEAL": "Das Progesteron führt durch die zweite Zyklushälfte, und die Basaltemperatur liegt meist etwas höher als zuvor im Zyklus. Manche bemerken im Lauf der Tage Veränderungen bei Schlaf, Appetit, Stimmung oder Energie, während andere durchgehend ausgeglichen bleiben. Das Muster ist persönlich und sieht von Monat zu Monat oft anders aus."
},
@@ -8593,8 +8940,8 @@
"ariaUnknown": "Noch kein aktiver Zyklus",
"caption": "deines Zyklus",
"firstPeriodCta": "Erste Periode erfassen",
- "periodOverdue": "Deine Periode ist später als üblich — die Tageszählung pausiert, bis die nächste Periode erfasst ist.",
- "periodOverdueDays": "Deine Periode ist {days} Tage später als üblich — die Tageszählung pausiert, bis die nächste Periode erfasst ist.",
+ "periodOverdue": "Deine Periode ist später als üblich. Die Tageszählung pausiert, bis die nächste Periode erfasst ist.",
+ "periodOverdueDays": "Deine Periode ist {days} Tage später als üblich. Die Tageszählung pausiert, bis die nächste Periode erfasst ist.",
"logPeriodCta": "Periode erfassen"
},
"calendar": {
@@ -8606,7 +8953,7 @@
"prevMonth": "Vorheriger Monat",
"nextMonth": "Nächster Monat",
"loading": "Kalender wird geladen…",
- "learning": "Wir lernen deinen Zyklus — erfasse mindestens drei volle Zyklen, dann füllen sich fruchtbares Fenster, Eisprung und Phasen. Bis dahin zeigt der Kalender nur, was du erfasst hast.",
+ "learning": "Wir lernen deinen Zyklus. Erfasse mindestens drei volle Zyklen, dann füllen sich fruchtbares Fenster, Eisprung und Phasen. Bis dahin zeigt der Kalender nur, was du erfasst hast.",
"legendSymptom": "Symptome",
"legendOvulationConfirmed": "Bestätigter Eisprung",
"flowSpotting": "Schmierblutung",
@@ -8619,7 +8966,7 @@
"predictions": {
"title": "Was als Nächstes kommt",
"nextPeriod": "Nächste Periode",
- "nextPeriodWindow": "{low} – {high}",
+ "nextPeriodWindow": "{low}–{high}",
"fertileWindow": "Fruchtbares Fenster",
"ovulation": "Geschätzter Eisprung",
"confidenceHigh": "Hoch",
@@ -8628,7 +8975,7 @@
"stillLearning": "Noch am Lernen",
"stillLearningBody": "Erfasse mindestens drei vollständige Zyklen, dann werden deine Prognosen genauer. Bis dahin ist dies eine grobe Schätzung.",
"rawMode": "Prognose ist aus",
- "rawModeBody": "Du hast Prognosen ausgeschaltet — es werden nur deine erfassten Daten gezeigt, ohne Vorhersage.",
+ "rawModeBody": "Du hast Prognosen ausgeschaltet. Es werden nur deine erfassten Daten gezeigt, ohne Vorhersage.",
"none": "Noch keine Prognose. Erfasse eine Periode, um zu starten.",
"ovulationConfirmed": "Eisprung (bestätigt)"
},
@@ -8661,7 +9008,7 @@
"symptoms": "Symptome",
"temperature": "Basaltemperatur",
"temperatureDisturbed": "Messwert als gestört markieren",
- "temperatureDisturbedHint": "Fieber, Krankheit, Alkohol oder eine späte Messung — wird von der Temperaturauswertung ausgeschlossen.",
+ "temperatureDisturbedHint": "Fieber, Krankheit, Alkohol oder eine späte Messung: wird von der Temperaturauswertung ausgeschlossen.",
"ovulationTest": "Ovulationstest",
"mucus": "Zervixschleim",
"cervix": "Gebärmutterhals",
@@ -8690,7 +9037,7 @@
"temperatureSection": "Temperatur & Eisprung",
"intimacySection": "Intimität & Verhütung",
"testsSection": "Tests",
- "phaseDayHeader": "Tag {day} · {phase}",
+ "phaseDayHeader": "Tag {day}, {phase}",
"bbtHint": "Miss sie morgens als Erstes, noch vor dem Aufstehen.",
"titleForDate": "Eintrag für {date}",
"descriptionToday": "Heute. Stärke, Symptome und Notizen.",
@@ -8724,7 +9071,7 @@
"remindersDescription": "Perioden-Erinnerungen pro Kanal in den Benachrichtigungseinstellungen aktivieren.",
"remindersLink": "Benachrichtigungseinstellungen",
"fertileReminder": "Erinnerung an das fruchtbare Fenster",
- "fertileReminderDescription": "Erhalte einige Tage vor deinem geschätzten fruchtbaren Fenster einen Hinweis. Standardmäßig aus — aktiviere sie pro Kanal in den Benachrichtigungseinstellungen."
+ "fertileReminderDescription": "Erhalte einige Tage vor deinem geschätzten fruchtbaren Fenster einen Hinweis. Standardmäßig aus. Aktiviere sie pro Kanal in den Benachrichtigungseinstellungen."
},
"testResult": {
"POSITIVE": "Positiv",
@@ -8757,15 +9104,15 @@
"fieldInfo": {
"bbt": "Deine Ruhetemperatur, morgens als Erstes vor dem Aufstehen gemessen. Nach dem Eisprung steigt sie leicht an (etwa 0,2–0,3 °C).",
"bbtLabel": "Was ist die Basaltemperatur?",
- "mucus": "Der Ausfluss verändert sich im Zyklus — von trocken oder klebrig zu cremig, dann wässrig und spinnbar rund um die fruchtbarsten Tage.",
+ "mucus": "Der Ausfluss verändert sich im Zyklus: von trocken oder klebrig zu cremig, dann wässrig und spinnbar rund um die fruchtbarsten Tage.",
"mucusLabel": "Was ist Zervixschleim?",
- "ovulationTest": "Ein Urintest für zu Hause, der den LH-Anstieg erkennt — den Hormonschub etwa 24–36 h vor dem Eisprung. Ein positives Ergebnis markiert die fruchtbarste Phase.",
+ "ovulationTest": "Ein Urintest für zu Hause, der den LH-Anstieg erkennt, also den Hormonschub etwa 24–36 h vor dem Eisprung. Ein positives Ergebnis markiert die fruchtbarste Phase.",
"ovulationTestLabel": "Was ist ein Ovulationstest?",
"spotting": "Leichte Blutung außerhalb der Periode.",
"spottingLabel": "Was ist Schmierblutung zwischen den Perioden?",
"pregnancyTest": "Ergebnis eines Schwangerschaftstests (hCG) für zu Hause.",
"pregnancyTestLabel": "Was ist ein Schwangerschaftstest?",
- "progesteroneTest": "Ein Test, der bestätigt, dass der Eisprung stattgefunden hat — das Progesteron steigt danach an.",
+ "progesteroneTest": "Ein Test, der bestätigt, dass der Eisprung stattgefunden hat. Das Progesteron steigt danach an.",
"progesteroneTestLabel": "Was ist ein Progesterontest?",
"intercourse": "Ob an diesem Tag Geschlechtsverkehr stattgefunden hat.",
"intercourseLabel": "Was erfasst Geschlechtsverkehr?",
@@ -8818,14 +9165,14 @@
"postpone": {
"menuItem": "Verschieben…",
"title": "Fälligkeit verschieben",
- "description": "Verschiebt nur die aktuelle Fälligkeit — der reguläre Rhythmus bleibt unverändert.",
+ "description": "Verschiebt nur die aktuelle Fälligkeit, der reguläre Rhythmus bleibt unverändert.",
"chip": "+{days} Tage",
"dateLabel": "Neues Datum",
"confirm": "Verschieben"
},
"skip": {
"heading": "Diese Fälligkeit überspringen",
- "explainer": "Wird ehrlich als übersprungen festgehalten — nicht als erledigt. Das Intervall startet ab heute neu.",
+ "explainer": "Wird ehrlich als übersprungen festgehalten, nicht als erledigt. Das Intervall startet ab heute neu.",
"action": "Überspringen",
"confirmTitle": "Fälligkeit überspringen?"
},
@@ -8867,10 +9214,10 @@
"VISCERAL_FAT": "Viszeralfett",
"BODY_MASS_INDEX": "Body-Mass-Index (BMI)",
"WAIST_CIRCUMFERENCE": "Taillenumfang",
- "PHQ9_SCORE": "PHQ-9 · Stimmung",
- "GAD7_SCORE": "GAD-7 · Sorgen",
- "WHO5_SCORE": "WHO-5 · Wohlbefinden",
- "SCI_SCORE": "SCI · Schlaf"
+ "PHQ9_SCORE": "PHQ-9 (Stimmung)",
+ "GAD7_SCORE": "GAD-7 (Sorgen)",
+ "WHO5_SCORE": "WHO-5 (Wohlbefinden)",
+ "SCI_SCORE": "SCI (Schlaf)"
},
"typeGroups": {
"vitals": "Vitalwerte",
@@ -8966,7 +9313,7 @@
"localOcr": {
"heading": "Scans auf diesem Gerät lesen",
"description": "Liest das Foto auf deinem Gerät und sendet nur den erkannten Text an deinen Anbieter.",
- "detail": "Nützlich, wenn dein KI-Anbieter keine Bilder lesen kann. Weniger genau — prüfe jeden Wert vor dem Speichern."
+ "detail": "Nützlich, wenn dein KI-Anbieter keine Bilder lesen kann. Weniger genau, prüfe jeden Wert vor dem Speichern."
},
"sort": {
"heading": "Sortierung",
@@ -9010,7 +9357,7 @@
"sourceRangePlaceholder": "z. B. 3,5 - 5,0 oder < 5",
"sourceRangeAdd": "+ Bereich aus dem Befund ergänzen",
"sourceRangeHint": "Optional. Gilt nur für diesen Wert; dein gespeicherter Bereich bleibt unverändert.",
- "sourceRangeUnparsed": "Wird so übernommen — daraus ließ sich kein Zahlenbereich lesen."
+ "sourceRangeUnparsed": "Wird so übernommen, daraus ließ sich kein Zahlenbereich lesen."
},
"backToLabs": "Zurück zu den Laborwerten",
"hasNote": "Notiz",
@@ -9028,7 +9375,7 @@
"emptyDescription": "Füge einen Wert hinzu, um diesen Biomarker über die Zeit zu verfolgen.",
"title": "Biomarker",
"genericDescription": "Ein Laborwert, den du verfolgst. Erfasse Messwerte, um den Verlauf gegenüber dem Referenzbereich zu sehen.",
- "valuesTitle": "{marker} — alle Werte",
+ "valuesTitle": "{marker}: alle Werte",
"valuesDescription": "Jeder Messwert für diesen Biomarker. Bearbeite oder entferne einen Eintrag direkt.",
"backToMarker": "Zurück zu {marker}",
"latestResult": "Letztes Ergebnis"
@@ -9048,7 +9395,7 @@
},
"biomarker": {
"manageTitle": "Biomarker",
- "managerDescription": "Marker einmal definieren — Einheit und Bereich gelten für jede Messung.",
+ "managerDescription": "Marker einmal definieren; Einheit und Bereich gelten dann für jede Messung.",
"define": "Hinzufügen",
"defineFirst": "Ersten Biomarker hinzufügen",
"defineTitle": "Biomarker hinzufügen",
@@ -9067,7 +9414,7 @@
"form": {
"seedLabel": "Von einem gängigen Marker starten",
"seedPlaceholder": "Marker wählen (optional)",
- "seedHint": "Eine Auswahl füllt die Felder unten — passe den Bereich an dein Labor an.",
+ "seedHint": "Eine Auswahl füllt die Felder unten. Passe den Bereich an dein Labor an.",
"name": "Name",
"namePlaceholder": "z. B. LDL-Cholesterin",
"unit": "Einheit",
@@ -9081,7 +9428,7 @@
"upperPlaceholder": "Obere Grenze",
"context": "Kontext",
"contextPlaceholder": "Optionale Notiz zu diesem Marker",
- "rangeHint": "Beide Grenzen sind optional — eine leer lassen für einen offenen Bereich (z. B. LDL < 116).",
+ "rangeHint": "Beide Grenzen sind optional. Eine leer lassen für einen offenen Bereich (z. B. LDL < 116).",
"requiredError": "Name und Einheit sind erforderlich.",
"rangeOrderError": "Die untere Grenze darf die obere nicht überschreiten.",
"save": "Biomarker speichern",
@@ -9137,36 +9484,36 @@
"bloodCount": "Blutbild"
},
"desc": {
- "total-cholesterol": "Das Gesamtcholesterin ist die Summe des Cholesterins über alle Lipoprotein-Partikel im Blut, einschließlich der LDL-, HDL- und Remnant-Fraktion. Cholesterin selbst ist ein unverzichtbarer Baustein für Zellmembranen, Hormone und Gallensäuren. Als einzelner Wert bleibt es ein grobes Maß, denn sowohl ein hoher HDL- als auch ein hoher LDL-Anteil heben die Summe — deshalb liest man es meist gemeinsam mit den Einzelfraktionen. Erfasst wird es vor allem im Rahmen der langfristigen kardiovaskulären Risikoeinschätzung.",
- "ldl": "Das LDL-Cholesterin ist das von Low-Density-Lipoprotein-Partikeln transportierte Cholesterin — die Fraktion, die Cholesterin zu den Geweben bringt, es aber auch in den Gefäßwänden ablagern kann. Über Jahre trägt diese Ablagerung zur arteriosklerotischen Plaque bei, weshalb LDL einer der am genauesten beobachteten Lipidmarker ist. Eine geringere langfristige Belastung geht in der Regel mit einem niedrigeren kardiovaskulären Risiko einher. Bewertet wird es nach Risikokategorie, nicht gegen einen festen Normwert.",
+ "total-cholesterol": "Das Gesamtcholesterin ist die Summe des Cholesterins über alle Lipoprotein-Partikel im Blut, einschließlich der LDL-, HDL- und Remnant-Fraktion. Cholesterin selbst ist ein unverzichtbarer Baustein für Zellmembranen, Hormone und Gallensäuren. Als einzelner Wert bleibt es ein grobes Maß, denn sowohl ein hoher HDL- als auch ein hoher LDL-Anteil heben die Summe. Deshalb liest man es meist gemeinsam mit den Einzelfraktionen. Erfasst wird es vor allem im Rahmen der langfristigen kardiovaskulären Risikoeinschätzung.",
+ "ldl": "Das LDL-Cholesterin ist das von Low-Density-Lipoprotein-Partikeln transportierte Cholesterin, also die Fraktion, die Cholesterin zu den Geweben bringt, es aber auch in den Gefäßwänden ablagern kann. Über Jahre trägt diese Ablagerung zur arteriosklerotischen Plaque bei, weshalb LDL einer der am genauesten beobachteten Lipidmarker ist. Eine geringere langfristige Belastung geht in der Regel mit einem niedrigeren kardiovaskulären Risiko einher. Bewertet wird es nach Risikokategorie, nicht gegen einen festen Normwert.",
"hdl": "Das HDL-Cholesterin ist das von High-Density-Lipoprotein-Partikeln transportierte Cholesterin, die überschüssiges Cholesterin aus den Geweben aufnehmen und zur Wiederverwertung oder Ausscheidung zur Leber zurückführen. Wegen dieser Transportfunktion wird es oft als die schützende Fraktion bezeichnet. Sehr niedrige Werte gehen tendenziell mit höherem kardiovaskulärem Risiko einher; ein künstliches Anheben hat das Risiko jedoch nicht zuverlässig gesenkt, weshalb man es als Marker und nicht als Zielgröße liest. In der Regel wird es zusammen mit LDL und Triglyzeriden beurteilt.",
"triglycerides": "Triglyzeride sind die wichtigste Form von Fett im Blut, die sowohl aus der Nahrung stammt als auch von der Leber gebildet wird, und sie dienen als zentraler Energiespeicher. Nach Mahlzeiten steigen die Werte, daher wird die Probe für die Vergleichbarkeit meist nüchtern entnommen. Dauerhaft hohe Triglyzeride werden mit metabolischem und kardiovaskulärem Risiko in Verbindung gebracht und treten häufig gemeinsam mit Insulinresistenz und niedrigem HDL auf. Sie werden als ein Bestandteil des gesamten Lipidbilds gelesen.",
"fasting-glucose": "Der Nüchternblutzucker ist die Zuckerkonzentration im Blut nach einer nächtlichen Nüchternphase und spiegelt wider, wie der Körper seinen Grundblutzucker ohne kürzliche Nahrungsaufnahme hält. Glukose ist der wichtigste unmittelbare Brennstoff des Körpers, und Insulin hält sie normalerweise in einem engen Bereich. Eine Nüchternmessung schließt den Effekt kürzlicher Mahlzeiten aus und ist damit ein Standardscreening dafür, wie die Glukoseregulation funktioniert. Er gehört zu den Markern, mit denen die Stoffwechselgesundheit über die Zeit beurteilt wird.",
"hba1c": "Der HbA1c ist der Anteil des Hämoglobins in den roten Blutkörperchen, der an Glukose gebunden ist und sich im Verhältnis zur durchschnittlichen Blutzuckerbelastung anreichert. Da rote Blutkörperchen etwa drei Monate leben, gibt er den durchschnittlichen Blutzucker der vorangegangenen zwei bis drei Monate wieder und nicht einen einzelnen Augenblick. Das macht ihn zu einem stabilen, mahlzeitenunabhängigen Maß der Blutzuckereinstellung. Seine Aussagekraft kann durch Erkrankungen beeinträchtigt sein, die die Lebensdauer der roten Blutkörperchen verändern.",
"tsh": "Das TSH (Thyreoidea-stimulierendes Hormon) wird von der Hirnanhangdrüse ausgeschüttet, um die Schilddrüse zur Hormonproduktion anzuregen, und es passt sich nach oben oder unten an, um diese im Gleichgewicht zu halten. Aufgrund dieses Regelkreises ist das TSH das empfindlichste Erstlinien-Screening der Schilddrüsenfunktion: Es bewegt sich meist, bevor die Schilddrüsenhormone selbst abweichen. Ein erhöhter oder erniedrigter Wert deutet auf eine Unter- oder Überfunktion hin und führt in der Regel zur direkten Bestimmung der Schilddrüsenhormone. Schwangerschaft und Alter verschieben den erwarteten Bereich.",
- "ft3": "Das freie T3 (Trijodthyronin) ist die ungebundene, biologisch aktive Form des wirksameren Schilddrüsenhormons — der Anteil, der den Geweben zur Verfügung steht. Der größte Teil des zirkulierenden T3 ist an Trägerproteine gebunden und inaktiv, sodass der freie Anteil die tatsächliche Hormonverfügbarkeit widerspiegelt. Gemessen wird es zur Beurteilung der Schilddrüsenfunktion, besonders gemeinsam mit TSH und freiem T4, wenn Befunde unklar sind. Die Werte sind stark assayabhängig und werden im klinischen Zusammenhang gelesen.",
+ "ft3": "Das freie T3 (Trijodthyronin) ist die ungebundene, biologisch aktive Form des wirksameren Schilddrüsenhormons, also der Anteil, der den Geweben zur Verfügung steht. Der größte Teil des zirkulierenden T3 ist an Trägerproteine gebunden und inaktiv, sodass der freie Anteil die tatsächliche Hormonverfügbarkeit widerspiegelt. Gemessen wird es zur Beurteilung der Schilddrüsenfunktion, besonders gemeinsam mit TSH und freiem T4, wenn Befunde unklar sind. Die Werte sind stark assayabhängig und werden im klinischen Zusammenhang gelesen.",
"ft4": "Das freie T4 (Thyroxin) ist die ungebundene Form des wichtigsten Hormons, das die Schilddrüse ins Blut abgibt; es dient als Reservoir, das die Gewebe bei Bedarf in das aktivere T3 umwandeln. Der freie Anteil spiegelt wider, was dem Körper tatsächlich zur Verfügung steht, unabhängig von der Menge der Trägerproteine. In der Regel wird es zusammen mit dem TSH bestimmt, um Probleme der Schilddrüse von Störungen der hypophysären Steuerung zu unterscheiden. Schwangerschaft und bestimmte Medikamente können es verschieben.",
- "ferritin": "Ferritin ist das wichtigste Eisenspeicherprotein des Körpers, und sein Blutwert spiegelt grob die gesamten Eisenreserven wider. Ein niedriger Wert deutet auf erschöpfte Speicher hin, ein hoher Wert kann eine Eisenüberladung anzeigen — oder schlicht eine Entzündung, denn Ferritin steigt auch als Akute-Phase-Protein. Aus diesem Grund wird es oft zusammen mit anderen Eisenmarkern und Entzündungszeichen beurteilt. Es ist eines der zentralen Maße zur Einschätzung des Eisenstatus.",
- "transferrin-saturation": "Die Transferrinsättigung ist der Prozentsatz des Transferrins — des Proteins, das Eisen durch das Blut transportiert –, der aktuell mit Eisen beladen ist. Sie verknüpft die Menge des zirkulierenden Eisens mit der Eisentransportkapazität und liefert damit eine Momentaufnahme der Eisenverfügbarkeit, die Ferritins Blick auf die Speicher ergänzt. Eine niedrige Sättigung deutet auf einen Eisenmangel hin, ein hoher Wert kann eine Überladung anzeigen. Sie wird zusammen mit Ferritin und dem gesamten Eisenpanel gelesen.",
+ "ferritin": "Ferritin ist das wichtigste Eisenspeicherprotein des Körpers, und sein Blutwert spiegelt grob die gesamten Eisenreserven wider. Ein niedriger Wert deutet auf erschöpfte Speicher hin, ein hoher Wert kann eine Eisenüberladung anzeigen oder schlicht eine Entzündung, denn Ferritin steigt auch als Akute-Phase-Protein. Aus diesem Grund wird es oft zusammen mit anderen Eisenmarkern und Entzündungszeichen beurteilt. Es ist eines der zentralen Maße zur Einschätzung des Eisenstatus.",
+ "transferrin-saturation": "Die Transferrinsättigung ist der Prozentsatz des Transferrins (des Proteins, das Eisen durch das Blut transportiert), der aktuell mit Eisen beladen ist. Sie verknüpft die Menge des zirkulierenden Eisens mit der Eisentransportkapazität und liefert damit eine Momentaufnahme der Eisenverfügbarkeit, die Ferritins Blick auf die Speicher ergänzt. Eine niedrige Sättigung deutet auf einen Eisenmangel hin, ein hoher Wert kann eine Überladung anzeigen. Sie wird zusammen mit Ferritin und dem gesamten Eisenpanel gelesen.",
"vitamin-d": "Vitamin D wird als 25-Hydroxy-Vitamin-D gemessen, der zirkulierenden Speicherform und dem Standardmarker des gesamten Vitamin-D-Status. Der Körper bildet es in der dem Sonnenlicht ausgesetzten Haut und nimmt einen Teil über die Nahrung auf; es unterstützt den Kalziumhaushalt, die Knochengesundheit und die Immunfunktion. Die Werte schwanken mit Jahreszeit, geografischer Breite, Hauttyp und Nahrungsergänzung. Erfasst wird es, um über die Zeit einzuschätzen, ob die Speicher ausreichend gefüllt sind.",
- "vitamin-b12": "Vitamin B12 (Cobalamin) ist ein essenzielles Vitamin, das vor allem aus tierischen Lebensmitteln stammt und größtenteils in der Leber gespeichert wird. Es wird für die Bildung roter Blutkörperchen, die DNA-Synthese und die Gesundheit des Nervensystems benötigt. Ein länger anhaltender Mangel — durch geringe Zufuhr oder gestörte Aufnahme — kann zu einer Blutarmut und nervenbedingten Beschwerden führen. Gemessen wird es zur Beurteilung des Ernährungsstatus, insbesondere dort, wo Zufuhr oder Aufnahme eingeschränkt sein können.",
+ "vitamin-b12": "Vitamin B12 (Cobalamin) ist ein essenzielles Vitamin, das vor allem aus tierischen Lebensmitteln stammt und größtenteils in der Leber gespeichert wird. Es wird für die Bildung roter Blutkörperchen, die DNA-Synthese und die Gesundheit des Nervensystems benötigt. Ein länger anhaltender Mangel (durch geringe Zufuhr oder gestörte Aufnahme) kann zu einer Blutarmut und nervenbedingten Beschwerden führen. Gemessen wird es zur Beurteilung des Ernährungsstatus, insbesondere dort, wo Zufuhr oder Aufnahme eingeschränkt sein können.",
"folate": "Folat (Vitamin B9) ist ein B-Vitamin, das in grünem Blattgemüse, Hülsenfrüchten und angereicherten Lebensmitteln vorkommt und für die Bildung roter Blutkörperchen und die DNA-Synthese zentral ist. Es arbeitet eng mit Vitamin B12 zusammen, und ein Mangel an einem von beiden kann eine ähnliche Form der Blutarmut hervorrufen. Eine ausreichende Folatversorgung ist besonders rund um die frühe Schwangerschaft für die gesunde Entwicklung des Kindes wichtig. Gemessen wird es zur Beurteilung des Ernährungsstatus und der Ursache bestimmter Anämien.",
"hs-crp": "Das hochsensitive CRP misst das C-reaktive Protein, einen Marker, den die Leber als Reaktion auf Entzündungen ausschüttet, mit einem Verfahren, das auf die für das kardiovaskuläre Risiko relevanten niedrigen Konzentrationen abgestimmt ist. Anders als das Standard-CRP für akute Infektionen erfasst die hochsensitive Variante die feine, chronische Entzündung, die mit Gefäßerkrankungen verbunden ist. Da jede kürzliche Infektion oder Verletzung den Wert anhebt, wird er außerhalb akuter Erkrankung beurteilt und oft mit einer Wiederholungsprobe bestätigt. Er wird als ein Beitrag zum gesamten kardiovaskulären Risiko gelesen.",
"creatinine": "Kreatinin ist ein Abfallprodukt des normalen Muskelstoffwechsels, das die Nieren mit recht gleichbleibender Rate aus dem Blut filtern. Da die Produktion verhältnismäßig konstant ist, dient der Blutwert als praktisches Maß dafür, wie gut die Nieren Abfallstoffe ausscheiden. Die Werte werden auch von Muskelmasse, Flüssigkeitshaushalt und Ernährung beeinflusst, weshalb sie meist gemeinsam mit der geschätzten Filtrationsrate beurteilt werden. Es ist ein zentraler Marker der Nierenfunktion.",
"egfr": "Die eGFR (geschätzte glomeruläre Filtrationsrate) schätzt, wie viel Blut die Nieren pro Minute filtern, berechnet aus dem Kreatinin zusammen mit Alter und Geschlecht. Sie übersetzt einen rohen Kreatininwert in ein unmittelbarer verständliches Maß der Nierenfunktion. Eine über die Zeit sinkende eGFR ist der wichtigste Weg, um chronische Nierenveränderungen einzustufen und zu verfolgen. Als Schätzwert wird sie als Trend und nicht als einzelner exakter Zahlenwert gelesen.",
"alt": "Die ALT (Alanin-Aminotransferase) ist ein vorwiegend in den Leberzellen konzentriertes Enzym, das ins Blut freigesetzt wird, wenn diese Zellen belastet oder geschädigt sind. Da sie verhältnismäßig leberspezifisch ist, gehört sie zu den wichtigsten Markern, um eine Leberschädigung oder -entzündung zu erkennen. Die Werte können bei Fettleber, Virushepatitis, Alkohol oder bestimmten Medikamenten steigen. In der Regel wird sie zusammen mit der AST und den übrigen Markern des Leberpanels gelesen.",
- "ast": "Die AST (Aspartat-Aminotransferase) ist ein Enzym, das in der Leber, aber auch in Muskel, Herz und roten Blutkörperchen vorkommt und bei einer Schädigung dieser Gewebe freigesetzt wird. Da sie weniger leberspezifisch ist als die ALT, werden beide gemeinsam beurteilt — ihr Verhältnis kann Hinweise auf Quelle und Art des Problems geben. Intensive körperliche Belastung oder eine Muskelverletzung können sie unabhängig von der Leber anheben. Sie ist eines der Standardenzyme des Leberpanels.",
- "ggt": "Die GGT (Gamma-Glutamyltransferase) ist ein Enzym in der Leber und den Gallenwegen, das besonders bei einem gestörten Gallenfluss oder bei Alkoholexposition ansteigt. Häufig wird sie genutzt, um zu klären, warum ein anderer Lebermarker erhöht ist — etwa um eine Quelle in der Leber oder den Gallenwegen zu unterscheiden. Sie ist empfindlich, aber wenig spezifisch und wird daher im Zusammenhang mit dem übrigen Leberpanel gelesen. Sie ist einer von mehreren Markern zur Beurteilung der Leber- und Gallengesundheit.",
+ "ast": "Die AST (Aspartat-Aminotransferase) ist ein Enzym, das in der Leber, aber auch in Muskel, Herz und roten Blutkörperchen vorkommt und bei einer Schädigung dieser Gewebe freigesetzt wird. Da sie weniger leberspezifisch ist als die ALT, werden beide gemeinsam beurteilt; ihr Verhältnis kann Hinweise auf Quelle und Art des Problems geben. Intensive körperliche Belastung oder eine Muskelverletzung können sie unabhängig von der Leber anheben. Sie ist eines der Standardenzyme des Leberpanels.",
+ "ggt": "Die GGT (Gamma-Glutamyltransferase) ist ein Enzym in der Leber und den Gallenwegen, das besonders bei einem gestörten Gallenfluss oder bei Alkoholexposition ansteigt. Häufig wird sie genutzt, um zu klären, warum ein anderer Lebermarker erhöht ist, etwa um eine Quelle in der Leber oder den Gallenwegen zu unterscheiden. Sie ist empfindlich, aber wenig spezifisch und wird daher im Zusammenhang mit dem übrigen Leberpanel gelesen. Sie ist einer von mehreren Markern zur Beurteilung der Leber- und Gallengesundheit.",
"sodium": "Natrium ist der wichtigste Elektrolyt in der Flüssigkeit außerhalb der Körperzellen und der zentrale Regler von Flüssigkeitshaushalt und Blutvolumen. Der Körper steuert es eng über Durst, Hormone und die Nieren, sodass der Blutwert eher das Gleichgewicht zwischen Wasser und Salz widerspiegelt als die Zufuhr allein. Abweichende Werte signalisieren meist eine Störung des Wasserhaushalts und nicht des Natriums selbst. Es ist ein Kernbestandteil des grundlegenden Stoffwechsel- und Elektrolytpanels.",
"potassium": "Kalium ist der wichtigste Elektrolyt innerhalb der Körperzellen und für die Nervenleitung und den gleichmäßigen Rhythmus des Herzmuskels unverzichtbar. Sein Blutwert wird durch die Nieren und hormonelle Signale in einem engen Bereich gehalten, da sowohl hohe als auch niedrige Werte die elektrische Aktivität des Herzens stören können. Die Werte können durch Nierenfunktion, Flüssigkeitshaushalt und mehrere gängige Medikamente beeinflusst werden. Es ist ein Kernmaß des Elektrolytpanels.",
"hemoglobin": "Hämoglobin ist das eisenhaltige Protein in den roten Blutkörperchen, das Sauerstoff in der Lunge bindet und zu den Geweben transportiert. Seine Konzentration ist das wichtigste Maß für die Sauerstofftransportkapazität des Blutes und der zentrale Marker zum Erkennen einer Blutarmut. Niedrige Werte können auf Blutverlust, Eisen- oder Vitaminmangel oder eine verringerte Bildung hinweisen, hohe Werte können auf Flüssigkeitsmangel oder sauerstoffarme Bedingungen folgen. Es ist ein Eckpfeiler des großen Blutbilds.",
"hematocrit": "Der Hämatokrit ist der Anteil der roten Blutkörperchen am Blutvolumen, ausgedrückt als Prozentwert. Er bewegt sich eng mit dem Hämoglobin und gibt einen ergänzenden Blick auf die Erythrozytenmasse und die Sauerstofftransportkapazität des Blutes. Der Flüssigkeitshaushalt beeinflusst ihn, da Veränderungen des Plasmavolumens den Anteil verschieben, selbst wenn die Zahl der roten Blutkörperchen stabil bleibt. Er wird zusammen mit dem Hämoglobin und dem übrigen Blutbild gelesen.",
"wbc": "Die Leukozytenzahl ist die Gesamtzahl der im Blut zirkulierenden Immunzellen, der mobilen Abwehr des Körpers gegen Infektionen. Sie steigt als Reaktion auf Infektion, Entzündung oder Stress und kann fallen, wenn das Knochenmark unterdrückt ist oder bei manchen Immunerkrankungen. Da sie mehrere unterschiedliche Zelltypen zusammenfasst, wird eine deutliche Veränderung oft durch ein Differenzialblutbild aufgeschlüsselt. Sie ist ein Kernbestandteil des großen Blutbilds.",
"platelets": "Thrombozyten sind kleine Zellfragmente im Blut, die sich zusammenlagern und den ersten Pfropf bilden, der eine Blutung aus einem geschädigten Gefäß stoppt. Ihre Zahl spiegelt das Gleichgewicht zwischen Bildung im Knochenmark und Verbrauch oder Abbau wider. Eine niedrige Zahl kann die Blutungsneigung erhöhen, eine hohe Zahl kann auf eine Entzündung oder andere Reize folgen. Sie sind Teil des großen Blutbilds und ein grundlegendes Maß der Gerinnungsfähigkeit.",
- "apob": "Apolipoprotein B ist das Strukturprotein, das jedes der atherogenen Lipoprotein-Partikel trägt; seine Messung zählt daher diese Partikel direkt und nicht das von ihnen transportierte Cholesterin. Da auf jedem LDL-, VLDL- und verwandten Partikel genau ein ApoB-Molekül sitzt, kann es kardiovaskuläres Risiko erfassen, das ein LDL-Cholesterinwert übersieht — besonders wenn Partikelzahl und Cholesteringehalt auseinandergehen. Aus diesem Grund wird es zunehmend genutzt, um die lipidbasierte Risikoeinschätzung zu verfeinern. Beurteilt wird es zusammen mit dem üblichen Lipidpanel.",
+ "apob": "Apolipoprotein B ist das Strukturprotein, das jedes der atherogenen Lipoprotein-Partikel trägt; seine Messung zählt daher diese Partikel direkt und nicht das von ihnen transportierte Cholesterin. Da auf jedem LDL-, VLDL- und verwandten Partikel genau ein ApoB-Molekül sitzt, kann es kardiovaskuläres Risiko erfassen, das ein LDL-Cholesterinwert übersieht, besonders wenn Partikelzahl und Cholesteringehalt auseinandergehen. Aus diesem Grund wird es zunehmend genutzt, um die lipidbasierte Risikoeinschätzung zu verfeinern. Beurteilt wird es zusammen mit dem üblichen Lipidpanel.",
"lp-a": "Lipoprotein(a) ist ein LDL-ähnliches Partikel mit einem zusätzlich angehefteten Protein, dessen Spiegel weitgehend genetisch festgelegt ist und ein Leben lang recht stabil bleibt. Ein erhöhtes Lp(a) ist ein unabhängiger, vererbter Risikofaktor für Herz-Kreislauf-Erkrankungen und eine Verengung der Aortenklappe. Da es sich kaum verändert, wird es meist einmal bestimmt, um das lebenslange Risiko einer Person festzulegen, statt es wiederholt zu verfolgen. Es wird in molaren Einheiten angegeben und als Teil des gesamten kardiovaskulären Risikos gelesen.",
- "fasting-insulin": "Das Nüchterninsulin ist der Spiegel des Insulins — des Hormons, das Glukose aus dem Blut in die Zellen schleust –, gemessen nach einer nächtlichen Nüchternphase. Zusammen mit dem Nüchternblutzucker hilft es, die Insulinresistenz abzuschätzen, also den Zustand, in dem der Körper mehr Insulin braucht, um den Blutzucker im Bereich zu halten. Ein hohes Nüchterninsulin bei normalem Blutzucker kann ein frühes Zeichen dafür sein, dass dieser Ausgleich im Gange ist. Gelesen wird es als Teil eines Stoffwechselbilds und nicht für sich allein.",
- "omega-3-index": "Der Omega-3-Index ist der Anteil von EPA und DHA — den beiden langkettigen marinen Omega-3-Fettsäuren — an den Fettsäuren in den Membranen der roten Blutkörperchen. Da rote Blutkörperchen sich langsam erneuern, spiegelt er die langfristige Omega-3-Zufuhr wider und nicht eine kürzliche Mahlzeit. Ein höherer Index wurde in Beobachtungsstudien mit einem niedrigeren kardiovaskulären Risiko in Verbindung gebracht. Er wird als stabiler Marker des Omega-3-Status verfolgt, der über Monate auf die Ernährung reagiert."
+ "fasting-insulin": "Das Nüchterninsulin ist der Spiegel des Insulins (des Hormons, das Glukose aus dem Blut in die Zellen schleust), gemessen nach einer nächtlichen Nüchternphase. Zusammen mit dem Nüchternblutzucker hilft es, die Insulinresistenz abzuschätzen, also den Zustand, in dem der Körper mehr Insulin braucht, um den Blutzucker im Bereich zu halten. Ein hohes Nüchterninsulin bei normalem Blutzucker kann ein frühes Zeichen dafür sein, dass dieser Ausgleich im Gange ist. Gelesen wird es als Teil eines Stoffwechselbilds und nicht für sich allein.",
+ "omega-3-index": "Der Omega-3-Index ist der Anteil von EPA und DHA (den beiden langkettigen marinen Omega-3-Fettsäuren) an den Fettsäuren in den Membranen der roten Blutkörperchen. Da rote Blutkörperchen sich langsam erneuern, spiegelt er die langfristige Omega-3-Zufuhr wider und nicht eine kürzliche Mahlzeit. Ein höherer Index wurde in Beobachtungsstudien mit einem niedrigeren kardiovaskulären Risiko in Verbindung gebracht. Er wird als stabiler Marker des Omega-3-Status verfolgt, der über Monate auf die Ernährung reagiert."
},
"apob": "Apolipoprotein B (ApoB)",
"lp-a": "Lipoprotein(a)",
@@ -9184,7 +9531,7 @@
"acceptImageOnly": "JPEG, PNG oder WebP",
"extracting": "Befund wird gelesen…",
"readingOnDevice": "Wird auf deinem Gerät gelesen…",
- "localModeHint": "Wird auf deinem Gerät gelesen und als Text gesendet. Weniger genau — prüfe jeden Wert.",
+ "localModeHint": "Wird auf deinem Gerät gelesen und als Text gesendet. Weniger genau, prüfe jeden Wert.",
"rangeFromScan": "Referenzbereich aus dem Scan übernommen",
"rangeVerify": "Referenzbereich prüfen",
"reviewTitle": "Vor dem Speichern prüfen",
@@ -9199,12 +9546,12 @@
"dateLabel": "Datum",
"refLowLabel": "Referenz unten",
"refHighLabel": "Referenz oben",
- "newBiomarker": "Neuer Biomarker — wird angelegt",
+ "newBiomarker": "Neuer Biomarker, wird angelegt",
"linksExisting": "Verknüpft mit vorhandenem {name}",
"duplicateWarning": "Ein passender Wert existiert bereits",
"lowConfidence": "Prüfe diesen Wert",
"lowConfidenceBadge": "Geringe Sicherheit",
- "valueUnreadable": "Wert unleserlich — bitte manuell eingeben",
+ "valueUnreadable": "Wert unleserlich, bitte manuell eingeben",
"saveSelected": "{count} ausgewählte speichern",
"discardAll": "Alle verwerfen",
"savedToast": "{count} Laborwerte gespeichert",
@@ -9320,6 +9667,10 @@
"vaccinations": {
"label": "Impfungen",
"description": "Der Impfpass in strukturierter Form: Dosis, Serie, nächste Auffrischung."
+ },
+ "timeline": {
+ "label": "Zeitleiste",
+ "description": "Jahre auf einen Blick: Erkrankungen, Medikamente und Arztbesuche neben deinen Werten, dazu deine Lebensereignisse."
}
},
"moduleOff": {
@@ -9351,8 +9702,8 @@
},
"form": {
"date": "Datum",
- "catalogLabel": "Impfstoff (Katalog)",
- "identityHint": "Katalog wählen oder unten frei eintragen — eins genügt.",
+ "catalogLabel": "Impfstoff",
+ "identityHint": "Aus der Liste wählen oder unten frei eintragen. Eins genügt.",
"catalogNone": "Impfstoff wählen",
"catalogSearch": "Impfstoff suchen",
"catalogNoMatch": "Kein Katalogeintrag gefunden",
@@ -9429,7 +9780,7 @@
},
"empty": {
"title": "Noch keine Impfung erfasst",
- "description": "Übertrage deinen Impfpass Zeile für Zeile — Datum und Impfstoff genügen."
+ "description": "Übertrage deinen Impfpass Zeile für Zeile. Datum und Impfstoff genügen."
},
"catalog": {
"tetanus": "Tetanus (Wundstarrkrampf)",
@@ -9472,10 +9823,31 @@
"overdue": "Auffrischung fällig seit {date}",
"dueSoon": "Auffrischung fällig am {date}",
"current": "Nächste Auffrischung {date}"
+ },
+ "custom": {
+ "title": "Eigene Impfstoffe",
+ "add": "Eigenen Impfstoff anlegen",
+ "name": "Name",
+ "components": "Schützt vor",
+ "seriesDoses": "Dosen der Grundimmunisierung",
+ "boosterMonths": "Auffrischung alle (Monate)",
+ "saved": "Impfstoff gespeichert",
+ "deleted": "Impfstoff entfernt",
+ "deleteConfirm": "Diesen Impfstoff entfernen? Bereits eingetragene Impfungen behalten ihren Namen.",
+ "saveFailed": "Der Impfstoff konnte nicht gespeichert werden.",
+ "nameTaken": "Du hast schon einen Impfstoff mit diesem Namen.",
+ "editTitle": "Eigenen Impfstoff bearbeiten",
+ "description": "Ein Impfstoff, den der Katalog nicht kennt, gezählt wie jeder Katalogeintrag.",
+ "componentsHint": "Alle Krankheiten wählen, vor denen er schützt.",
+ "boosterHint": "Leer lassen, wenn keine Auffrischung nötig ist.",
+ "protectsAgainst": "Schützt vor {list}.",
+ "deleteTitle": "Eigenen Impfstoff entfernen?",
+ "sectionDescription": "Selbst angelegte Impfstoffe, die der Katalog nicht kennt.",
+ "catalogGroup": "Katalog"
}
},
"illness": {
- "title": "Krankheitstagebuch",
+ "title": "Krankheit",
"subtitle": "Ein rückblickendes Protokoll deiner Krankheits- und Beschwerde-Episoden.",
"settings": {
"viewDescription": "Wähle, ob Episoden als Karten oder als kompakte Liste erscheinen.",
@@ -9562,7 +9934,7 @@
},
"correlation": {
"title": "Wie es verlief",
- "learning": "Noch am Lernen — erfasse ein paar weitere Tage, um hier Muster zu sehen.",
+ "learning": "Noch am Lernen. Erfasse ein paar weitere Tage, um hier Muster zu sehen.",
"preOnsetTitle": "Wie es sich ankündigte",
"preOnsetBody": "Werte, die vor dem Beginn von deiner Baseline abwichen.",
"nadirTitle": "Was sich änderte",
@@ -9589,7 +9961,7 @@
"countOne": "Du hast in diesem Zeitraum 1 Episode erfasst.",
"countOther": "Du hast in diesem Zeitraum {count} Episoden erfasst.",
"typicalGap": "Typische Erholungslücke: {days} Tage zwischen Besserung und normalisierten Werten.",
- "typicalGapWithDriver": "Typische Erholungslücke: {days} Tage — du fühlst dich meist besser, bevor sich {vital} normalisiert.",
+ "typicalGapWithDriver": "Typische Erholungslücke: {days} Tage. Du fühlst dich meist besser, bevor sich {vital} normalisiert.",
"typicalGapLearning": "Noch ein paar abgeschlossene Episoden und hier erscheint eine typische Erholungslücke.",
"none": "In diesem Zeitraum keine Episoden erfasst.",
"analyze": "Erholungslücke analysieren"
@@ -9614,7 +9986,7 @@
"loadError": "Diese Episode konnte nicht geladen werden.",
"backToList": "Zurück zum Journal",
"detail": {
- "note": "Note"
+ "note": "Notiz"
},
"edit": {
"title": "Episode bearbeiten",
@@ -9650,8 +10022,8 @@
"stop": "Ich messe genug",
"accepted": "Erinnerung gesetzt",
"dismissed": "Okay, das schlage ich nicht noch einmal vor",
- "stopped": "Verstanden — ich schlage keine Erinnerungen mehr vor",
- "failed": "Hat nicht geklappt — bitte erneut versuchen",
+ "stopped": "Verstanden, ich schlage keine Erinnerungen mehr vor",
+ "failed": "Hat nicht geklappt. Bitte erneut versuchen",
"cadence": {
"weightDaily": "Wieg dich täglich, ungefähr zur selben Zeit",
"bp722": "Miss deinen Blutdruck eine Woche lang zweimal täglich",
@@ -9663,7 +10035,7 @@
"dismiss": "Jetzt nicht",
"applied": "Hinzugefügt",
"dismissed": "Verworfen",
- "failed": "Konnte das nicht hinzufügen — bitte erneut versuchen",
+ "failed": "Konnte das nicht hinzufügen. Bitte erneut versuchen",
"checkup": {
"title": "Vorsorge-Erinnerung hinzufügen?"
},
@@ -9691,9 +10063,9 @@
"reviewPrefix": "Check-in: {when}",
"accept": "Plan übernehmen",
"decline": "Ablehnen",
- "accepted": "Plan übernommen — der Coach behält ihn im Blick.",
+ "accepted": "Plan übernommen. Der Coach behält ihn im Blick.",
"declined": "Vorschlag abgelehnt.",
- "failed": "Das hat nicht geklappt — bitte erneut versuchen.",
+ "failed": "Das hat nicht geklappt. Bitte erneut versuchen.",
"markMet": "Ziel erreicht",
"abandon": "Plan beenden",
"remove": "Entfernen",
@@ -9714,11 +10086,11 @@
}
},
"refusal": {
- "outOfScope": "Ich kann nur bei den Gesundheitsdaten in deinem Log helfen — Messwerte, Medikamente, Stimmung, Trends. Fragen außerhalb dieses Themas beantworte ich nicht.",
- "promptInjection": "In deiner Nachricht stehen Anweisungen, die meine Vorgaben überschreiben sollen. Ich bin der HealthLog-Coach und fasse ausschließlich deine eigenen Gesundheitsdaten zusammen — formuliere deine Frage bitte in diesem Rahmen neu.",
+ "outOfScope": "Ich kann nur bei den Gesundheitsdaten in deinem Log helfen: Messwerte, Medikamente, Stimmung, Trends. Fragen außerhalb dieses Themas beantworte ich nicht.",
+ "promptInjection": "In deiner Nachricht stehen Anweisungen, die meine Vorgaben überschreiben sollen. Ich bin der HealthLog-Coach und fasse ausschließlich deine eigenen Gesundheitsdaten zusammen. Formuliere deine Frage bitte in diesem Rahmen neu.",
"conversationPoisoned": "Eine frühere Nachricht in dieser Unterhaltung enthält Anweisungen, die meine Vorgaben überschreiben sollen. Beginne bitte eine neue Unterhaltung."
},
- "noRecordedData": "Zu deiner Frage sind bei dir keine Messwerte erfasst, also kann ich dir dazu keine Zahl nennen. Sobald du etwas eingetragen hast, frag mich noch einmal — dann rechne ich mit deinen eigenen Werten.",
+ "noRecordedData": "Zu deiner Frage sind bei dir keine Messwerte erfasst, also kann ich dir dazu keine Zahl nennen. Sobald du etwas eingetragen hast, frag mich noch einmal, dann rechne ich mit deinen eigenen Werten.",
"step": {
"read": "Prüfe: {domain}",
"readWindow": "Prüfe: {domain}, {window}",
@@ -9726,9 +10098,6 @@
"showResult": "Öffne eine frühere Tabelle",
"reuse": "Nutze die Tabelle von vorhin",
"headerActive": "{label}…",
- "headerDoneOne": "1 Quelle angesehen",
- "headerDoneFew": "{count} Quellen angesehen",
- "headerDoneOther": "{count} Quellen angesehen",
"listLabel": "Was der Coach angesehen hat",
"toggleShow": "Anzeigen, was der Coach angesehen hat",
"toggleHide": "Ausblenden, was der Coach angesehen hat",
@@ -9754,7 +10123,9 @@
"illness": "Krankheit und Erholung",
"cycle": "Zyklus",
"correlations": "Zusammenhänge zwischen Messgrößen",
- "snapshot": "Datenübersicht"
+ "snapshot": "Datenübersicht",
+ "environment": "Wetter und Luft",
+ "day": "Ein Tag"
},
"window": {
"last7days": "letzte 7 Tage",
@@ -9878,7 +10249,8 @@
"relatedMetric": "Siehe auch: {metric}",
"continue": "Weitersuchen",
"settingLabel": "Anschlussfragen vorschlagen",
- "settingHint": "Zeigt unter der letzten Antwort des Coaches bis zu drei Vorschläge für die nächste Frage."
+ "settingHint": "Zeigt unter der letzten Antwort des Coaches bis zu drei Vorschläge für die nächste Frage.",
+ "changeAssumption": "Stattdessen: {value}"
},
"clarify": {
"cardLabel": "Der Coach hat eine Rückfrage",
@@ -9891,6 +10263,12 @@
"last90days": "Letzte 90 Tage",
"lastYear": "Letzte 12 Monate",
"allTime": "Gesamter Zeitraum"
+ },
+ "comparison": {
+ "baseline90d": "Mit meinen üblichen 90 Tagen vergleichen"
+ },
+ "anchor": {
+ "illness": "Seit der Erkrankung vom {date}"
}
},
"reuse": {
@@ -9925,7 +10303,7 @@
},
"mentalHealth": {
"pageTitle": "Seelisches Wohlbefinden",
- "pageDescription": "Vier validierte, freiwillige Selbsttests — aussagekräftig ist der Verlauf, nicht ein einzelner Wert.",
+ "pageDescription": "Vier validierte, freiwillige Selbsttests. Aussagekräftig ist der Verlauf, nicht ein einzelner Wert.",
"attributionLabel": "Quelle",
"attributionShow": "Quelle und Lizenz für {instrument}",
"choosePrompt": "Check-in auswählen",
@@ -9936,10 +10314,10 @@
"saved": "Gespeichert",
"error": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
"instrument": {
- "phq9": "PHQ-9 · Stimmung",
- "gad7": "GAD-7 · Sorgen",
- "who5": "WHO-5 · Wohlbefinden",
- "sci": "SCI · Schlaf"
+ "phq9": "PHQ-9 (Stimmung)",
+ "gad7": "GAD-7 (Sorgen)",
+ "who5": "WHO-5 (Wohlbefinden)",
+ "sci": "SCI (Schlaf)"
},
"instrumentSub": {
"phq9": "Depressions-Screening, 9 Fragen",
@@ -9960,7 +10338,7 @@
"2": "Sehr erschwert",
"3": "Extrem erschwert"
},
- "functionalOptionalHint": "Optional — du kannst auch ohne Antwort auf diese Frage absenden.",
+ "functionalOptionalHint": "Optional: Du kannst auch ohne Antwort auf diese Frage absenden.",
"result": {
"title": "Dein Ergebnis",
"totalLabel": "Gesamtwert",
@@ -10002,7 +10380,7 @@
},
"crisis": {
"title": "Du musst das nicht allein durchstehen",
- "intro": "Du hast Gedanken angegeben, lieber tot zu sein oder dir etwas anzutun. Es gibt jetzt sofort Unterstützung — sich zu melden ist ein Zeichen von Stärke.",
+ "intro": "Du hast Gedanken angegeben, lieber tot zu sein oder dir etwas anzutun. Es gibt jetzt sofort Unterstützung. Sich zu melden ist ein Zeichen von Stärke.",
"ifDanger": "Wenn du in unmittelbarer Gefahr bist, ruf jetzt {emergency} an.",
"resourcesTitle": "Menschen, die helfen können"
},
@@ -10150,7 +10528,7 @@
},
"openDetail": "Details anzeigen",
"historyLoadError": "Dein Testverlauf konnte nicht geladen werden",
- "historyLoadErrorHint": "Das heißt nicht, dass keine Ergebnisse vorliegen — die Liste konnte nicht abgerufen werden. Versuche es erneut.",
+ "historyLoadErrorHint": "Das heißt nicht, dass keine Ergebnisse vorliegen. Die Liste konnte nicht abgerufen werden. Versuche es erneut.",
"privacyNote": "Deine Antworten sind verschlüsselt gespeichert und erreichen den KI-Coach nicht."
},
"records": {
@@ -10380,12 +10758,34 @@
"daylight": "Tageslicht",
"precip": "Niederschlag",
"pressureMean": "Luftdruck",
- "pressureDelta": "Druckschwankung"
+ "pressureDelta": "Druckschwankung",
+ "pm25": "Feinstaub (PM2,5)",
+ "pm10": "Feinstaub (PM10)",
+ "no2": "Stickstoffdioxid",
+ "ozone8h": "Ozon (höchster 8-Stunden-Wert)",
+ "europeanAqi": "Europäischer Luftqualitätsindex",
+ "uvIndexMax": "UV-Index (Höchstwert)",
+ "dustMax": "Saharastaub",
+ "pollenMax": "Pollen (höchster Wert)"
+ },
+ "chip": {
+ "day": "Wetter am {date}",
+ "pollenHigh": "Viel Pollen: {kinds}",
+ "hotNight": "Warme Nacht",
+ "veryPoorAir": "Sehr schlechte Luft"
+ },
+ "pollen": {
+ "alder": "Erle",
+ "birch": "Birke",
+ "grass": "Gräser",
+ "mugwort": "Beifuß",
+ "olive": "Olive",
+ "ragweed": "Ambrosia"
}
},
"documents": {
"title": "Dokumente",
- "subtitle": "Jeder Brief, Befund und Scan an einem durchsuchbaren Ort — verschlüsselt gespeichert.",
+ "subtitle": "Jeder Brief, Befund und Scan an einem durchsuchbaren Ort, verschlüsselt gespeichert.",
"pageUpload": "Hochladen",
"ownerOnlyUpload": "Dokumente kann nur die Inhaberin oder der Inhaber der Akte hinzufügen.",
"kind": {
@@ -10402,14 +10802,16 @@
},
"filter": {
"searchLabel": "Dokumente durchsuchen",
- "searchPlaceholder": "Titel oder Dateiname suchen…",
+ "searchPlaceholder": "Titel, Dateiname",
"typeGroup": "Dokumenttyp",
"conditionGroup": "Erkrankung",
+ "procedureGroup": "Eingriffe und Operationen",
"yearGroup": "Jahr",
"clear": "Filter zurücksetzen",
"typeAll": "Alle Typen",
"typeCount": "{count} Typen",
"conditionAll": "Alle Erkrankungen",
+ "procedureAll": "Alle Eingriffe",
"yearAll": "Alle Jahre"
},
"card": {
@@ -10425,15 +10827,15 @@
},
"error": {
"fileTooLarge": "Diese Datei ist größer als das Limit von {maxSize}.",
- "quotaExceeded": "Der Speicher ist voll — {used} von {quota} belegt.",
+ "quotaExceeded": "Der Speicher ist voll: {used} von {quota} belegt.",
"unsupportedType": "Dieser Dateityp wird nicht unterstützt.",
"purged": "Dieses Dokument wurde endgültig entfernt und kann nicht wiederhergestellt werden.",
"duplicateExists": "Ein identisches Dokument ist bereits gespeichert.",
- "rateLimited": "Zu viele Uploads — versuche es in ein paar Minuten erneut.",
+ "rateLimited": "Zu viele Uploads. Versuche es in ein paar Minuten erneut.",
"generic": "Upload fehlgeschlagen. Bitte erneut versuchen."
},
"toast": {
- "duplicate": "Bereits gespeichert — das vorhandene Dokument wird hervorgehoben.",
+ "duplicate": "Bereits gespeichert. Das vorhandene Dokument ist hervorgehoben.",
"deleted": "Dokument gelöscht",
"restoreFailed": "Das Dokument konnte nicht wiederhergestellt werden."
},
@@ -10456,7 +10858,7 @@
},
"empty": {
"title": "Dein Dokumenten-Tresor ist leer",
- "description": "Bewahre Arztbriefe, Laborbefunde, Bildgebung, Rezepte und Impfnachweise auf — verschlüsselt und durchsuchbar nach Typ, Erkrankung und Jahr.",
+ "description": "Bewahre Arztbriefe, Laborbefunde, Bildgebung, Rezepte und Impfnachweise auf. Sie werden verschlüsselt gespeichert und sind nach Typ, Erkrankung und Jahr durchsuchbar.",
"action": "Erstes Dokument hochladen",
"noMatchesTitle": "Keine passenden Dokumente",
"noMatchesDescription": "Nichts entspricht der aktuellen Suche und den Filtern."
@@ -10487,6 +10889,7 @@
"saveError": "Die Änderung konnte nicht gespeichert werden.",
"deleteError": "Das Dokument konnte nicht gelöscht werden.",
"download": "Herunterladen",
+ "openInNewTab": "In neuem Tab öffnen",
"delete": "Löschen",
"titleLabel": "Titel",
"titlePlaceholder": "Titel hinzufügen",
@@ -10496,9 +10899,9 @@
"linkCondition": "Verknüpfen",
"noConditions": "Mit keiner Erkrankung verknüpft",
"previewUnavailable": "Keine Inline-Vorschau für diesen Dateityp",
- "uploadedMeta": "Hinzugefügt am {date} · {size}",
+ "uploadedMeta": "Hinzugefügt am {date}, {size}",
"importedFrom": "Importiert aus {system}",
- "importedFromWithId": "Importiert aus {system} · ID {id}",
+ "importedFromWithId": "Importiert aus {system}, ID {id}",
"sourceOther": "einem anderen System",
"share": "Teilen",
"labValuesLink": "{count} Laborwerte ansehen",
@@ -10565,7 +10968,7 @@
"errorNotIndexed": "Lies das Dokument zuerst aus, dann extrahiere.",
"errorLocalOcrDisabled": "Aktiviere zuerst die lokale Texterkennung in den KI-Einstellungen.",
"errorProvider": "Dafür ist kein KI-Anbieter eingerichtet.",
- "errorTextImageOnly": "Lokale Texterkennung liest nur Bilder — nicht diese Datei.",
+ "errorTextImageOnly": "Lokale Texterkennung liest nur Bilder, nicht diese Datei.",
"errorConsent": "Für das Lesen von Dokumenten mit KI brauchst du zuerst deine Einwilligung.",
"errorUnavailable": "Das Lesen von Dokumenten mit KI ist hier gerade nicht verfügbar."
},
@@ -10598,7 +11001,7 @@
"CONDITION": "Erkrankung",
"MEDICATION_STATEMENT": "Medikament"
},
- "statedRange": "angegebener Bereich {range}",
+ "statedRange": "Angegebener Bereich {range}",
"needsCheck": "Prüfe diesen Wert gegen das Dokument und speichere ihn dann.",
"verify": "Prüfen",
"verifySave": "Wert speichern",
@@ -10714,6 +11117,21 @@
"finishedHint": "Suche erneut oder ändere einen Filter, um weitere Dokumente auszuwählen.",
"maxReached": "Du kannst höchstens {max} Dokumente auf einmal importieren.",
"noMatchHint": "Versuche einen Teil des Namens oder setze Tag und Datum zurück."
+ },
+ "customize": "Darstellung anpassen",
+ "layout": {
+ "viewDescription": "Vorschaukarten mit Miniaturansicht oder eine kompakte Liste.",
+ "arrangementHeading": "Anordnung",
+ "arrangementDescription": "Jeder Monat als eigener Block oder alles fortlaufend über die ganze Breite.",
+ "stacked": "Monate untereinander",
+ "flow": "Fortlaufend",
+ "saveFailed": "Die Darstellung konnte nicht gespeichert werden."
+ },
+ "deleteConfirm": {
+ "titleOne": "Dieses Dokument löschen?",
+ "titleMany": "{count} Dokumente löschen?",
+ "body": "Direkt danach lässt sich das noch rückgängig machen. Nach 30 Tagen wird es endgültig entfernt.",
+ "action": "Löschen"
}
},
"selfDescription": {
@@ -10800,7 +11218,7 @@
"genericDescription": "Ein Messwert, den du verfolgst. Erfasse Werte, um ihn über die Zeit zu beobachten.",
"emptyTitle": "Noch keine Werte",
"emptyDescription": "Erfasse deinen ersten Wert, um diese Metrik darzustellen.",
- "valuesTitle": "{metric} — alle Werte",
+ "valuesTitle": "{metric}: alle Werte",
"valuesDescription": "Alle erfassten Werte dieser Metrik.",
"backToMetric": "Zurück zu {metric}"
}
@@ -10847,35 +11265,41 @@
"lastAttemptReason": {
"unitMismatch": "Die übermittelten Werte entsprachen nicht den Einheiten, die diese App für diese Nährstoffe erwartet, daher konnten sie nicht gespeichert werden.",
"valueOutOfRange": "Die übermittelten Mengen lagen höher, als diese App plausibel speichern kann, daher konnten sie nicht gesichert werden.",
- "dayInvalid": "Die Einträge trugen ein Datum, das der Server nicht akzeptieren konnte. Das kann passieren, wenn Uhrzeit oder Zeitzone auf der sendenden Seite falsch eingestellt sind — das lohnt sich zu prüfen.",
+ "dayInvalid": "Die Einträge trugen ein Datum, das der Server nicht akzeptieren konnte. Das kann passieren, wenn Uhrzeit oder Zeitzone auf der sendenden Seite falsch eingestellt sind. Das lohnt sich zu prüfen.",
"upsertFailed": "Diese Einträge haben den Server erreicht, aber ein Problem auf unserer Seite hat verhindert, dass sie gespeichert wurden. Du musst nichts weiter tun.",
"unknown": "Diese Einträge sind angekommen, konnten aber nicht gespeichert werden."
}
},
"hydration": {
- "referenceMeta": "Richtwert {value} {unit}/Tag · EFSA"
+ "referenceMeta": "Richtwert {value} {unit}/Tag (EFSA)"
},
"caffeine": {
- "ceilingMeta": "Sichere Obergrenze {value} {unit}/Tag · EFSA"
+ "ceilingMeta": "Sichere Obergrenze {value} {unit}/Tag (EFSA)"
},
"micronutrients": {
"title": "Vitamine & Mineralstoffe",
- "rowMeta": "{days} von {window} Tagen · Richtwert {value} {unit} (EFSA)",
+ "rowMeta": "{days} von {window} Tagen, Richtwert {value} {unit} (EFSA)",
"rowMetaNoRef": "{days} von {window} Tagen",
"footer": "{tracked} von {total} erfassten Mikronährstoffen haben Daten in diesem Zeitraum.",
"emptyTitle": "Noch keine Mikronährstoffdaten synchronisiert",
"loadError": "Deine Nährstoffdaten konnten nicht geladen werden"
},
- "sparseNote": "Ein niedriger Wert bedeutet hier meist lückenhafte Erfassung, keinen Mangel — die Tagessummen aus Apple Health spiegeln wider, was deine Apps erfasst haben, nicht die gesamte Nahrungsaufnahme."
+ "sparseNote": "Ein niedriger Wert bedeutet hier meist lückenhafte Erfassung, keinen Mangel. Die Tagessummen aus Apple Health spiegeln wider, was deine Apps erfasst haben, nicht die gesamte Nahrungsaufnahme."
},
"daily": {
"line": {
- "score": "Dein Gesundheitsscore heute liegt bei {score}.",
- "allClear": "Heute braucht nichts deine Aufmerksamkeit."
+ "allClear": "Heute braucht nichts deine Aufmerksamkeit.",
+ "scoreReady": "Dein Gesundheitsscore für heute ist da."
},
"push": {
"title": "Dein Morgenüberblick",
- "bodyProvisional": "{line} · Der Schlaf von letzter Nacht fehlt noch."
+ "bodyProvisional": "{line}\nDer Schlaf von letzter Nacht fehlt noch.",
+ "titleScore": "Morgenscore {score}",
+ "band": {
+ "green": "Heute sieht es stabil aus.",
+ "yellow": "Heute lohnt sich ein Blick auf ein paar Dinge.",
+ "red": "Heute brauchen einige Bereiche Aufmerksamkeit."
+ }
},
"item": {
"doseWindow": {
@@ -10893,27 +11317,27 @@
},
"syncIssue": {
"title": "Synchronisierung braucht Aufmerksamkeit",
- "body": "{integration} synchronisiert nicht — verbinde neu, um deine Daten aktuell zu halten."
+ "body": "{integration} synchronisiert nicht. Verbinde neu, um deine Daten aktuell zu halten."
},
"coachCheckin": {
"title": "Zeit, einen Plan anzuschauen",
- "body": "Es ist etwa eine Woche her, seit du diesen Plan gesetzt hast — behalte ihn, passe ihn an oder lass ihn los. Ganz ohne Druck.",
- "bodyPlan": "Vor etwa einer Woche hast du dir vorgenommen: „{plan}“. Behalte es, passe es an oder lass es los — ganz ohne Druck.",
+ "body": "Es ist etwa eine Woche her, seit du diesen Plan gesetzt hast. Behalte ihn, passe ihn an oder lass ihn los. Ganz ohne Druck.",
+ "bodyPlan": "Vor etwa einer Woche hast du dir vorgenommen: „{plan}“. Behalte es, passe es an oder lass es los, ganz ohne Druck.",
"adjustPrompt": "Ich möchte meinen Check-in-Plan anpassen.",
"adjustPromptPlan": "Ich möchte diesen Plan anpassen: „{plan}“"
},
"tensionWindow": {
"title": "Vielleicht eine angespannte Phase",
"body": {
- "morning": "Dein Puls lag heute Vormittag über längere Zeit über deinem Ruhewert, während du ruhig warst — möglicherweise etwas Anspannung.",
- "afternoon": "Dein Puls lag heute Nachmittag über längere Zeit über deinem Ruhewert, während du ruhig warst — möglicherweise etwas Anspannung.",
- "evening": "Dein Puls lag heute Abend über längere Zeit über deinem Ruhewert, während du ruhig warst — möglicherweise etwas Anspannung.",
- "night": "Dein Puls lag in der Nacht über längere Zeit über deinem Ruhewert, während du ruhig warst — möglicherweise etwas Anspannung."
+ "morning": "Dein Puls lag heute Vormittag über längere Zeit über deinem Ruhewert, während du ruhig warst, möglicherweise etwas Anspannung.",
+ "afternoon": "Dein Puls lag heute Nachmittag über längere Zeit über deinem Ruhewert, während du ruhig warst, möglicherweise etwas Anspannung.",
+ "evening": "Dein Puls lag heute Abend über längere Zeit über deinem Ruhewert, während du ruhig warst, möglicherweise etwas Anspannung.",
+ "night": "Dein Puls lag in der Nacht über längere Zeit über deinem Ruhewert, während du ruhig warst, möglicherweise etwas Anspannung."
}
},
"ecgNewRecording": {
"title": "Neue EKG-Aufzeichnung",
- "body": "Dein Gerät hat ein neues EKG aufgezeichnet — bereit zum Ansehen.",
+ "body": "Dein Gerät hat ein neues EKG aufgezeichnet, bereit zum Ansehen.",
"bodyVerdict": "Dein Gerät hat ein neues EKG aufgezeichnet und meldet: {verdict}.",
"verdict": {
"irregular": "einen möglichen unregelmäßigen Rhythmus",
@@ -10928,14 +11352,14 @@
},
"body": {
"below": "Bis {time} stehst du bei {today} Schritten. An einem üblichen Tag sind es um diese Zeit etwa {typical}.",
- "above": "Bis {time} stehst du bei {today} Schritten — mehr als die {typical}, die um diese Zeit für dich üblich sind."
+ "above": "Bis {time} stehst du bei {today} Schritten, mehr als die {typical}, die um diese Zeit für dich üblich sind."
}
},
"upcomingVisit": {
"title": "Termin steht an",
- "bodyToday": "{what} — heute",
- "bodyTomorrow": "{what} — morgen",
- "bodyDayAfterTomorrow": "{what} — übermorgen"
+ "bodyToday": "{what}, heute",
+ "bodyTomorrow": "{what}, morgen",
+ "bodyDayAfterTomorrow": "{what}, übermorgen"
}
},
"action": {
@@ -10954,10 +11378,34 @@
"scoreLabel": "Gesundheitsscore",
"deltaVsBaseline": "{delta} ggü. deinem Ausgangswert",
"deltaFlat": "Im Bereich deines Ausgangswerts",
- "sleepPending": "Der Schlaf von letzter Nacht fehlt noch — der heutige Wert ist vorläufig.",
+ "sleepPending": "Der Schlaf von letzter Nacht fehlt noch, der heutige Wert ist vorläufig.",
"worthALook": "Einen Blick wert",
"allClear": "Heute braucht nichts deine Aufmerksamkeit.",
- "ringLink": "{metric}-Details öffnen"
+ "ringLink": "{metric}-Details öffnen",
+ "heading": "Heute",
+ "steadyWeeksOne": "Seit {count} Woche stabil",
+ "steadyWeeksFew": "Seit {count} Wochen stabil",
+ "steadyWeeksOther": "Seit {count} Wochen stabil",
+ "valueWithUnit": "{value} {unit}",
+ "rangeWithUnit": "{low} bis {high} {unit}",
+ "rangeAtThisTime": "{range} für diese Tageszeit",
+ "vital": {
+ "RESTING_HEART_RATE": "Ruhepuls",
+ "HEART_RATE_VARIABILITY": "Herzfrequenzvariabilität",
+ "RESPIRATORY_RATE": "Atemfrequenz",
+ "OXYGEN_SATURATION": "Sauerstoffsättigung",
+ "BODY_TEMPERATURE": "Körpertemperatur",
+ "SKIN_TEMPERATURE": "Hauttemperatur",
+ "BLOOD_PRESSURE_SYS": "Systolischer Blutdruck",
+ "BLOOD_PRESSURE_DIA": "Diastolischer Blutdruck",
+ "BLOOD_GLUCOSE": "Blutzucker",
+ "PULSE": "Puls",
+ "WEIGHT": "Gewicht",
+ "generic": "Ein Vitalwert"
+ },
+ "steadyAtLeastWeeksOne": "Seit mindestens {count} Woche stabil",
+ "steadyAtLeastWeeksFew": "Seit mindestens {count} Wochen stabil",
+ "steadyAtLeastWeeksOther": "Seit mindestens {count} Wochen stabil"
},
"milestone": {
"metric": {
@@ -10969,11 +11417,11 @@
},
"record": {
"title": "Neue Bestmarke: {metric}",
- "body": "Heute ein persönlicher Bestwert für {metric} — ganz ruhig festgehalten, du musst nichts tun."
+ "body": "Heute ein persönlicher Bestwert für {metric}, ganz ruhig festgehalten, du musst nichts tun."
},
"returnToBaseline": {
"title": "Wieder im Bereich: {metric}",
- "body": "{metric} liegt wieder in deinem gewohnten Bereich. Was auch immer es war — es ist vorbei."
+ "body": "{metric} liegt wieder in deinem gewohnten Bereich. Was auch immer es war, es ist vorbei."
},
"sustainedInRange": {
"week1": {
@@ -10993,6 +11441,68 @@
"body": "Ein ganzer Monat mit {metric} in deinem gewohnten Bereich. Wunderbar gleichmäßig."
}
}
+ },
+ "lead": {
+ "vitalOutside": {
+ "above": "{metric} liegt bei {value}, über deinem üblichen Bereich von {range}.",
+ "below": "{metric} liegt bei {value}, unter deinem üblichen Bereich von {range}."
+ },
+ "vitalsOutside": {
+ "aboveOne": "{metric} liegt bei {value}, über deinem üblichen Bereich von {range}, und {count} weiterer Vitalwert liegt ebenfalls außerhalb.",
+ "aboveFew": "{metric} liegt bei {value}, über deinem üblichen Bereich von {range}, und {count} weitere Vitalwerte liegen ebenfalls außerhalb.",
+ "aboveOther": "{metric} liegt bei {value}, über deinem üblichen Bereich von {range}, und {count} weitere Vitalwerte liegen ebenfalls außerhalb.",
+ "belowOne": "{metric} liegt bei {value}, unter deinem üblichen Bereich von {range}, und {count} weiterer Vitalwert liegt ebenfalls außerhalb.",
+ "belowFew": "{metric} liegt bei {value}, unter deinem üblichen Bereich von {range}, und {count} weitere Vitalwerte liegen ebenfalls außerhalb.",
+ "belowOther": "{metric} liegt bei {value}, unter deinem üblichen Bereich von {range}, und {count} weitere Vitalwerte liegen ebenfalls außerhalb."
+ },
+ "sleep": {
+ "more": "Letzte Nacht hast du {duration} geschlafen, {diff} mehr als sonst.",
+ "less": "Letzte Nacht hast du {duration} geschlafen, {diff} weniger als sonst.",
+ "usual": "Letzte Nacht hast du {duration} geschlafen, etwa so viel wie sonst.",
+ "plain": "Letzte Nacht hast du {duration} geschlafen."
+ },
+ "vitalsInRangeOne": "Dein letzter Vitalwert liegt in seinem üblichen Bereich.",
+ "vitalsInRangeFew": "Alle {count} letzten Vitalwerte liegen in ihrem üblichen Bereich.",
+ "vitalsInRangeOther": "Alle {count} letzten Vitalwerte liegen in ihrem üblichen Bereich.",
+ "vitalInRange": "{metric} liegt bei {value}, innerhalb deines üblichen Bereichs von {range}."
+ },
+ "todayFact": {
+ "restMode": {
+ "label": "Ruhemodus",
+ "value": "Tag {day}"
+ },
+ "medications": {
+ "label": "Medikamente",
+ "value": "{taken} von {scheduled} genommen"
+ },
+ "appointment": {
+ "label": "Termin",
+ "today": "Heute {time}, {what}",
+ "tomorrow": "Morgen {time}, {what}"
+ },
+ "sleep": {
+ "label": "Letzte Nacht",
+ "plain": "{duration}",
+ "usual": "{duration}, etwa wie sonst",
+ "more": "{duration}, {diff} mehr als sonst",
+ "less": "{duration}, {diff} weniger als sonst"
+ },
+ "vitals": {
+ "label": "Vitalwerte",
+ "allInOne": "{count} geprüft, im Bereich",
+ "allInFew": "{count} geprüft, alle im Bereich",
+ "allInOther": "{count} geprüft, alle im Bereich",
+ "oneOut": {
+ "above": "{metric} über deinem Bereich",
+ "below": "{metric} unter deinem Bereich"
+ },
+ "someOut": "{count} von {total} außerhalb deines Bereichs"
+ },
+ "cycle": {
+ "label": "Zyklus",
+ "value": "{phase}, Tag {day}",
+ "dayOnly": "Tag {day}"
+ }
}
},
"reportSelection": {
@@ -11122,7 +11632,7 @@
"errorScope": "So lassen sich diese Bereiche nicht teilen. Wähle mindestens einen Bereich aus oder teile die gesamte Akte.",
"errorManageBrowserOnly": "Verwaltenden Zugriff kannst du nur im Browser vergeben.",
"errorStepUp": "Bestätige deinen zweiten Faktor und versuche es erneut.",
- "errorOffline": "Das hat den Server nicht erreicht. Deine Einladung steht noch hier — prüfe deine Verbindung und versuche es erneut."
+ "errorOffline": "Das hat den Server nicht erreicht. Deine Einladung steht noch hier. Prüfe deine Verbindung und versuche es erneut."
},
"given": {
"title": "Wer meine Akte öffnen kann",
@@ -11177,7 +11687,7 @@
"managed": {
"title": "Profile, die du betreust",
"description": "Gesundheitsakten ohne eigenen Login.",
- "explainer": "Ein verwaltetes Profil ist eine Gesundheitsakte für jemanden, der sich nicht selbst anmeldet — ein Kind oder eine Person, um die du dich kümmerst. Es hat keinen Login und keine E-Mail-Adresse, und du wirst die erste betreuende Person. Später kannst du weitere hinzufügen, und ihr alle könnt in die Akte wechseln, um sie zu pflegen.",
+ "explainer": "Ein verwaltetes Profil ist eine Gesundheitsakte für jemanden, der sich nicht selbst anmeldet, etwa ein Kind oder eine Person, um die du dich kümmerst. Es hat keinen Login und keine E-Mail-Adresse, und du wirst die erste betreuende Person. Später kannst du weitere hinzufügen, und ihr alle könnt in die Akte wechseln, um sie zu pflegen.",
"nameLabel": "Name",
"namePlaceholder": "Der Name, an dem du diese Akte erkennst",
"nameHint": "Bis zu 80 Zeichen. Wird überall dort angezeigt, wo diese Akte auftaucht.",
@@ -11199,7 +11709,7 @@
"creating": "Wird angelegt…",
"created": "{name} ist eingerichtet. Über das Kontomenü kannst du in diese Akte wechseln.",
"errorStepUp": "Bestätige deinen zweiten Faktor und versuche es erneut.",
- "errorOffline": "Das hat den Server nicht erreicht. Es hat sich nichts geändert — prüfe deine Verbindung und versuche es erneut.",
+ "errorOffline": "Das hat den Server nicht erreicht. Es hat sich nichts geändert. Prüfe deine Verbindung und versuche es erneut.",
"errorInvalid": "Der Server hat diese Angaben abgelehnt. Prüfe Name, Datum und Zeitzone.",
"errorFailed": "Das Profil konnte nicht angelegt werden.",
"edit": "Bearbeiten",
@@ -11210,7 +11720,7 @@
"editErrorFailed": "Die Änderungen konnten nicht gespeichert werden.",
"delete": "Profil löschen",
"deleteTitle": "{name} löschen?",
- "deleteBody": "Alles, was für {name} erfasst wurde, verschwindet mit dem Profil — Messwerte, Medikamente, Dokumente und Notizen. Das lässt sich nicht rückgängig machen.",
+ "deleteBody": "Alles, was für {name} erfasst wurde, verschwindet mit dem Profil: Messwerte, Medikamente, Dokumente und Notizen. Das lässt sich nicht rückgängig machen.",
"deleteGuardiansOne": "{count} Person betreut diese Akte derzeit und verliert sie.",
"deleteGuardiansFew": "{count} Personen betreuen diese Akte derzeit und verlieren sie.",
"deleteGuardiansOther": "{count} Personen betreuen diese Akte derzeit und verlieren sie.",
@@ -11225,7 +11735,7 @@
"guardianRemoveBody": "{name} kann {profile} dann nicht mehr öffnen. Was diese Person eingetragen hat, bleibt in der Akte.",
"guardianWithdrawBody": "Die Einladung an {name} wird zurückgezogen. Die Person hat {profile} nie geöffnet, in der Akte ändert sich nichts.",
"guardianRemoveConfirm": "Zugriff beenden",
- "soleGuardian": "Du bist die einzige Person, die sich um {name} kümmert — hier gibt es also keinen Zugriff, den du beenden könntest. Lade jemanden ein und warte auf die Annahme, gib deinen eigenen Zugriff unter „Akten, die ich öffnen kann“ zurück, oder lösche das Profil.",
+ "soleGuardian": "Du bist die einzige Person, die sich um {name} kümmert. Hier gibt es also keinen Zugriff, den du beenden könntest. Lade jemanden ein und warte auf die Annahme, gib deinen eigenen Zugriff unter „Akten, die ich öffnen kann“ zurück, oder lösche das Profil.",
"guardianInviteLabel": "Weitere betreuende Person einladen",
"guardianInvite": "Einladung senden",
"guardianInviting": "Wird gesendet…",
@@ -11239,7 +11749,47 @@
"guardianErrorFailed": "Die Einladung konnte nicht gesendet werden.",
"errorMfaRequired": "Dafür ist ein zweiter Faktor nötig, und dein Konto hat noch keinen. Richte unter Einstellungen → Sicherheit einen ein und versuche es dann erneut.",
"rosterLoadError": "Es ließ sich nicht laden, wer sich um diese Akte kümmert.",
- "errorRateLimit": "Zu viele Profile in der letzten Stunde angelegt. Warte etwas und versuche es erneut."
+ "errorRateLimit": "Zu viele Profile in der letzten Stunde angelegt. Warte etwas und versuche es erneut.",
+ "handover": {
+ "action": "Übergeben",
+ "title": "An {name} übergeben",
+ "description": "Gib {name} eine eigene Anmeldung. Nichts wird kopiert oder verschoben: Die Akte wird zum eigenen Konto.",
+ "guardiansLabel": "Zugriff nach der Übergabe",
+ "guardiansHint": "Dein Vorschlag. {name} sieht ihn und entscheidet nach der Anmeldung.",
+ "access": {
+ "end": "Kein Zugriff",
+ "read": "Nur ansehen",
+ "manage": "Verwalten"
+ },
+ "expiryLabel": "Link gültig für",
+ "expiry1": "1 Tag",
+ "expiry7": "7 Tage",
+ "expiry14": "14 Tage",
+ "minorHint": "{name} ist unter 16. Es lohnt sich, den Lesezugriff zu behalten und den Schritt gemeinsam zu besprechen.",
+ "irreversible": "Sobald {name} den Link benutzt, lässt sich die Übergabe nicht rückgängig machen. Die Akte gehört dann {name}, und {name} entscheidet, wer Zugriff behält.",
+ "afterwards": "{name} bestätigt den Hinweis und jede KI-Einwilligung selbst. Erinnerungen für diese Akte erreichen dich nicht mehr.",
+ "replaceNote": "Ein neuer Link zieht den offenen zurück.",
+ "create": "Link erstellen",
+ "creating": "Wird erstellt…",
+ "linkOnce": "Dieser Link wird nur jetzt angezeigt. Gib ihn {name} persönlich oder über einen Weg, dem du vertraust, oder lass den Code scannen.",
+ "qrAlt": "QR-Code des Übergabelinks für {name}",
+ "urlLabel": "Übergabelink",
+ "copy": "Kopieren",
+ "copied": "Link kopiert",
+ "copyError": "Der Link ließ sich nicht kopieren. Markiere ihn und kopiere ihn von Hand.",
+ "linkExpires": "Gültig bis {date}.",
+ "done": "Fertig",
+ "pending": "Ein Übergabelink ist bis {date} offen.",
+ "replace": "Neuer Link",
+ "revoke": "Link zurückziehen",
+ "revoked": "Link zurückgezogen",
+ "revokeFailed": "Der Link ließ sich nicht zurückziehen. Versuche es erneut.",
+ "errorFailed": "Der Übergabelink ließ sich nicht erstellen.",
+ "errorRosterChanged": "Inzwischen hat sich geändert, wer die Akte betreut. Schließe das hier und öffne es erneut.",
+ "oidcOnly": "Ein Profil zu übergeben ist nicht möglich, solange dieser Server nur Single Sign-on erlaubt.",
+ "loadError": "Der Stand der Übergabe ließ sich nicht laden.",
+ "entryHint": "Wenn {name} die Akte selbst führen möchte."
+ }
},
"toast": {
"savedTo": "In der Akte von {name} gespeichert"
@@ -11358,7 +11908,13 @@
"symptomEventDelete": "{name} hat einen Symptom-Eintrag gelöscht",
"symptomDefinitionCreate": "{name} hat ein Symptom angelegt",
"symptomDefinitionUpdate": "{name} hat ein Symptom geändert",
- "symptomDefinitionDelete": "{name} hat ein Symptom entfernt"
+ "symptomDefinitionDelete": "{name} hat ein Symptom entfernt",
+ "customVaccineCreate": "{name} hat einen eigenen Impfstoff angelegt",
+ "customVaccineUpdate": "{name} hat einen eigenen Impfstoff bearbeitet",
+ "customVaccineDelete": "{name} hat einen eigenen Impfstoff entfernt",
+ "lifeEventCreate": "{name} hat ein Lebensereignis hinzugefügt",
+ "lifeEventUpdate": "{name} hat ein Lebensereignis geändert",
+ "lifeEventDelete": "{name} hat ein Lebensereignis entfernt"
},
"section": {
"measurements": {
@@ -11396,7 +11952,7 @@
},
"actionError": {
"retry": "Erneut versuchen",
- "offline": "Das hat den Server nicht erreicht. Es hat sich nichts geändert — prüfe deine Verbindung und versuche es erneut.",
+ "offline": "Das hat den Server nicht erreicht. Es hat sich nichts geändert. Prüfe deine Verbindung und versuche es erneut.",
"rateLimited": "Zu viele Versuche hintereinander. Warte eine Minute und versuche es erneut.",
"acceptExpired": "Diese Einladung ist abgelaufen. Bitte um eine neue.",
"acceptWithdrawn": "Diese Einladung wurde zurückgezogen.",
@@ -11408,12 +11964,28 @@
"notFound": "Diesen Zugriff gibt es nicht mehr.",
"revokeFailed": "Der Zugriff konnte nicht beendet werden.",
"renounceFailed": "Der Zugriff konnte nicht zurückgegeben werden.",
- "profileNotFound": "Dieses Profil gibt es nicht mehr — es wurde vielleicht schon gelöscht, oder dein Zugriff darauf ist beendet.",
+ "profileNotFound": "Dieses Profil gibt es nicht mehr. Es wurde vielleicht schon gelöscht, oder dein Zugriff darauf ist beendet.",
"deleteProfileFailed": "Das Profil konnte nicht gelöscht werden.",
"removeGuardianFailed": "Dieser Zugriff konnte nicht beendet werden."
},
"anamnesis": {
"relativeNote": "Ein Eintrag zur Familienanamnese beschreibt die Angehörigen dieser Person. Trage ihn nur ein, wenn sie das hier festgehalten haben möchte."
+ },
+ "handoverDecision": {
+ "title": "Wer Zugriff auf deine Akte behält",
+ "description": "Entscheide, was die Personen, die deine Akte geführt haben, ab jetzt dürfen.",
+ "proposed": "Vorgeschlagen: {access}",
+ "settled": "Bereits unter Geteilter Zugriff geändert",
+ "empty": "Niemand sonst hatte Zugriff auf deine Akte.",
+ "later": "Du kannst das jederzeit unter Einstellungen, Geteilter Zugriff ändern.",
+ "save": "Bestätigen",
+ "saving": "Wird gespeichert…",
+ "decideLater": "Später",
+ "errorFailed": "Deine Entscheidung ließ sich nicht speichern. Versuche es erneut.",
+ "loadError": "Wer deine Akte bisher betreut hat, ließ sich nicht laden.",
+ "notificationsTitle": "Richte deine eigenen Erinnerungen ein",
+ "notificationsBody": "Erinnerungen für diese Akte gingen bisher an die Personen, die sie geführt haben. Ab jetzt erreichen sie dich, sobald du einen Benachrichtigungskanal einrichtest.",
+ "notificationsAction": "Benachrichtigungen öffnen"
}
},
"encounters": {
@@ -11434,7 +12006,7 @@
"both": "Beidseitig",
"none": "Keine Angabe"
},
- "bodySiteWithSide": "{site} · {side}",
+ "bodySiteWithSide": "{site} ({side})",
"sectionTitle": "Termine",
"sectionDescription": "Arztbesuche und gebuchte Termine an einem Ort.",
"add": "Termin",
@@ -11449,7 +12021,7 @@
"created": "Termin gespeichert",
"updated": "Termin aktualisiert",
"saveFailed": "Termin konnte nicht gespeichert werden.",
- "checkupNotClosed": "Termin gespeichert. Die Vorsorge bleibt offen — dafür reicht deine Freigabe nicht.",
+ "checkupNotClosed": "Termin gespeichert. Die Vorsorge bleibt offen, dafür reicht deine Freigabe nicht.",
"status": {
"planned": "Geplant",
"done": "Stattgefunden",
@@ -11466,7 +12038,7 @@
"dateDerivesStatusHint": "Ein Datum in der Zukunft ist ein gebuchter Termin.",
"status": "Status",
"practitioner": "Praxis oder Arzt",
- "practitionerHint": "Optional — du kannst den Namen später nachtragen.",
+ "practitionerHint": "Optional, du kannst den Namen später nachtragen.",
"practitionerNone": "Keine Angabe",
"practitionerSearch": "Name oder Praxis suchen",
"practitionerNoMatch": "Kein Eintrag gefunden.",
@@ -11524,10 +12096,9 @@
},
"practitioners": {
"title": "Praxen und Ärzte",
- "description": "Dein eigenes Adressbuch — einmal tippen, überall auswählen.",
+ "description": "Dein eigenes Adressbuch: einmal tippen, überall auswählen.",
"linkLabel": "Adressbuch",
- "add": "Eintrag",
- "searchPlaceholder": "Name, Praxis oder Fachrichtung suchen",
+ "searchPlaceholder": "Name, Praxis oder Fach suchen",
"loadError": "Adressbuch konnte nicht geladen werden.",
"emptyTitle": "Noch kein Eintrag",
"emptyDescription": "Lege eine Praxis an, damit du sie beim Termin auswählen kannst.",
@@ -11623,7 +12194,6 @@
"unreadableLabel": "Unlesbares Symptom",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Symptom erfassen",
"sheetDescription": "Eines deiner eigenen Symptome, wie stark und wann.",
"symptom": "Symptom",
"symptomRequired": "Wähle ein Symptom.",
@@ -11672,5 +12242,426 @@
"title": "Erfasste Symptome",
"filed": "zu dieser Erkrankung erfasst"
}
+ },
+ "day": {
+ "scoreOfMax": "von {max}",
+ "cycleDay": "Zyklustag {n}",
+ "cyclePhaseLine": {
+ "MENSTRUAL": "Zyklustag {n}, Menstruation",
+ "FOLLICULAR": "Zyklustag {n}, Follikelphase",
+ "OVULATORY": "Zyklustag {n}, Ovulation",
+ "LUTEAL": "Zyklustag {n}, Lutealphase"
+ },
+ "previousDay": "Vortag",
+ "nextDay": "Folgetag",
+ "close": "Tag schließen",
+ "hidePanel": "Tag ausblenden",
+ "showPanel": "{date} einblenden",
+ "openDay": "Tag öffnen",
+ "viewWholeDay": "Ganzen Tag ansehen",
+ "captureForDay": "Für diesen Tag erfassen",
+ "empty": "Für diesen Tag gibt es keine Einträge.",
+ "notShared": "Einige Bereiche sind für dich nicht freigegeben.",
+ "inTimeline": "In der Zeitleiste",
+ "askCoach": "Im Coach nach diesem Tag fragen",
+ "loadFailed": "Dieser Tag konnte nicht geladen werden.",
+ "dayOf": "Tag {n} von {m}",
+ "dayN": "Tag {n}",
+ "usualRange": "Üblich {lo} bis {hi}",
+ "groups": {
+ "scores": "Scores",
+ "running": "Laufend an diesem Tag",
+ "values": "Werte",
+ "happened": "Was war"
+ },
+ "notable": {
+ "extremeHigh": "Höchster Tageswert seit {since}",
+ "extremeLow": "Niedrigster Tageswert seit {since}",
+ "firstValue": "Erster Wert dieser Art",
+ "gap": "{days} Tage ohne Wert"
+ },
+ "previousDayShortcut": "Vortag (Alt + Pfeil links)",
+ "nextDayShortcut": "Folgetag (Alt + Pfeil rechts)",
+ "captureShort": "Erfassen",
+ "inTimelineShort": "Zeitleiste",
+ "askCoachPrompt": "Was war am {date}?",
+ "coachChips": "Tage in dieser Antwort",
+ "description": "Alles, was deine Einträge für diesen Tag enthalten.",
+ "openDocument": "Dokument öffnen",
+ "previousReading": "Vorher, {date}",
+ "sheetExpand": "Auf volle Höhe aufziehen",
+ "sheetShrink": "Verkleinern",
+ "valuesLegend": "Punkt für diesen Tag, Strich für die 30 Tage davor",
+ "valuesAllOne": "Alle Werte",
+ "valuesAllFew": "Alle {count} Werte",
+ "valuesAllOther": "Alle {count} Werte",
+ "valuesFewer": "Weniger Werte",
+ "countEntriesOne": "1 Eintrag",
+ "countEntriesFew": "{count} Einträge",
+ "countEntriesOther": "{count} Einträge",
+ "meanOfReadingsOne": "Schnitt aus 1 Messung",
+ "meanOfReadingsFew": "Schnitt aus {count} Messungen",
+ "meanOfReadingsOther": "Schnitt aus {count} Messungen",
+ "sinceLastVisit": {
+ "title": "Seit dem letzten Besuch am {date}",
+ "countOne": "1 Eintrag",
+ "countFew": "{count} Einträge",
+ "countOther": "{count} Einträge",
+ "allOne": "Alle anzeigen",
+ "allFew": "Alle {count} anzeigen",
+ "allOther": "Alle {count} anzeigen",
+ "fewer": "Weniger anzeigen",
+ "note": "Zusammengestellt aus deinen Einträgen, ohne Bewertung. Jedes Datum öffnet den ganzen Tag.",
+ "report": "Arztbericht {date} bis heute"
+ },
+ "travel": "Unterwegs",
+ "event": {
+ "started": "{label} begonnen",
+ "ended": "{label} beendet",
+ "paused": "{label} pausiert",
+ "resumed": "{label} wieder aufgenommen",
+ "illnessBegan": "{label} begann",
+ "illnessResolved": "{label} abgeklungen"
+ },
+ "pickDate": "Datum wählen",
+ "pickerTitle": "Zu einem Tag springen",
+ "today": "Heute",
+ "hasEntries": "mit Einträgen",
+ "dayNSince": "Tag {n}, seit {date}",
+ "sinceDate": "Seit {date}"
+ },
+ "timeline": {
+ "title": "Zeitleiste",
+ "subtitle": "Was wann war, neben deinen Werten.",
+ "standing": "Dauerhaft",
+ "startUnknown": "Beginn unbekannt",
+ "zoom": {
+ "all": "Alles",
+ "year": "12 Monate",
+ "quarter": "3 Monate",
+ "range": "Zeitraum"
+ },
+ "lanes": {
+ "life": "Leben",
+ "illness": "Erkrankungen",
+ "allergies": "Allergien",
+ "medications": "Medikamente",
+ "vaccinations": "Impfungen",
+ "visits": "Arzt und Eingriffe",
+ "labs": "Labor",
+ "documents": "Dokumente",
+ "cycle": "Zyklus"
+ },
+ "readiness": {
+ "title": "Was deine Zeitleiste schon trägt",
+ "subtitle": "Eine ehrliche Bestandsaufnahme, bevor du sie öffnest.",
+ "menuEntry": "Datenlage",
+ "hint": "Leere Spuren bleiben ausgeblendet, bis etwas da ist. Abgeschaltete Module erscheinen nie. Du findest diese Übersicht später unter Datenlage in der Zeitleiste.",
+ "later": "Später",
+ "open": "Zeitleiste öffnen",
+ "verdict": {
+ "carries": "Trägt schon.",
+ "thin": "Noch dünn. Die Zeitleiste zeigt vorerst vor allem deine Werte."
+ },
+ "status": {
+ "carries": "trägt",
+ "thin": "dünn",
+ "empty": "leer"
+ },
+ "lanes": {
+ "values": "Werte",
+ "mood": "Stimmung",
+ "environment": "Umwelt",
+ "life": "Lebensereignisse"
+ },
+ "meter": "{carrying} von {total} Spuren tragen",
+ "carriesDetail": "{carrying} von {total} Spuren reichen für einen Überblick ab {since}.",
+ "loadFailed": "Die Bestandsaufnahme konnte nicht geladen werden.",
+ "card": {
+ "title": "Was die Zeitleiste schon trägt",
+ "summary": "Sie reicht bis {since} zurück, {carrying} von {total} Spuren tragen schon.",
+ "dismiss": "Ausblenden"
+ },
+ "detail": {
+ "valuesOne": "{count} Messgröße seit {since}",
+ "valuesFew": "{count} Messgrößen seit {since}",
+ "valuesOther": "{count} Messgrößen seit {since}",
+ "illnessOne": "{count} Episode, davon {chronic} chronisch",
+ "illnessFew": "{count} Episoden, davon {chronic} chronisch",
+ "illnessOther": "{count} Episoden, davon {chronic} chronisch",
+ "allergiesOne": "{count} Allergie",
+ "allergiesFew": "{count} Allergien",
+ "allergiesOther": "{count} Allergien",
+ "medicationsOne": "{count} Medikament, {missingStart} ohne Startdatum",
+ "medicationsFew": "{count} Medikamente, {missingStart} ohne Startdatum",
+ "medicationsOther": "{count} Medikamente, {missingStart} ohne Startdatum",
+ "medicationsDatedOne": "{count} Medikament mit Startdatum",
+ "medicationsDatedFew": "{count} Medikamente mit Startdatum",
+ "medicationsDatedOther": "{count} Medikamente mit Startdatum",
+ "vaccinationsOne": "{count} Impfung",
+ "vaccinationsFew": "{count} Impfungen",
+ "vaccinationsOther": "{count} Impfungen",
+ "visitsOne": "{count} Besuch, {procedures} mit Eingriff",
+ "visitsFew": "{count} Besuche, {procedures} mit Eingriff",
+ "visitsOther": "{count} Besuche, {procedures} mit Eingriff",
+ "visitsPlannedOne": "{count} Besuch, nur geplant",
+ "visitsPlannedFew": "{count} Besuche, nur geplant",
+ "visitsPlannedOther": "{count} Besuche, nur geplant",
+ "labsOne": "{count} Befundtag",
+ "labsFew": "{count} Befundtage",
+ "labsOther": "{count} Befundtage",
+ "documentsOne": "{count} Dokument mit Datum",
+ "documentsFew": "{count} Dokumente mit Datum",
+ "documentsOther": "{count} Dokumente mit Datum",
+ "lifeOne": "{count} Lebensereignis",
+ "lifeFew": "{count} Lebensereignisse",
+ "lifeOther": "{count} Lebensereignisse",
+ "cycleOne": "{count} Zyklus",
+ "cycleFew": "{count} Zyklen",
+ "cycleOther": "{count} Zyklen",
+ "moodOne": "{count} Eintrag seit {since}",
+ "moodFew": "{count} Einträge seit {since}",
+ "moodOther": "{count} Einträge seit {since}",
+ "environmentOne": "{count} Tag mit Umweltdaten",
+ "environmentFew": "{count} Tage mit Umweltdaten",
+ "environmentOther": "{count} Tage mit Umweltdaten",
+ "empty": "Noch keine"
+ },
+ "gap": {
+ "valuesEmpty": "Quellen verbinden",
+ "illnessEmpty": "Episode anlegen",
+ "medicationsWithoutStart": "Ergänzen",
+ "visitsEmpty": "Besuch nachtragen",
+ "documentsUndated": "Ansehen",
+ "labsEmpty": "Befund erfassen",
+ "vaccinationsEmpty": "Impfpass erfassen",
+ "lifeEventsEmpty": "Hinzufügen"
+ },
+ "gapLine": {
+ "medicationsWithoutStartOne": "{count} Medikament ohne Startdatum",
+ "medicationsWithoutStartFew": "{count} Medikamente ohne Startdatum",
+ "medicationsWithoutStartOther": "{count} Medikamente ohne Startdatum",
+ "documentsUndatedOne": "{count} Dokument ohne Datum",
+ "documentsUndatedFew": "{count} Dokumente ohne Datum",
+ "documentsUndatedOther": "{count} Dokumente ohne Datum",
+ "valuesEmpty": "Noch keine Werte über längere Zeit",
+ "illnessEmpty": "Noch keine Erkrankungen mit Beginn",
+ "visitsEmpty": "Noch keine erledigten Arztbesuche",
+ "labsEmpty": "Noch keine Laborbefunde",
+ "vaccinationsEmpty": "Noch keine Impfungen",
+ "lifeEventsEmpty": "Noch keine Lebensereignisse"
+ }
+ },
+ "legendSpan": "Zeitraum",
+ "legendEvent": "Ereignis",
+ "legendMeanQuarter": "Quartalsmittel",
+ "legendMeanMonth": "Monatsmittel",
+ "legendMeanWeek": "Wochenmittel",
+ "legendThin": "Weniger als 3 Messungen",
+ "legendGap": "Kurze Lücke ohne Messungen",
+ "today": "Heute",
+ "noDataYet": "Noch keine Daten",
+ "layers": "Ebenen",
+ "layersLanes": "Spuren",
+ "zoomLabel": "Zeitraum der Ansicht",
+ "chartLabel": "Zeitleiste von {from} bis {to}",
+ "keyboardHint": "Pfeiltasten verschieben die Auswahl, Pos1 und Ende springen an die Enden, Eingabe öffnet den Tag.",
+ "loadFailed": "Die Zeitleiste konnte nicht geladen werden.",
+ "empty": {
+ "title": "Noch nichts auf der Zeitleiste",
+ "description": "Sobald es Einträge mit Datum gibt, erscheinen sie hier."
+ },
+ "values": {
+ "menuTitle": "Werte unter den Spuren",
+ "trigger": "Werte ({count})",
+ "noneInWindow": "Keine Werte in diesem Zeitraum",
+ "series": {
+ "BLOOD_PRESSURE_SYS": "Blutdruck sys.",
+ "BLOOD_PRESSURE_DIA": "Blutdruck dia."
+ },
+ "noValue": "kein Wert",
+ "bloodPressure": "Blutdruck",
+ "readingsOne": "1 Messung",
+ "readingsFew": "Schnitt aus {count} Messungen",
+ "readingsOther": "Schnitt aus {count} Messungen"
+ },
+ "table": {
+ "caption": "Einträge der Zeitleiste",
+ "lane": "Spur",
+ "entry": "Eintrag",
+ "start": "Beginn",
+ "end": "Ende",
+ "seriesCaption": "Mittelwerte nach Zeitraum",
+ "period": "Zeitraum"
+ },
+ "selection": {
+ "empty": "Keine Einträge in diesem Zeitraum",
+ "since": "seit {date}",
+ "range": "{from} bis {to}",
+ "mean": "Ø {values}",
+ "week": "Woche vom {date}",
+ "hintClick": "Klicke auf einen Zeitpunkt, um ihn hier zu sehen",
+ "hintTap": "Tippe auf einen Zeitpunkt, um ihn hier zu sehen"
+ },
+ "chronicle": {
+ "months": "Monate",
+ "years": "Jahre",
+ "groupingLabel": "Gruppierung",
+ "empty": "Keine Einträge von {from} bis {to}",
+ "emptySingle": "{period}: keine Einträge",
+ "ended": "{label} vorbei",
+ "afterDaysOne": "nach einem Tag",
+ "afterDaysFew": "nach {count} Tagen",
+ "afterDaysOther": "nach {count} Tagen",
+ "notable": {
+ "extremeHigh": "Höchster Wert seit Langem",
+ "extremeLow": "Niedrigster Wert seit Langem",
+ "firstValue": "Erster Wert einer Messgröße",
+ "gap": "Längere Lücke in den Werten"
+ },
+ "notableMeta": "Tag öffnen für Einzelheiten"
+ },
+ "standingAllergiesOne": "{count} Allergie",
+ "standingAllergiesFew": "{count} Allergien",
+ "standingAllergiesOther": "{count} Allergien",
+ "legendMeanDay": "Tagesmittel",
+ "range": {
+ "from": "Von",
+ "to": "Bis",
+ "label": "Gewählter Zeitraum",
+ "open": "Zeitraum wählen",
+ "clear": "Alles zeigen",
+ "apply": "Übernehmen"
+ },
+ "item": {
+ "labValuesOne": "1 Laborwert",
+ "labValuesFew": "{count} Laborwerte",
+ "labValuesOther": "{count} Laborwerte"
+ }
+ },
+ "lifeEvents": {
+ "title": "Lebensereignis",
+ "add": "Lebensereignis",
+ "privacy": "Titel und Notiz liegen verschlüsselt. Sie gehen an keine KI.",
+ "emptyHint": "Umzug, Geburt oder eine neue Stelle geben den Jahren einen Anker.",
+ "save": "Speichern",
+ "cancel": "Abbrechen",
+ "delete": "Lebensereignis löschen",
+ "saved": "Lebensereignis gespeichert",
+ "deleted": "Lebensereignis gelöscht",
+ "fields": {
+ "title": "Was war?",
+ "category": "Art",
+ "when": "Wann",
+ "precision": "Genauigkeit",
+ "addEnd": "Enddatum, wenn es ein Zeitraum war",
+ "endDate": "Bis",
+ "note": "Notiz",
+ "optionalTag": "optional",
+ "notePlaceholder": "Etwas, das du später wissen willst",
+ "removeEnd": "Enddatum entfernen"
+ },
+ "category": {
+ "FAMILY": "Familie",
+ "HOME": "Wohnen",
+ "WORK": "Arbeit",
+ "LOSS": "Verlust",
+ "OTHER": "Sonstiges"
+ },
+ "precision": {
+ "DAY": "Tag",
+ "MONTH": "Monat",
+ "YEAR": "Jahr"
+ },
+ "edit": "Lebensereignis bearbeiten",
+ "errors": {
+ "titleRequired": "Gib dem Ereignis einen Namen.",
+ "titleTooLong": "Höchstens 120 Zeichen.",
+ "categoryRequired": "Wähle eine Art.",
+ "dateRequired": "Gib ein gültiges Datum an.",
+ "endBeforeStart": "Das Ende liegt vor dem Beginn.",
+ "noteTooLong": "Höchstens 2000 Zeichen."
+ },
+ "saveFailed": "Speichern hat nicht geklappt.",
+ "deleteFailed": "Löschen hat nicht geklappt.",
+ "deleteConfirm": {
+ "title": "Lebensereignis löschen?",
+ "body": "Titel und Notiz sind danach nicht mehr da."
+ }
+ },
+ "palette": {
+ "title": "Suche",
+ "description": "Finde eine Seite, eine Einstellung, ein Modul, eine Aktion oder einen Eintrag in deinen Daten.",
+ "placeholder": "Seiten, Einstellungen, deine Einträge suchen…",
+ "results": "Treffer",
+ "noResults": "Nichts gefunden für „{query}“.",
+ "moduleOn": "An",
+ "moduleOff": "Aus",
+ "hintMove": "Wählen",
+ "hintOpen": "Öffnen",
+ "hintClose": "Schließen",
+ "groups": {
+ "recent": "Zuletzt",
+ "pages": "Seiten",
+ "insights": "Insights",
+ "settings": "Einstellungen",
+ "modules": "Module",
+ "actions": "Aktionen",
+ "records": "In deinen Einträgen"
+ },
+ "actions": {
+ "capture": "Neuer Eintrag",
+ "coach": "Coach fragen",
+ "backup": "Backup erstellen",
+ "today": "Heute öffnen",
+ "shortcuts": "Tastenkürzel zeigen"
+ },
+ "records": {
+ "loading": "Deine Einträge werden durchsucht…",
+ "empty": "Nichts in deinen Einträgen für „{query}“.",
+ "error": "Deine Einträge konnten nicht durchsucht werden. Versuch es gleich noch einmal.",
+ "truncated": "Nur die neuesten Einträge wurden durchsucht.",
+ "kind": {
+ "visit": "Arztbesuch",
+ "condition": "Erkrankung",
+ "document": "Dokument",
+ "vaccination": "Impfung",
+ "lab": "Laborwert",
+ "medication": "Medikament",
+ "symptom": "Symptom",
+ "workout": "Training",
+ "note": "Notiz"
+ },
+ "source": {
+ "mood": "Stimmungsnotiz",
+ "measurement": "Notiz zu einem Messwert",
+ "illness": "Krankheitstagebuch"
+ }
+ }
+ },
+ "shortcuts": {
+ "title": "Tastenkürzel",
+ "description": "Tasten, die überall in der App funktionieren, außer während du in ein Feld tippst.",
+ "menuEntry": "Tastenkürzel",
+ "then": "dann",
+ "search": "Suchen",
+ "help": "Diese Liste zeigen",
+ "close": "Dialog oder Bereich schließen",
+ "previousDay": "Vorheriger Tag",
+ "nextDay": "Nächster Tag",
+ "toggleDay": "Tag öffnen oder schließen",
+ "dayHint": "Vorheriger und nächster Tag gehen, solange ein Tag offen ist.",
+ "groups": {
+ "navigation": "Navigation",
+ "actions": "Aktionen",
+ "day": "Tag"
+ }
+ },
+ "aboutPage": {
+ "eyebrow": "Info",
+ "title": "Über HealthLog",
+ "lastUpdated": "Zuletzt aktualisiert: {date}",
+ "project": "Projekt",
+ "credits": "Danksagungen"
}
}
diff --git a/messages/en.json b/messages/en.json
index 3cd9aa9a4..7191e99c1 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -49,7 +49,9 @@
"dismiss": "Dismiss",
"moreInfo": "More information",
"durationHoursMinutes": "{hours}h {minutes}m",
- "durationMinutes": "{minutes}m"
+ "durationMinutes": "{minutes}m",
+ "durationClockMinutes": "{time} min",
+ "durationClockHours": "{time} h"
},
"apiErrors": {
"measurement": {
@@ -165,7 +167,7 @@
"anamnesisShiftSchedule": "Work schedule",
"anamnesisColFact": "Profile item",
"anamnesisColValue": "Recorded value",
- "subtitle": "HealthLog — Personal Health Report",
+ "subtitle": "HealthLog: Personal Health Report",
"patient": "Patient",
"dateOfBirth": "Date of birth",
"gender": "Gender",
@@ -211,18 +213,18 @@
"contacts": "Emergency contacts",
"notes": "Additional notes"
},
- "vitalsTitle": "Vital signs — Summary",
+ "vitalsTitle": "Vital signs: summary",
"colParameter": "Parameter",
"colCurrent": "Current",
"colAvgPeriod": "Avg. period",
"colMin": "Min",
"colMax": "Max",
"colN": "n",
- "bpClassificationTitle": "Blood pressure — ESH classification (2023)",
+ "bpClassificationTitle": "Blood pressure: ESH classification (2023)",
"avgBp": "Average blood pressure",
"classification": "Classification",
"bmiTitle": "Body Mass Index (BMI)",
- "bmiRow": "BMI: {bmi} kg/m² — Classification: {class} (WHO)",
+ "bmiRow": "BMI: {bmi} kg/m², Classification: {class} (WHO)",
"complianceTitle": "Medication adherence",
"glp1Title": "GLP-1 therapy",
"glp1WeightSummary": "Weight over report period: {start} kg → {end} kg ({delta} kg)",
@@ -240,7 +242,7 @@
"colExpected": "Expected",
"colComplianceRate": "Adherence rate",
"moodTitle": "Mood trajectory",
- "moodSummary": "Average: {avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+ "moodSummary": "Average {avg} / 5.0, {count} entries, range {min}–{max}",
"colMood": "Mood",
"colCount": "Count",
"colShare": "Share",
@@ -270,8 +272,8 @@
"typeGlucoseRandom": "Glucose (random)",
"typeGlucoseBedtime": "Glucose (bedtime)",
"typeGlucoseUnspecified": "Glucose (untagged)",
- "glucoseClassificationTitle": "Blood glucose — reference ranges (ADA 2024 / DDG)",
- "glucoseRow": "{label}: avg {avg} {unit} (target {rangeMin}–{rangeMax}) — {class}",
+ "glucoseClassificationTitle": "Blood glucose: reference ranges (ADA 2024 / DDG)",
+ "glucoseRow": "{label}: avg {avg} {unit} (target {rangeMin}–{rangeMax}), {class}",
"glucoseInTarget": "in target",
"glucoseBelowTarget": "below target",
"glucoseAboveTarget": "above target",
@@ -323,7 +325,7 @@
"gmi": "Glucose Management Indicator (GMI)",
"eA1c": "Estimated A1C",
"cv": "Variability (CV%)",
- "spotCaveat": "These are spot-reading estimates from individual measurements, not a continuous-monitor profile — read them as a direction of travel rather than a clinical verdict."
+ "spotCaveat": "These are spot-reading estimates from individual measurements, not a continuous-monitor profile. Read them as a direction of travel rather than a clinical verdict."
},
"illnessTitle": "Conditions & illnesses",
"illnessOngoing": "Ongoing",
@@ -376,12 +378,12 @@
"backToDashboard": "Back to dashboard"
},
"offlineBanner": {
- "message": "No connection — changes can't be saved until you're back online.",
- "saveFailed": "Couldn't save — you're offline. Please try again once you're back online.",
+ "message": "No connection: changes can't be saved until you're back online.",
+ "saveFailed": "Couldn't save, you're offline. Please try again once you're back online.",
"showingCached": "Showing your last synced data."
},
"demoBanner": {
- "message": "Demo mode — changes are not saved."
+ "message": "Demo mode: changes are not saved."
},
"nav": {
"dashboard": "Dashboard",
@@ -390,7 +392,7 @@
"insights": "Insights",
"achievements": "Achievements",
"settings": "Settings",
- "admin": "Admin Console",
+ "admin": "Admin console",
"logout": "Sign out",
"login": "Sign in",
"notifications": "Notifications",
@@ -407,6 +409,7 @@
"mood": "Mood",
"coach": "Coach",
"skipToContent": "Skip to content",
+ "breadcrumb": "You are here",
"sidebar": "Sidebar",
"mainNavigation": "Main navigation",
"mobileNavigation": "Mobile navigation",
@@ -426,7 +429,9 @@
"workout": "Workout",
"workoutDescription": "A session your devices did not record",
"symptom": "Symptom",
- "symptomDescription": "One of your own symptoms, with how strong"
+ "symptomDescription": "One of your own symptoms, with how strong",
+ "lifeEventDescription": "A move, a birth, a new job",
+ "lifeEvent": "Life event"
},
"cycle": "Cycle",
"labs": "Labs",
@@ -435,7 +440,8 @@
"illness": "Illness",
"documents": "Documents",
"vaccinations": "Vaccinations",
- "mentalWellbeing": "Mental wellbeing"
+ "mentalWellbeing": "Mental wellbeing",
+ "timeline": "Timeline"
},
"auth": {
"login": "Sign in",
@@ -452,14 +458,13 @@
"hasAccount": "Already have an account?",
"loginFailed": "Login failed",
"registerFailed": "Registration failed",
- "invitedBanner": "You have been invited — create your account even while registration is otherwise closed.",
+ "invitedBanner": "You have been invited. Create your account even while registration is otherwise closed.",
"alreadySignedIn": {
"title": "You are already signed in",
- "body": "You are signed in as {username}. An invitation creates a new account — sign out to accept it.",
+ "body": "You are signed in as {username}. An invitation creates a new account. Sign out to accept it.",
"signOut": "Sign out and accept the invitation"
},
"createAccount": "Create account",
- "newAccount": "Create new account",
"backToPasskey": "Back to Passkey",
"passkeySetupLater": "You can set up passkeys at any time in Settings.",
"passwordMinLength": "Min. 12 characters",
@@ -512,6 +517,37 @@
"fixLan": "Server on a home network without TLS: set SESSION_COOKIE_SECURE=false in its settings and restart it.",
"fixHttps": "Server behind an HTTPS proxy: open the https:// address instead, and make sure the proxy forwards X-Forwarded-Proto: https.",
"localhostNote": "This page is on http://localhost while the server sets its sign-in cookie for https:// only. Most browsers accept that on localhost; if signing in keeps returning here, set SESSION_COOKIE_SECURE=false."
+ },
+ "claim": {
+ "title": "Take over your health record",
+ "titleNamed": "Welcome, {name}",
+ "description": "Your health record has been looked after for you until now. Choose how you sign in and it becomes your own account. Nothing is copied or moved.",
+ "loading": "Checking your link…",
+ "invalid": "This link no longer works",
+ "invalidHint": "It is unknown, expired, withdrawn or already used. Ask the person who sent it for a new one.",
+ "oidcOnly": "This server only allows single sign-on, so a profile cannot be taken over here yet.",
+ "signedInTitle": "Sign out first",
+ "signedInBody": "You are signed in as {username}. This link sets up a sign-in for someone else, so sign out before you use it.",
+ "signOut": "Sign out and continue",
+ "guardiansTitle": "Who keeps access for now",
+ "guardiansHint": "Proposed by the people who looked after your record. You decide right after signing in.",
+ "guardianUnnamed": "A guardian",
+ "usernameHint": "3 to 30 letters, numbers, _ or -.",
+ "usernameReserved": "Usernames starting with managed- are reserved.",
+ "emailHint": "Required, so you can get back in if you forget your password.",
+ "consentNote": "You confirm the disclaimer and any use of AI yourself. Until you do, nothing from your record is sent to an AI provider.",
+ "expires": "This link is valid until {date}.",
+ "submit": "Take over",
+ "submitting": "Taking over…",
+ "errorTaken": "That username or email address is already in use.",
+ "errorRateLimit": "Too many attempts. Wait a few minutes and try again.",
+ "errorFailed": "That did not work. Try again in a moment.",
+ "errorOffline": "That did not reach the server. Check your connection and try again."
+ },
+ "inviteInvalid": {
+ "title": "Invitation invalid",
+ "description": "This invitation link is incomplete or was copied wrongly. Ask the person who invited you for a new link.",
+ "toLogin": "Go to sign in"
}
},
"dashboard": {
@@ -543,17 +579,17 @@
"sleepShort": "Sleep",
"stepsShort": "Steps",
"vo2MaxShort": "VO₂ max",
- "bpInTargetWindow90": "BP in Target · 90d",
- "bpInTargetWindowDynamic": "BP in Target · {days}d",
+ "bpInTargetWindow90": "BP in Target (90d)",
+ "bpInTargetWindowDynamic": "BP in Target ({days}d)",
"bpInTargetCollecting": "collecting data",
"medications": "Medications",
"quickAddMeasurement": "Log measurement",
"quickAddMood": "Log mood",
"quickAddMedicationIntake": "Log medication intake",
"quickAddWorkout": "Log workout",
+ "quickAddLifeEvent": "Log life event",
"customizeDashboard": "Customize dashboard",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Log medication intake",
"sheetDescription": "Record dose, time, and medication in a few taps.",
"medicationLabel": "Medication",
"medicationPlaceholder": "Select a medication",
@@ -666,7 +702,8 @@
"body": "Without a copy of the key, your data cannot be recovered after a reinstall. It takes a minute.",
"action": "Open the step"
},
- "quickAddSymptom": "Log symptom"
+ "quickAddSymptom": "Log symptom",
+ "todayCardVisible": "Show the top card"
},
"measurements": {
"title": "Measurements",
@@ -738,7 +775,7 @@
"formReset": "Reset form",
"saveError": "Error saving",
"duplicateTimestamp": "A measurement of this type already exists at the selected timestamp.",
- "serverOwnedSource": "Values from a connected source can't be edited — the timestamp and note stay editable.",
+ "serverOwnedSource": "Values from a connected source can't be edited, but the timestamp and note stay editable.",
"deleteError": "Error deleting",
"deletedToast": "Measurement deleted",
"restoredToast": "Measurement restored",
@@ -833,11 +870,12 @@
"durationHoursOnly": "{hours} h",
"durationMinutesOnly": "{minutes} min",
"durationInvalid": "Enter a duration like 7.5, 7:30 or 7h 30m.",
- "durationTooLong": "One entry can't be longer than 24 hours."
+ "durationTooLong": "One entry can't be longer than 24 hours.",
+ "sourceHealthConnect": "Health Connect"
},
"metricCatalog": {
"title": "What HealthLog can track",
- "description": "Every metric domain HealthLog supports, whether or not you have data for it yet — and what would supply it.",
+ "description": "Every metric domain HealthLog supports, whether or not you have data for it yet, and what would supply it.",
"statusPresent": "Tracked",
"statusAbsent": "Not tracked yet",
"statusModuleOff": "Off",
@@ -845,11 +883,11 @@
"connectCta": "Connect a source",
"viewCta": "View",
"ecg": {
- "source": "Single-lead ECG strips from a compatible watch — Withings ScanWatch today — synced automatically."
+ "source": "Single-lead ECG strips from a compatible watch (Withings ScanWatch today), synced automatically."
},
"watchdog": {
"title": "Automatic screening",
- "description": "HealthLog also watches quietly in the background — irregular-rhythm and breathing-disturbance notifications from a connected watch, baseline-drift health-status alerts, and notable shifts between your two most recent lab panels — and surfaces a card only when something stands out. With at least one of those sources connected, no card showing is a good sign, not a missing feature; with none connected yet, there's simply nothing being watched."
+ "description": "HealthLog also watches quietly in the background: irregular-rhythm and breathing-disturbance notifications from a connected watch, baseline-drift health-status alerts, and notable shifts between your two most recent lab panels. It surfaces a card only when something stands out. With at least one of those sources connected, no card showing is a good sign, not a missing feature; with none connected yet, there's simply nothing being watched."
}
},
"mood": {
@@ -1006,7 +1044,7 @@
"archive": "Archive",
"restore": "Restore",
"archivedTitle": "Archived tags",
- "archivedDescription": "Archived tags stay in your past entries — they only disappear from the picker.",
+ "archivedDescription": "Archived tags stay in your past entries. They only disappear from the picker.",
"archivedEmpty": "Nothing archived.",
"purgeTitle": "Delete tag permanently?",
"purgeBody": "This removes the tag from {count} past entries. This cannot be undone.",
@@ -1031,7 +1069,7 @@
},
"sectionMoreTags": "More tags",
"sectionNoteDetails": "Note & details",
- "quickTagsHelp": "Your frequent tags — one tap",
+ "quickTagsHelp": "Your frequent tags, one tap",
"quickTagsExpand": "More",
"loadError": "Could not load your mood entries.",
"sectionLevelA": "State of mind",
@@ -1191,7 +1229,8 @@
"bodyLogged": "Logged in the illness journal: {count} symptoms",
"bodyNotLogged": "Nothing is logged in the illness journal for this day",
"openIllness": "Open"
- }
+ },
+ "sourceNotification": "Notification"
},
"medications": {
"title": "Medications",
@@ -1209,8 +1248,8 @@
"dialogDescription": "These medications are due now. Confirm to record all of them as taken.",
"confirm": "Take all ({count})",
"successToast": "{count} intakes recorded",
- "partialToast": "{taken} recorded, {failed} failed – please try again",
- "failedToast": "No intake recorded – please try again"
+ "partialToast": "{taken} recorded, {failed} failed. Please try again",
+ "failedToast": "No intake recorded. Please try again"
},
"logIntake": {
"dialogTitle": "Log an intake",
@@ -1284,6 +1323,14 @@
"disabledToast": "Intake tracking off",
"toggleFailed": "Could not change intake tracking."
},
+ "autoMarkTaken": {
+ "label": "Record doses automatically",
+ "helper": "HealthLog records each scheduled dose as taken at its time. You can still change or skip a dose by hand. No reminders are sent.",
+ "cardHint": "Recorded automatically",
+ "enabledToast": "Doses are now recorded automatically",
+ "disabledToast": "Doses are no longer recorded automatically",
+ "toggleFailed": "Could not change automatic recording."
+ },
"withoutNotification": "Without notification",
"pausedSince": "Paused since",
"takeNow": "Take now",
@@ -1299,7 +1346,7 @@
"complianceInfoLabel": "What does adherence mean?",
"lastIntake": "Last intake:",
"nextIntake": "Next intake:",
- "nextIntakeOverdue": "Overdue · {time} — can still be taken",
+ "nextIntakeOverdue": "Overdue since {time}, can still be taken",
"dayStreak": "day streak",
"dayStreakOne": "day streak",
"cycleDueToday": "Due today",
@@ -1308,10 +1355,10 @@
"skipped": "Skipped",
"intakeToastTaken": "{name} recorded as taken",
"intakeToastSkipped": "{name} skipped",
- "intakeToastFailed": "Could not record {name} — try again",
+ "intakeToastFailed": "Could not record {name}. Try again",
"intakeUndo": "Undo",
"intakeUndone": "Entry reverted",
- "intakeUndoFailed": "Could not undo — open the history to fix it",
+ "intakeUndoFailed": "Could not undo. Open the history to fix it",
"glp1LastInjection": "{label}",
"glp1NextInjectionDays": "{label} (in {days} days)",
"glp1NextInjectionToday": "today",
@@ -1393,10 +1440,10 @@
"error": {
"empty": "Type a description first.",
"tooLong": "Keep the description under 2000 characters.",
- "rateLimit": "Too many requests — wait a moment and try again.",
- "noProvider": "No AI provider configured — set one up in Settings.",
- "budget": "Daily AI budget reached — try again tomorrow.",
- "network": "Could not reach the AI provider — please retry.",
+ "rateLimit": "Too many requests. Wait a moment and try again.",
+ "noProvider": "No AI provider configured. Set one up in Settings.",
+ "budget": "Daily AI budget reached. Try again tomorrow.",
+ "network": "Could not reach the AI provider. Please retry.",
"consent": "Reading your description with AI needs your consent first.",
"unavailable": "Reading descriptions with AI is not available here right now."
}
@@ -1412,7 +1459,7 @@
"everyNMonths": "Every N months on a specific day",
"yearly": "Once a year",
"rolling": "Every N days from when I last took it (flexible)",
- "rollingExplainer": "Counts from your last logged intake — pauses if you skip a dose.",
+ "rollingExplainer": "Counts from your last logged intake; pauses if you skip a dose.",
"oneShot": "One-time dose"
},
"weekdays": {
@@ -1470,7 +1517,7 @@
"night": "Night"
},
"max": {
- "reached": "Maximum reached — remove a time before adding another."
+ "reached": "Maximum reached. Remove a time before adding another."
}
},
"courseWindow": {
@@ -1520,7 +1567,7 @@
},
"step3": {
"title": "What's the dose?",
- "subline": "A tablet, a puff, a drop — depending on the form.",
+ "subline": "A tablet, a puff, a drop, depending on the form.",
"amountLabel": "Amount",
"amountPlaceholder": "e.g. 5",
"unitLabel": "Unit",
@@ -1546,7 +1593,7 @@
},
"dosesPerUnitLabel": "Doses per unit",
"dosesPerUnitPlaceholder": "e.g. 4",
- "dosesPerUnitHint": "Doses per pen / vial — leave empty for no inventory tracking.",
+ "dosesPerUnitHint": "Doses per pen / vial. Leave empty for no inventory tracking.",
"short": "Dose",
"unitsPerDoseLabel": "Units per dose",
"unitsPerDoseHint": "e.g. 2 tablets of 2 mg for a 4 mg dose, or ½ a tablet for a split dose"
@@ -1558,7 +1605,7 @@
},
"step5": {
"title": "How often do you take it?",
- "subline": "Pick the cadence — the details follow on the next step.",
+ "subline": "Pick the cadence. The details follow on the next step.",
"short": "Cadence"
},
"step6": {
@@ -1580,7 +1627,7 @@
},
"step7": {
"title": "When do you take it?",
- "subline": "One or more times of day — reminders follow this list.",
+ "subline": "One or more times of day. Reminders follow this list.",
"presetsLabel": "Time-of-day presets",
"presets": {
"morning": "Morning",
@@ -1588,7 +1635,7 @@
"evening": "Evening",
"night": "Night"
},
- "overrideHint": "Optional — set a different dose or unit count just for this time of day. Leave blank to use the medication's default.",
+ "overrideHint": "Optional: set a different dose or unit count just for this time of day. Leave blank to use the medication's default.",
"doseOverrideLabel": "Dose for this time (optional)",
"doseOverridePlaceholder": "e.g. half a tablet",
"unitsOverrideLabel": "Units consumed at this time",
@@ -1596,11 +1643,11 @@
"short": "Times"
},
"step8": {
- "title": "All done — should HealthLog remind you?",
+ "title": "All done. Should HealthLog remind you?",
"subline": "Review everything below before you save.",
"remindersLabel": "Reminders on",
- "remindersDescription": "Push, Telegram or ntfy — based on your settings.",
- "noChannelHint": "Reminders need a notification channel — none is active right now.",
+ "remindersDescription": "Push, Telegram or ntfy, based on your settings.",
+ "noChannelHint": "Reminders need a notification channel, and none is active right now.",
"noChannelHintLink": "Set up notifications",
"short": "Done"
}
@@ -1649,7 +1696,7 @@
},
"asNeeded": {
"label": "As needed",
- "description": "No fixed plan — log an intake whenever you need one (e.g. pain relief)."
+ "description": "No fixed plan. Log an intake whenever you need one (e.g. pain relief)."
}
},
"summary": {
@@ -1684,7 +1731,7 @@
}
},
"errors": {
- "submitFailed": "Could not save the medication — please retry."
+ "submitFailed": "Could not save the medication. Please retry."
}
},
"doseStrength": {
@@ -1700,11 +1747,11 @@
"empty": "No dose-escalation steps recorded yet. Add dated dose changes to plot the plan.",
"youAreHere": "You are here",
"doseStep": "{dose} {unit}",
- "plannedFor": "Planned · {date}"
+ "plannedFor": "Planned for {date}"
},
"injectionSiteBodyOutlineAriaLabel": "Body outline",
"injectionSitePickerAriaLabel": "Injection site picker",
- "injectionSiteRecommendedAriaLabel": "{site} — recommended next site",
+ "injectionSiteRecommendedAriaLabel": "{site}: recommended next site",
"injectionSiteLegendAriaLabel": "Marker legend",
"injectionSiteLegendRecommended": "Recommended next site",
"injectionSiteLegendLastUsed": "Last used site",
@@ -1740,6 +1787,7 @@
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "via {origin}",
+ "intakeRecordedAutomatically": "Recorded automatically",
"intakeHistoryEmptyTitle": "No intakes recorded yet",
"intakeHistoryEmptyDescription": "Log a dose on the daily intake page to start building the history.",
"intakeHistoryEmptyAction": "Open daily intake",
@@ -1751,7 +1799,7 @@
"intakeHistoryStatusSkipped": "Skipped",
"intakeHistorySortByTaken": "Sort by taken time",
"intakeHistorySortByScheduled": "Sort by scheduled time",
- "intakeHistoryPageInfo": "Page {page} of {total} · {count} entries",
+ "intakeHistoryPageInfo": "Page {page} of {total}, {count} entries",
"intakeHistoryPrev": "Previous",
"intakeHistoryNext": "Next",
"glp1": {
@@ -1792,7 +1840,7 @@
"cadence": {
"oneShotOn": "One-time dose on {date}.",
"oneShotPending": "One-time dose pending.",
- "asNeeded": "As needed — no fixed plan."
+ "asNeeded": "As needed, no fixed plan."
},
"intake": {
"title": "Intake history",
@@ -1819,7 +1867,7 @@
"dialogTitle": "Edit intake",
"takenAtLabel": "Taken at",
"scheduledForHint": "Scheduled: {dateTime}",
- "farFromScheduledWarning": "This is far from the scheduled time — check the date.",
+ "farFromScheduledWarning": "This is far from the scheduled time. Check the date.",
"skippedLabel": "Skipped",
"save": "Save",
"cancel": "Cancel",
@@ -1851,7 +1899,7 @@
"markAsTaken": "Mark as taken",
"markAsSkipped": "Mark as skipped",
"adHocTag": "off-schedule",
- "takenDetail": "planned {planned} · taken {taken}",
+ "takenDetail": "planned {planned}, taken {taken}",
"doseTaken": "Dose: {dose}",
"status": {
"taken_on_time": "on time",
@@ -1863,10 +1911,10 @@
},
"addDialog": {
"title": "Add intake",
- "description": "Record a dose — including a backdated or off-schedule one.",
+ "description": "Record a dose, including a backdated or off-schedule one.",
"takenAtLabel": "Taken at",
"skippedLabel": "Skipped",
- "nudgeHint": "Close to the {slot} slot — on save we ask whether to attribute it.",
+ "nudgeHint": "Close to the {slot} slot. On save we ask whether to attribute it.",
"cancel": "Cancel",
"submit": "Save"
},
@@ -1915,7 +1963,7 @@
},
"codes": {
"label": "Clinical codes (ATC / RxNorm)",
- "note": "Optional. Used on the FHIR health-record export. Codes are taken exactly as entered — never guessed.",
+ "note": "Optional. Used on the FHIR health-record export. Codes are taken exactly as entered, never guessed.",
"atcLabel": "ATC",
"rxNormLabel": "RxNorm",
"saved": "Clinical codes saved",
@@ -1935,7 +1983,7 @@
"title": "End medication",
"helper": "Stop reminders and mark the medication as ended. History stays visible.",
"button": "End",
- "dialogTitle": "End {name} — stop reminders, history stays visible",
+ "dialogTitle": "End {name}? Reminders stop, history stays visible",
"dialogBody": "No more reminders. Old entries remain in the timeline.",
"toast": "Medication ended",
"failed": "Could not end the medication."
@@ -1973,7 +2021,7 @@
"copyFailed": "Could not copy.",
"tokenCopied": "Token copied",
"copyToken": "Copy token",
- "mintedHint": "Copy this token now — it is shown only once."
+ "mintedHint": "Copy this token now. It is shown only once."
},
"advanced": {
"dataPortability": {
@@ -2002,7 +2050,6 @@
"bestand": "Supply",
"verlauf": "History",
"injektion": "Injection",
- "api": "API",
"erweitert": "Advanced",
"wirkung": "Effect"
},
@@ -2014,9 +2061,9 @@
"statusTitle": "Status",
"nextDoseLabel": "Next dose",
"nextDoseNone": "No upcoming dose.",
- "nextDoseAsNeeded": "As needed — no planned dose.",
+ "nextDoseAsNeeded": "As needed, no planned dose.",
"nextDoseNotTracked": "Not tracked, kept as a record.",
- "pausedHint": "Paused — no reminders.",
+ "pausedHint": "Paused: no reminders.",
"reminderLabel": "Reminder",
"reminderOn": "On",
"reminderOff": "Off",
@@ -2087,7 +2134,7 @@
"packagingDefaultPack": "Default pack size: {units} units",
"packagingNoDefaultPack": "No default pack size set",
"packagingEdit": "Edit",
- "packagingDescription": "Set how many units one dose consumes and how large a new pack usually is — e.g. after a manufacturer switch with a different blister size.",
+ "packagingDescription": "Set how many units one dose consumes and how large a new pack usually is, e.g. after a manufacturer switch with a different blister size.",
"packagingUnitsPerDoseLabel": "Units per dose",
"packagingUnitsPerDoseHelper": "E.g. two 2 mg tablets for a 4 mg dose, or 1.5 for one and a half (above 0, up to 100). Doses already recorded stay unchanged.",
"packagingDefaultPackLabel": "Default pack size (units)",
@@ -2138,11 +2185,11 @@
"hideEras": "Hide earlier eras",
"addEra": "Add earlier era",
"addEraTitle": "Add earlier era",
- "addEraDescription": "Record the times this medication used to be taken at — for example before you adjusted it here.",
+ "addEraDescription": "Record the times this medication used to be taken at, for example before you adjusted it here.",
"editAria": "Edit era",
"editEraTitle": "Edit era",
"editEraDescription": "Adjust the dose times and the date range of this era.",
- "archivedEditHint": "This era was recorded automatically — your change corrects the recorded history; the original is kept internally as a record.",
+ "archivedEditHint": "This era was recorded automatically. Your change corrects the recorded history; the original is kept internally as a record.",
"updatedToast": "Era updated",
"updateError": "Could not update the era. It must not overlap other eras and must end before the current plan.",
"timesLabel": "Dose times",
@@ -2168,7 +2215,7 @@
"saved": "Schedule saved",
"failed": "Could not save the schedule",
"window": {
- "intro": "For each dose, set when an intake still counts as on time — a precise time (±1h) or your own window such as 07:00–09:00.",
+ "intro": "For each dose, set when an intake still counts as on time: a precise time (±1h) or your own window such as 07:00–09:00.",
"explicitToggle": "Custom window",
"startLabel": "Window start",
"endLabel": "Window end",
@@ -2181,10 +2228,10 @@
}
},
"logInjectionSiteTitle": "Log injection site",
- "logInjectionSiteDescription": "Where did you inject {name}? Optional — you can skip this.",
+ "logInjectionSiteDescription": "Where did you inject {name}? Optional: you can skip this.",
"logInjectionSiteSkip": "Skip",
"logInjectionSiteConfirm": "Save site",
- "logInjectionSiteNoneAvailable": "No injection site is available — every site is excluded in your settings.",
+ "logInjectionSiteNoneAvailable": "No injection site is available. Every site is excluded in your settings.",
"logInjectionSiteSaveFailed": "Could not save the injection site. Please try again.",
"perSlotUnits": "{units} per dose",
"trackInjectionSitesToggle": "Track injection sites",
@@ -2209,16 +2256,15 @@
"tableStockDoses": "{count} doses",
"tableStockRunway": "≈ {days} days left",
"cardLowStockRunway": "Supply: ≈ {days} days",
- "takeActionFor": "Take – {name}",
- "skipActionFor": "Skip – {name}",
- "reorderTitle": "Adjust order",
- "reorderDescription": "Set the order of your medications – it applies to both the card and the table view.",
+ "takeActionFor": "Take {name}",
+ "skipActionFor": "Skip {name}",
+ "reorderDescription": "Set the order of your medications for both the card and the table view.",
"reorderSaved": "Order saved",
"reorderSaveFailed": "Order could not be saved",
"customize": "Customize medications",
"efficacy": {
"title": "Effect",
- "intro": "How the measurements this medication is meant to influence have moved around the time you started it. It shows what lines up in time — an association, not a cause — and never advice about your treatment or dose.",
+ "intro": "How the measurements this medication is meant to influence have moved around the time you started it. It shows what lines up in time (an association, not a cause) and never advice about your treatment or dose.",
"notEligible": "There is no outcome to relate this medication to.",
"startFallback": "No start date is set, so the comparison uses your first recorded reading as the reference point.",
"noStart": "Set a start date for this medication to compare the readings before and after it began.",
@@ -2231,7 +2277,7 @@
"levelShift": "A shift in the level was detected around {date}.",
"plateau": {
"title": "Weight plateau on the current dose",
- "evidence": "Weight moved by {delta} {unit} over the last {window} days ({readings} readings) — {days} days on {dose}."
+ "evidence": "Weight moved by {delta} {unit} over the last {window} days ({readings} readings), {days} days on {dose}."
},
"retarget": {
"label": "Not the right measurement?",
@@ -2241,16 +2287,16 @@
"error": "Could not save that change. Try again."
},
"beforeAfter": {
- "summary": "Ø {before} before → Ø {after} since — a change of {delta}.",
+ "summary": "Ø {before} before → Ø {after} since, a change of {delta}.",
"counts": "Based on {beforeCount} readings before and {afterCount} since.",
- "needBefore": "Not enough readings before the start yet — about {weeks} weeks are needed before and after to compare.",
- "needAfter": "Not enough readings since the start yet — about {weeks} weeks are needed before and after to compare.",
+ "needBefore": "Not enough readings before the start yet. About {weeks} weeks are needed before and after to compare.",
+ "needAfter": "Not enough readings since the start yet. About {weeks} weeks are needed before and after to compare.",
"needStart": "Set a start date to compare the readings before and after it began.",
- "needData": "Not enough readings yet — about {weeks} weeks are needed before and after the start to compare."
+ "needData": "Not enough readings yet. About {weeks} weeks are needed before and after the start to compare."
},
"insights": {
"title": "Medication effect",
- "subtitle": "What lines up with the measurements your medications target — association only, never a verdict.",
+ "subtitle": "What lines up with the measurements your medications target: association only, never a verdict.",
"link": "Open the effect view",
"empty": "Not enough data yet to relate your medications to their target measurements."
},
@@ -2284,7 +2330,7 @@
"reasonMissingMedication": "the row names no medication",
"reasonUnreadableDosage": "the dosage is not a number",
"reasonUnreadableRow": "the row has fewer columns than the header",
- "reasonStatusNoDoseInformation": "the file records nothing about that dose — nobody answered and nothing was logged",
+ "reasonStatusNoDoseInformation": "the file records nothing about that dose; nobody answered and nothing was logged",
"reasonStatusReminderEvent": "the row is about a snoozed reminder, which says nothing about the dose",
"reasonStatusNotificationNotSent": "the row records that no notification was sent, which is about the app rather than the dose"
},
@@ -2356,7 +2402,7 @@
},
"charts": {
"loadingLabel": "Loading chart…",
- "loadingSlowHint": "Computing analytics — this can take a moment.",
+ "loadingSlowHint": "Computing analytics. This can take a moment.",
"weight": "Weight",
"bloodPressure": "Blood Pressure",
"pulse": "Pulse",
@@ -2378,6 +2424,7 @@
"days7Label": "7d",
"days7Title": "Last 7 days",
"days30Label": "30d",
+ "daysNLabel": "{days}d",
"days30Title": "Last 30 days",
"days90Label": "90d",
"days90Title": "Last 90 days",
@@ -2437,13 +2484,15 @@
"loadErrorTitle": "Chart could not be loaded",
"loadErrorReload": "Reload",
"sparseDataCaption": "More days will fill out the trend.",
+ "tooltipHourlyMean": "{metric} (average of the hours)",
"dataTable": {
"toggle": "Data points ({count})",
"columnDate": "Date",
"noValue": "No value",
"captionDay": "One row per day of {metric}, the same points the chart draws.",
- "captionWeek": "One row per week of {metric}, the average of that week's days — the same points the chart draws.",
- "captionMonth": "One row per month of {metric}, the average of that month's days — the same points the chart draws."
+ "captionWeek": "One row per week of {metric}, the average of that week's days, the same points the chart draws.",
+ "captionMonth": "One row per month of {metric}, the average of that month's days, the same points the chart draws.",
+ "hourlyMeanNote": "{metric} is each day's average, with every hour counting once, so a workout does not outweigh the rest of the day."
},
"personalBaseline": "Your normal",
"deltaVsBaseline": "{delta} vs. your normal",
@@ -2471,7 +2520,8 @@
"moodHeatmapEmpty": "Mood heatmap, no days logged yet.",
"complianceHeatmap": "Medication compliance heatmap from {start} to {end}, {days} active days.",
"complianceHeatmapEmpty": "Medication compliance heatmap, no active days yet."
- }
+ },
+ "medicationsEmptyAction": "Add a medication"
},
"chart": {
"overlay": {
@@ -2502,8 +2552,8 @@
},
"tooltipPrior": "Prior period",
"unavailable": {
- "lastMonth": "Comparison unavailable — no data from last month yet",
- "lastYear": "Comparison unavailable — no data from last year yet"
+ "lastMonth": "Comparison unavailable: no data from last month yet",
+ "lastYear": "Comparison unavailable: no data from last year yet"
},
"notAvailableForCompliance": "Comparison N/A for adherence"
},
@@ -2515,7 +2565,9 @@
"dismissError": "Could not dismiss this pattern.",
"restoreError": "Could not restore this pattern.",
"loadError": "Patterns could not be loaded.",
- "empty": "No current pattern cleared the evidence threshold."
+ "empty": "No current pattern cleared the evidence threshold.",
+ "seasonalNote": "Since version 1.42, HealthLog removes season and long-term trend before it compares values. You see fewer relationships, but the ones that remain hold up better. Many of the earlier patterns were just the calendar.",
+ "seasonalNoteDismiss": "Dismiss note"
},
"correlation": {
"direction": {
@@ -2547,29 +2599,37 @@
"ENV_DAYLIGHT": "daylight",
"ENV_PRECIP": "precipitation",
"ENV_PRESSURE_MEAN": "barometric pressure",
- "ENV_PRESSURE_DELTA": "intraday pressure swing"
+ "ENV_PRESSURE_DELTA": "intraday pressure swing",
+ "ENV_PM25": "fine particles (PM2.5)",
+ "ENV_OZONE_8H": "ozone",
+ "ENV_POLLEN_MAX": "pollen"
},
"daily": {
- "high": "Higher {behaviour} tends to go with {direction} next-day {outcome} in your data — a pattern worth watching, not a cause.",
- "moderate": "Higher {behaviour} looks like it goes with {direction} next-day {outcome} in your data, on the evidence so far — a pattern worth watching, not a cause.",
- "faint": "Higher {behaviour} shows a faint hint, if anything, of {direction} next-day {outcome} in your data — too small to lean on, never a cause."
+ "high": "Higher {behaviour} tends to go with {direction} next-day {outcome} in your data. It is a pattern to watch, not a cause.",
+ "moderate": "Higher {behaviour} looks like it goes with {direction} next-day {outcome} in your data, on the evidence so far. It is a pattern to watch, not a cause.",
+ "faint": "Higher {behaviour} shows at most a faint hint of {direction} next-day {outcome} in your data. It is too small to lean on and never a cause."
},
"lab": {
- "default": "Higher {lab} readings line up with {direction} {outcome} over the same periods in your data — an association worth watching with your clinician, never a cause.",
- "faint": "Higher {lab} readings show a faint hint, if anything, of {direction} {outcome} over the same periods in your data — too small to lean on, an association to raise with your clinician, never a cause."
+ "default": "Higher {lab} readings line up with {direction} {outcome} over the same periods in your data, an association worth watching with your clinician, never a cause.",
+ "faint": "Higher {lab} readings show at most a faint hint of {direction} {outcome} over the same periods in your data. It is too small to lean on and never a cause, but worth raising with your clinician."
},
"hypothesis": {
"bpCompliance": {
- "support": "Higher medication compliance lines up with lower systolic readings — a pattern worth watching.",
- "surprising": "Compliance and systolic move together in your data — surprising; talk to your doctor before adjusting anything.",
+ "support": "Higher medication compliance lines up with lower systolic readings, a pattern worth watching.",
+ "surprising": "Compliance and systolic move together in your data, which is surprising. Talk to your doctor before adjusting anything.",
"none": "Compliance and systolic do not move together strongly in this window."
},
"moodPulse": {
- "support": "Lower-mood days line up with higher resting pulse — a pattern worth watching.",
- "surprising": "Higher-mood days line up with higher resting pulse in your data — surprising.",
+ "support": "Lower-mood days line up with higher resting pulse, a pattern worth watching.",
+ "surprising": "Higher-mood days line up with higher resting pulse in your data, which is surprising.",
"none": "Mood and resting pulse do not move together strongly in this window."
},
- "weightWeekday": "{weekday} weights run {delta} kg {direction} your other-day average — a pattern worth watching."
+ "weightWeekday": "{weekday} weights run {delta} kg {direction} your other-day average, a pattern worth watching."
+ },
+ "dailySameDay": {
+ "high": "Higher {behaviour} on the day and the day before tends to go with {direction} {outcome} in your data. It is a pattern to watch, not a cause.",
+ "moderate": "Higher {behaviour} on the day and the day before looks like it goes with {direction} {outcome} in your data, on the evidence so far. It is a pattern to watch, not a cause.",
+ "faint": "Higher {behaviour} on the day and the day before shows a faint hint, if anything, of {direction} {outcome} in your data. It is too small to lean on and never a cause."
}
},
"noAnalysisYet": "No assessment yet.",
@@ -2681,7 +2741,7 @@
"referenceSleep": "At least {low} minutes per night; longer sleep is not penalised",
"referenceAdiposity": "Waist-to-height ratio below {high}",
"referenceHba1c": "HbA1c {low} to {high}%",
- "referenceDetail": "{label} · {source} · {kind} · {low} to {high}",
+ "referenceDetail": "{label}: {low} to {high} ({kind}, {source})",
"openBound": "open",
"laboratoryReferenceSource": "Reporting laboratory reference ranges",
"personalTargetSource": "Personal target",
@@ -2716,7 +2776,8 @@
"coverage": {
"recommendedMet": "Covers the three areas the score recommends",
"areas": "{present} of {required} areas of health"
- }
+ },
+ "historyLink": "History"
},
"dailyBriefing": {
"title": "Daily Briefing",
@@ -2726,7 +2787,7 @@
"emptyDescription": "Generate today's briefing to get a narrative read of your trends.",
"emptyAction": "Generate briefing",
"loadingLabel": "Synthesising today's briefing",
- "refreshFailedHint": "Couldn't refresh the briefing — showing the last one.",
+ "refreshFailedHint": "Couldn't refresh the briefing. Showing the last one.",
"retryAction": "Retry",
"failedTitle": "Couldn't generate the briefing",
"failedDescription": "The AI provider timed out or returned an error. Try again, or raise the response timeout in AI settings for a slow local model.",
@@ -2775,7 +2836,7 @@
"experimentCta": "Try a 7-day experiment",
"experimentTooltip": "Coming soon",
"emptyTitle": "Need more data to see this relationship",
- "sourceChip": "based on {n} paired readings · {window}",
+ "sourceChip": "based on {n} paired readings ({window})",
"sourceWindowLast30": "last 30 days",
"confidenceHigh": "High confidence",
"confidenceModerate": "Moderate confidence",
@@ -2790,13 +2851,14 @@
}
},
"coach": {
- "refusalOutOfScope": "I can only help with the health metrics in your log — measurements, medications, mood, trends. I cannot answer questions outside that scope.",
- "refusalInjection": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data — please rephrase your question in those terms.",
- "errorBudget": "You've reached today's Coach usage limit. The budget refreshes at midnight UTC.",
+ "refusalOutOfScope": "I can only help with the health metrics in your log: measurements, medications, mood, trends. I cannot answer questions outside that scope.",
+ "refusalInjection": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data. Please rephrase your question in those terms.",
+ "errorBudget": "You've reached today's Coach usage limit. The budget refreshes at midnight in your time zone.",
"errorProvider": "The Coach could not reach an Insights provider just now. Please try again in a moment.",
- "errorNoProvider": "The Coach reads your logged health data and answers questions about it (not medical advice). HealthLog needs an AI provider for that — connect your own key or a local model.",
+ "errorDemo": "The Coach doesn't answer in the demo.",
+ "errorNoProvider": "The Coach reads your logged health data and answers questions about it (not medical advice). HealthLog needs an AI provider for that. Connect your own key or a local model.",
"errorNoProviderAction": "Connect an AI provider",
- "errorNetwork": "No internet connection — try again once you're back online.",
+ "errorNetwork": "No internet connection. Try again once you're back online.",
"prefsSaveError": "Couldn't save your Coach preferences. Please try again.",
"errorCredentialExpired": "Your AI provider connection has expired. Reconnect it in Settings to keep the Coach running.",
"tagline": "Personal health coach, grounded in your data.",
@@ -2815,7 +2877,7 @@
"thinking": "Thinking…",
"unverifiedFiguresNotice": "Some figures couldn't be checked against your data and were left out.",
"composerPlaceholder": "Ask me anything…",
- "nudgeBubbleLabel": "New message from the Coach — open chat",
+ "nudgeBubbleLabel": "New message from the Coach. Open chat",
"fabLabel": "Open the Coach",
"readAloud": "Read aloud",
"readAloudStop": "Stop reading",
@@ -2833,14 +2895,13 @@
},
"rememberMessage": {
"action": "Remember",
- "done": "Saved to your self-context",
- "duplicate": "Already part of your self-context",
- "failed": "Could not save. Please try again."
+ "failed": "Could not save. Please try again.",
+ "alreadyKept": "The Coach already remembers this."
},
"guided": {
"offerTitle": "The Coach has {count} follow-up questions for you",
"offerTitleOne": "The Coach has a follow-up question for you",
- "offerBody": "Short answers sharpen your self-context — one question at a time, right here in the chat.",
+ "offerBody": "Short answers sharpen your self-context, one question at a time, right here in the chat.",
"offerStart": "Answer now",
"offerLater": "Later",
"offerDismiss": "Don't ask again",
@@ -2850,7 +2911,7 @@
"later": "Later",
"dismissRemaining": "Don't ask again",
"answerPlaceholder": "Type your answer…",
- "summaryTitle": "All set — thanks for your answers!",
+ "summaryTitle": "All set, thanks for your answers!",
"summaryAnswered": "{answered} of {total} answered",
"summaryAdopted": "{adopted} added to your self-context",
"summaryAdoptedNone": "nothing new added to your self-context",
@@ -2862,6 +2923,7 @@
"historyEmpty": "Your conversations will appear here. Ask anything to start.",
"historyDeleteAria": "Delete conversation",
"historyDeleted": "Conversation deleted",
+ "historyDeleteFailed": "The conversation could not be deleted.",
"rename": {
"action": "Rename conversation",
"input": "Conversation title",
@@ -2875,7 +2937,6 @@
"sourcesFresh": "Fresh",
"sourcesStale": "Stale",
"sourcesFooter": "The Coach reads only the data you've connected.",
- "evidenceLabel": "What I'm looking at",
"metric": {
"bp": "Blood pressure",
"weight": "Weight",
@@ -2921,14 +2982,6 @@
"skin_temp": "Skin temperature",
"workouts": "Workouts"
},
- "window": {
- "last7days": "last 7 days",
- "last30days": "last 30 days",
- "last90days": "last 90 days",
- "lastYear": "last 12 months",
- "allTime": "all time (12 months of detail)"
- },
- "windowLabel": "Window",
"settingsAriaLabel": "Coach settings",
"settingsTitle": "Coach settings",
"settingsDescription": "Tune how the Coach talks with you.",
@@ -2940,9 +2993,10 @@
"settingsVerbosityBrief": "Brief",
"settingsVerbosityDefault": "Default",
"settingsVerbosityDetailed": "Detailed",
- "settingsVerbosityConciseHint": "The concise tone already keeps replies short — verbosity is pinned to Brief while it is active.",
- "settingsSourcesPointer": "Which data clusters the coach may draw on and the default analysis window are managed directly in the coach chat (\"What I draw on\" rail).",
- "settingsSourcesPointerLink": "Open the coach",
+ "settingsVerbosityConciseHint": "The concise tone already keeps replies short, so verbosity is pinned to Brief while it is active.",
+ "settingsSourcesSummary": "The Coach sees {count} of {total} data areas and looks back {window}.",
+ "settingsSourcesSummaryAll": "The Coach sees {count} of {total} data areas and your whole history.",
+ "settingsSourcesPointerLink": "Open Coach settings",
"settingsExcludeLabel": "Exclude metrics",
"settingsExcludeHint": "The Coach will not see data for selected metrics.",
"settingsContextLabel": "Include context",
@@ -2954,34 +3008,30 @@
"settingsSave": "Save",
"settingsSaved": "Saved",
"settingsCancel": "Cancel",
- "feedbackHelpful": "Helpful",
- "feedbackUnhelpful": "Not quite",
- "feedbackThanks": "Thanks for the signal.",
- "feedbackError": "Could not save feedback",
"dailyLimitTitle": "Daily limit reached",
- "dailyLimitBody": "Daily limit reached. The budget refreshes at midnight UTC.",
+ "dailyLimitBody": "Daily limit reached. The budget refreshes at midnight in your time zone.",
"providerRateLimitTitle": "Provider rate-limited",
"providerRateLimitBody": "Provider temporarily rate-limited; retry in ~5 min.",
"cluster": {
"cardio": {
"label": "Cardiovascular",
- "hint": "BP · pulse · HRV · resting & walking HR"
+ "hint": "BP, pulse, HRV, resting and walking HR"
},
"body": {
"label": "Body composition",
- "hint": "Weight · body fat · muscle · BMI"
+ "hint": "Weight, body fat, muscle and BMI"
},
"activity": {
"label": "Activity",
- "hint": "Steps · energy · distance · VO₂ max"
+ "hint": "Steps, energy, distance and VO₂ max"
},
"workouts": {
"label": "Workouts",
- "hint": "Runs · rides · sessions"
+ "hint": "Runs, rides and sessions"
},
"sleep": {
"label": "Sleep",
- "hint": "Duration · stages"
+ "hint": "Duration and stages"
},
"mood": {
"label": "Mood",
@@ -2989,60 +3039,72 @@
},
"glucose": {
"label": "Glucose",
- "hint": "Fasting · postprandial readings"
+ "hint": "Fasting and postprandial readings"
},
"medication": {
"label": "Medication",
- "hint": "Adherence · GLP-1 context"
+ "hint": "Adherence and GLP-1 context"
},
"mobility": {
"label": "Mobility & gait",
- "hint": "Steadiness · asymmetry · gait"
+ "hint": "Steadiness, asymmetry and gait"
},
"environment": {
"label": "Environment",
- "hint": "Noise · daylight · skin temp"
+ "hint": "Noise, daylight and skin temperature"
}
},
"settingsClustersLabel": "Data the Coach sees",
"settingsClustersHint": "Pick which clusters of your data the Coach can read. More data means richer answers and a larger prompt.",
"settingsClusterNoData": "No data yet",
- "sourcesMemberCount": "{count} metrics",
+ "sourcesMemberCountOne": "1 metric",
+ "sourcesMemberCountFew": "{count} metrics",
+ "sourcesMemberCountOther": "{count} metrics",
+ "lookback": {
+ "label": "How far back the Coach can look",
+ "option": {
+ "last7days": "{days} days",
+ "last30days": "{days} days",
+ "last90days": "{days} days",
+ "lastYear": "{months} months",
+ "allTime": "All"
+ },
+ "detailDays": "The Coach reads nothing older than {days} days, in any answer.",
+ "detailMonths": "The Coach reads nothing older than {months} months, in any answer.",
+ "detailAll": "Everything you recorded: {dailyMonths} months in daily detail, older readings as monthly values up to {years} years back, lab results from the last {labMonths} months.",
+ "note": "Your profile, current medications, an ongoing illness and upcoming visits stay visible; summaries that need more history are shortened or left out. Answers the Coach gave earlier in older conversations may still quote older values."
+ },
"showConversations": "Show conversations",
"hideConversations": "Hide conversations",
"heroGreeting": "Ask me anything about your data",
"heroSubline": "Ask about your trends, medications, or readings.",
- "thinkingElapsed": "{seconds}s",
"thinkingDuration": "Thought for {seconds}s",
"thinkingExpandAria": "Show what the Coach considered",
"thinkingDetail": "Answered from your health snapshot.",
"tokensUsed": "{count} tokens",
- "tokensUsedWithModel": "{count} tokens · {model}",
"askAboutThis": "Ask the coach about this",
"readStrip": {
"label": "Usual range",
"baselineWithin": "Your usual range is {low}–{high} {unit}; today's {value} sits within it.",
"baselineAbove": "Your usual range is {low}–{high} {unit}; today's {value} sits above it.",
"baselineBelow": "Your usual range is {low}–{high} {unit}; today's {value} sits below it.",
- "insufficient": "Still learning your usual range — keep logging for a few more days.",
+ "sameHoursWithin": "By this time of day you usually sit at {low}–{high} {unit}; today so far, {value} is within that.",
+ "sameHoursAbove": "By this time of day you usually sit at {low}–{high} {unit}; today so far, {value} is above that.",
+ "sameHoursBelow": "By this time of day you usually sit at {low}–{high} {unit}; today so far, {value} is below that.",
+ "steadyWithin": "Your readings usually sit at {low} {unit}; today's {value} matches.",
+ "steadyAbove": "Your readings usually sit at {low} {unit}; today's {value} is above that.",
+ "steadyBelow": "Your readings usually sit at {low} {unit}; today's {value} is below that.",
+ "latestWithin": "Your usual range is {low}–{high} {unit}; your latest reading, {date}, was {value}, within it.",
+ "latestAbove": "Your usual range is {low}–{high} {unit}; your latest reading, {date}, was {value}, above it.",
+ "latestBelow": "Your usual range is {low}–{high} {unit}; your latest reading, {date}, was {value}, below it.",
+ "steadyLatestWithin": "Your readings usually sit at {low} {unit}; your latest reading, {date}, was {value} and matches.",
+ "steadyLatestAbove": "Your readings usually sit at {low} {unit}; your latest reading, {date}, was {value}, above that.",
+ "steadyLatestBelow": "Your readings usually sit at {low} {unit}; your latest reading, {date}, was {value}, below that.",
+ "insufficient": "Still learning your usual range. Keep logging for a few more days.",
"driver": "{note}"
},
"scope": {
- "prefix": "The Coach is already on {metric}",
- "tap": "Tap to ask",
- "question": "Walk me through this metric — anything I should watch?"
- },
- "seeded": {
- "prefix": "Worth a look today: {signal}",
- "signal": {
- "readiness": "readiness",
- "recovery": "recovery"
- },
- "question": {
- "readiness": "My readiness is off today — what's driving it?",
- "recovery": "My recovery shifted — what's behind it?"
- },
- "dismiss": "Dismiss"
+ "prefix": "The Coach is already on {metric}"
},
"settings": "Settings",
"history": {
@@ -3052,39 +3114,39 @@
"groupEarlier": "Earlier"
},
"seed": {
- "bloodPressure": "Walk me through my blood pressure trend over the last 30 days — anything I should keep an eye on?",
+ "bloodPressure": "Walk me through my blood pressure trend over the last 30 days. Anything I should keep an eye on?",
"weight": "How has my weight been trending lately, and is the direction something to act on?",
"bmi": "What does my BMI trend tell me, and how should I read it?",
- "pulse": "Walk me through my pulse readings — is anything out of the ordinary?",
+ "pulse": "Walk me through my pulse readings. Is anything out of the ordinary?",
"restingHr": "How has my resting heart rate been trending, and what does it say about my fitness?",
"hrv": "What is my heart-rate variability telling me about recovery and stress lately?",
- "sleep": "Walk me through my recent sleep — duration, consistency, and anything worth changing.",
+ "sleep": "Walk me through my recent sleep: duration, consistency, and anything worth changing.",
"mood": "What patterns do you see in my mood over the last few weeks?",
"medications": "How is my medication adherence looking, and is it lined up with how I've been feeling?",
"steps": "How active have I been lately based on my steps, and how does it compare to a healthy baseline?",
"activeEnergy": "What does my active-energy trend say about my activity level lately?",
"cardioFitness": "What does my cardio fitness (VO₂ max) trend mean, and how do I move it in the right direction?",
- "bloodGlucose": "Walk me through my recent glucose readings — are they in a healthy range?",
+ "bloodGlucose": "Walk me through my recent glucose readings. Are they in a healthy range?",
"oxygenSaturation": "What does my blood-oxygen (SpO₂) trend tell me?",
"respiratoryRate": "What does my respiratory-rate trend say about my health?",
- "workouts": "Walk me through my recent workouts — load, frequency, and how I'm recovering between them.",
+ "workouts": "Walk me through my recent workouts: load, frequency, and how I'm recovering between them.",
"recoveryPage": "Why is my recovery where it is right now, and what's driving it?",
- "briefing": "Walk me through today's briefing — what should I focus on, and is anything worth acting on?",
+ "briefing": "Walk me through today's briefing. What should I focus on, and is anything worth acting on?",
"healthScore": "Why is my health score {score} out of 100, and what would move it the most?",
- "periodWeek": "Walk me through how my last week went across my health metrics — what stands out?",
- "periodMonth": "Walk me through how my last month went across my health metrics — what stands out?",
+ "periodWeek": "Walk me through how my last week went across my health metrics. What stands out?",
+ "periodMonth": "Walk me through how my last month went across my health metrics. What stands out?",
"correlationBpCompliance": "How does my medication adherence relate to my blood pressure? Walk me through what this correlation means for me.",
"correlationMoodPulse": "Is there a real link between my mood and my pulse? Help me read this correlation.",
"correlationWeightWeekday": "Does my weight follow a weekly pattern? What does this day-of-week correlation tell me?",
- "recommendation": "Tell me more about this recommendation: \"{text}\" — why does it apply to me, and what should I do about it?"
+ "recommendation": "Tell me more about this recommendation: \"{text}\" Why does it apply to me, and what should I do about it?"
},
- "assessmentPrompt": "Walk me through my {metric} assessment — what does it mean and what should I do about it?",
+ "assessmentPrompt": "Walk me through my {metric} assessment. What does it mean and what should I do about it?",
"attach": {
"menuChooseFromDocuments": "Choose from documents",
"menuUploadNew": "Upload new document",
"trigger": "Attach documents",
"pickerTitle": "Attach documents",
- "pickerDescription": "The coach reads only the documents you attach — not your health data.",
+ "pickerDescription": "The coach reads only the documents you attach, not your health data.",
"pickerSearchPlaceholder": "Search documents…",
"pickerEmpty": "No documents in your vault yet.",
"pickerNotIndexed": "Not yet indexed",
@@ -3092,13 +3154,13 @@
"pickerConfirm": "Attach ({count})",
"pillRemove": "Remove {title}",
"indexing": "Indexing…",
- "indexingHint": "A document is still being indexed — you can send once it's ready.",
- "fencedHint": "This conversation runs without access to your health data — the coach only reads the attached documents.",
+ "indexingHint": "A document is still being indexed. You can send once it's ready.",
+ "fencedHint": "This conversation runs without access to your health data. The coach only reads the attached documents.",
"documentsAttachedOne": "1 document attached",
"documentsAttachedOther": "{count} documents attached",
"railBadge": "Fenced document chat",
"uploadError": "Upload failed. Please try again.",
- "notFoundError": "That document link is no longer valid — starting a normal chat instead."
+ "notFoundError": "That document link is no longer valid. Starting a normal chat instead."
},
"interrupted": {
"note": "This answer was interrupted before it finished.",
@@ -3111,7 +3173,102 @@
"errorNotPermitted": "The Coach is not available inside a record someone shared with you.",
"errorUnavailable": "The Coach could not check its settings just now. Please try again in a moment.",
"errorOperatorOff": "AI is switched off on this server, so the Coach cannot answer.",
- "errorCoachOff": "The Coach is switched off. You can turn it back on in Settings, Modules."
+ "errorCoachOff": "The Coach is switched off. You can turn it back on in Settings, Modules.",
+ "frame": {
+ "showPanel": "Show conversations",
+ "hidePanel": "Hide conversations",
+ "panelDescription": "Your conversations with the Coach, newest first.",
+ "rowActions": "Actions for {title}",
+ "rename": "Rename",
+ "delete": "Delete",
+ "modelSection": "Model",
+ "provider": "Answers with",
+ "modelManaged": "The model for this provider is set in AI settings.",
+ "noProvider": "No provider available",
+ "saving": "Saving…",
+ "saveFailed": "That change could not be saved.",
+ "allSettings": "All AI settings",
+ "providerNotActive": "{provider} could not take over: it has no working sign-in or key. Set it up in AI settings.",
+ "operatorCodex": "ChatGPT (shared by the operator)",
+ "operatorManaged": "Your server's operator manages this connection. Once one of your own providers is set up, it answers first."
+ },
+ "answer": {
+ "scrollToBottom": "Jump to the latest message",
+ "rememberDone": "Remembered",
+ "timeYesterday": "yesterday {time}",
+ "timeEarlier": "{date}, {time}",
+ "info": "Answer details",
+ "infoModel": "Model: {model}"
+ },
+ "activity": {
+ "thinking": "Thinking…",
+ "fetching": "Fetching {domain}, {window}…",
+ "digestOne": "Summarising {count} reading…",
+ "digestFew": "Summarising {count} readings…",
+ "digestOther": "Summarising {count} readings…",
+ "digestDoneOne": "{count} reading from {areas}",
+ "digestDoneFew": "{count} readings from {areas}",
+ "digestDoneOther": "{count} readings from {areas}",
+ "areasOne": "{count} area",
+ "areasFew": "{count} areas",
+ "areasOther": "{count} areas",
+ "memoryOne": "Recalls {count} thing you told it",
+ "memoryFew": "Recalls {count} things you told it",
+ "memoryOther": "Recalls {count} things you told it",
+ "remember": "Remembering something…",
+ "plan": "Drafting a plan…",
+ "asking": "Asking you…",
+ "answer": "Writing the answer…",
+ "stop": {
+ "budget": "Budget reached, answering with what it has",
+ "time": "Time limit reached, answering with what it has",
+ "cap": "Many steps, wrapping up",
+ "noProgress": "No new data, wrapping up"
+ },
+ "thoughtProcessOne": "Thought process ({count} step)",
+ "thoughtProcessFew": "Thought process ({count} steps)",
+ "thoughtProcessOther": "Thought process ({count} steps)",
+ "awaitingAnswer": "Answer needed"
+ },
+ "memory": {
+ "saved": "Remembered: {fact}",
+ "undo": "Undo",
+ "accept": "Yes, remember it",
+ "decline": "No",
+ "confirmed": "Got it, I'll remember.",
+ "declined": "Okay, I won't."
+ },
+ "plan": {
+ "accept": "Take on this plan",
+ "decline": "Not now",
+ "confirmed": "Noted. I'll check in with you in {days} days.",
+ "declined": "Okay, not now."
+ },
+ "assumption": {
+ "line": "Assumed: {value}",
+ "change": "Change"
+ },
+ "suggestedReplies": {
+ "groupLabel": "Suggested replies"
+ },
+ "chart": {
+ "compare": {
+ "periods": "{a} vs {b}"
+ }
+ },
+ "reasoning": {
+ "label": "Thinking depth",
+ "hint": "Deeper thinking means more precise answers and a little more waiting.",
+ "off": "Off",
+ "minimal": "Minimal",
+ "low": "Low",
+ "medium": "Medium",
+ "high": "High",
+ "capped": "(limited by the admin)",
+ "disabled": "The admin has turned deeper thinking off."
+ },
+ "memoryLink": "Memory ({count})",
+ "newConversation": "New conversation"
},
"relativeJustNow": "just now",
"relativeMinutesAgoOne": "{count} minute ago",
@@ -3125,7 +3282,7 @@
"relativeDaysAgoFew": "{count} days ago",
"navOverview": "Overview",
"navCatalog": "All metrics",
- "navBloodPressure": "Blood Pressure",
+ "navBloodPressure": "Blood pressure",
"navWeight": "Weight",
"navPulse": "Pulse",
"navMood": "Mood",
@@ -3174,7 +3331,7 @@
},
"hrv": {
"title": "HRV",
- "description": "Heart-rate variability over time — your nervous-system recovery signal, captured during sleep by Apple Watch and other compatible wearables.",
+ "description": "Heart-rate variability over time: your nervous-system recovery signal, captured during sleep by Apple Watch and other compatible wearables.",
"chartTitle": "Heart-rate variability",
"emptyState": {
"title": "No HRV samples yet",
@@ -3183,17 +3340,17 @@
},
"restingHr": {
"title": "Resting heart rate",
- "description": "The daily floor your heart settles at — distinct from the spot pulse samples on the pulse page. Apple Health surfaces one value per day.",
+ "description": "The daily floor your heart settles at, distinct from the spot pulse samples on the pulse page. Apple Health surfaces one value per day.",
"chartTitle": "Resting heart rate",
"emptyState": {
"title": "No resting heart rate yet",
"description": "Resting heart rate arrives once Apple Watch or a compatible Withings device has worn through one quiet day."
},
- "coachPrefill": "I haven't logged any resting heart rate yet — why is the daily resting value different from a spot pulse, and how do I improve it?"
+ "coachPrefill": "I haven't logged any resting heart rate yet. Why is the daily resting value different from a spot pulse, and how do I improve it?"
},
"oxygenSaturation": {
"title": "Oxygen saturation",
- "description": "Blood oxygen (SpO₂) — clinical normal is 95–100 %, with target zones shaded on the chart.",
+ "description": "Blood oxygen (SpO₂): clinical normal is 95–100 %, with target zones shaded on the chart.",
"chartTitle": "Oxygen saturation",
"emptyState": {
"title": "No SpO₂ samples yet",
@@ -3202,7 +3359,7 @@
},
"bodyTemperature": {
"title": "Body temperature",
- "description": "Core body temperature trend — Celsius. Skin-temperature readings from Withings live on a separate metric.",
+ "description": "Core body temperature trend in Celsius. Skin-temperature readings from Withings live on a separate metric.",
"chartTitle": "Body temperature",
"emptyState": {
"title": "No body temperature yet",
@@ -3212,7 +3369,7 @@
},
"steps": {
"title": "Steps",
- "description": "Daily step count — one row per day, sourced from Apple Health or a connected activity device.",
+ "description": "Daily step count, one row per day, sourced from Apple Health or a connected activity device.",
"chartTitle": "Steps per day",
"emptyState": {
"title": "No step data yet",
@@ -3221,7 +3378,7 @@
},
"activeEnergy": {
"title": "Active energy",
- "description": "Daily active-energy total in kilocalories — one row per day, sourced from Apple Health or Withings activity.",
+ "description": "Daily active-energy total in kilocalories, one row per day, sourced from Apple Health or Withings activity.",
"chartTitle": "Active energy",
"emptyState": {
"title": "No active-energy data yet",
@@ -3241,27 +3398,27 @@
},
"explainer": {
"bloodPressureBody": "Blood pressure is the force your blood exerts on the artery walls, written as systolic over diastolic (the peak during a heartbeat over the resting value between beats). Consistently high readings strain the heart and blood vessels over time.",
- "pulseBody": "Your pulse is how many times your heart beats per minute. A spot reading reflects what you were doing in the moment — at rest it is typically 60–100 beats per minute, and it rises naturally with movement, stress, or caffeine.",
+ "pulseBody": "Your pulse is how many times your heart beats per minute. A spot reading reflects what you were doing in the moment. At rest it is typically 60–100 beats per minute, and it rises naturally with movement, stress, or caffeine.",
"weightBody": "Body weight is one of the simplest signals of long-term energy balance and overall health. The trend over weeks matters far more than any single reading, which fluctuates with hydration, food, and time of day.",
"bmiBody": "BMI is your weight divided by your height squared, giving a single number that places you in a broad weight category. It is a population-level screen, not a diagnosis, and does not distinguish muscle from fat.",
"sleepBody": "Sleep duration is how long you actually slept across the night, split into light, deep, and REM stages. Most adults need roughly seven to nine hours; both consistent quantity and quality support recovery, mood, and metabolism.",
- "moodBody": "Mood tracking records how you feel over time on a simple scale. Logging it regularly reveals patterns — how sleep, activity, or medication line up with how you feel — that a single day never shows.",
+ "moodBody": "Mood tracking records how you feel over time on a simple scale. Logging it regularly reveals patterns (how sleep, activity, or medication line up with how you feel) that a single day never shows.",
"medicationsBody": "Adherence is how reliably you take your medication as prescribed, shown here as the share of expected doses you actually logged. Steady intake is often what makes a treatment work as intended.",
- "restingHrBody": "Resting heart rate is your heart rate when you are completely at rest, usually measured overnight — distinct from your pulse during activity. A lower resting rate generally points to better cardiovascular fitness, and a rising trend can be an early sign of stress or illness. As general guidance, a normal adult resting rate is 60–100 beats per minute (AHA 2024); a trained heart can sit benignly at 40–60.",
+ "restingHrBody": "Resting heart rate is your heart rate when you are completely at rest, usually measured overnight, distinct from your pulse during activity. A lower resting rate generally points to better cardiovascular fitness, and a rising trend can be an early sign of stress or illness. As general guidance, a normal adult resting rate is 60–100 beats per minute (AHA 2024); a trained heart can sit benignly at 40–60.",
"hrvBody": "HRV is the tiny variation in time between consecutive heartbeats. Higher variability usually reflects a well-recovered, adaptable nervous system, while a sustained drop can signal fatigue, stress, or insufficient recovery.",
"oxygenSaturationBody": "SpO₂ is the percentage of your red blood cells carrying oxygen. Readings of 95–100% are normal at rest; values that stay below 90% warrant attention, though brief nightly dips are common.",
- "bodyTemperatureBody": "Body temperature reflects your core thermal state. As general guidance, the population mean is about 36.6 °C — not 37.0 °C — with a normal oral range of roughly 35.7–37.4 °C and a fever from 38.0 °C (J Gen Intern Med 2019). It swings about 0.5 °C across the day, and sites differ by up to ~1 °C, so tag where you measured. Overnight wrist readings track relative change, not a clinical fever value.",
+ "bodyTemperatureBody": "Body temperature reflects your core thermal state. As general guidance, the population mean is about 36.6 °C (not 37.0 °C), with a normal oral range of roughly 35.7–37.4 °C and a fever from 38.0 °C (J Gen Intern Med 2019). It swings about 0.5 °C across the day, and sites differ by up to ~1 °C, so tag where you measured. Overnight wrist readings track relative change, not a clinical fever value.",
"activeEnergyBody": "Active energy is the calories you burn through movement, on top of the baseline your body uses at rest. It is a direct measure of how physically active your day was.",
"workoutsBody": "Workouts are the structured exercise sessions your devices record, with type, duration, and energy. Tracking them shows how regular training accumulates over weeks and how it relates to your other health signals.",
- "respiratoryRateBody": "Respiratory rate is the number of breaths you take per minute. As general guidance, at rest most adults breathe 12–20 times a minute (American Lung Association); it is meaningful only truly at rest or asleep. A sustained rise of about +3 above your own nightly baseline can be an early illness signal — clinical escalation cutoffs are hospital tools, not self-alarms.",
+ "respiratoryRateBody": "Respiratory rate is the number of breaths you take per minute. As general guidance, at rest most adults breathe 12–20 times a minute (American Lung Association); it is meaningful only truly at rest or asleep. A sustained rise of about +3 above your own nightly baseline can be an early illness signal; clinical escalation cutoffs are hospital tools, not self-alarms.",
"bodyWaterBody": "Total body water is the share of your body weight made up of fluid, inside and around your cells. It shifts with hydration, food, and activity, so the trend over weeks tells you more than any single reading.",
"boneMassBody": "Bone mass is the estimated weight of the mineral in your skeleton. Scale-based estimates are approximate, but a steady or rising trend supports long-term bone strength.",
- "fatFreeMassBody": "Fat-free mass is everything in your body that is not fat — muscle, bone, organs, and water. Holding or building it while losing weight usually signals healthier body composition.",
- "fatMassBody": "Fat mass is the total weight of fat in your body. Some fat is essential for hormones and energy storage, but too much — especially around the abdomen — raises long-term health risk (WHO 2024).",
+ "fatFreeMassBody": "Fat-free mass is everything in your body that is not fat: muscle, bone, organs, and water. Holding or building it while losing weight usually signals healthier body composition.",
+ "fatMassBody": "Fat mass is the total weight of fat in your body. Some fat is essential for hormones and energy storage, but too much, especially around the abdomen, raises long-term health risk (WHO 2024).",
"bodyFatBody": "Body fat percentage is the share of your weight that is fat. Healthy ranges depend on sex and age, and home scales estimate it from bioimpedance, so the trend over several weeks says more than a single reading.",
"muscleMassBody": "Muscle mass is the estimated weight of the muscle in your body. More muscle supports strength, metabolism, and mobility, and it tends to need active effort to preserve as you age.",
"visceralFatBody": "Visceral fat is the fat stored deep in the abdomen around your organs. Unlike fat under the skin, a high level is closely linked to heart disease and type 2 diabetes, so lower is generally better.",
- "leanBodyMassBody": "Lean body mass is your total weight minus body fat — muscle, bone, organs, and water combined. Tracking it alongside weight shows whether changes come from muscle or from fat.",
+ "leanBodyMassBody": "Lean body mass is your total weight minus body fat: muscle, bone, organs, and water combined. Tracking it alongside weight shows whether changes come from muscle or from fat.",
"flightsClimbedBody": "Flights climbed counts the floors you go up each day, with one flight roughly ten feet of elevation. It is a simple marker of incidental activity and lower-body effort.",
"walkingDistanceBody": "Walking and running distance is the ground you cover on foot each day. It complements step count by reflecting how far you moved, not just how often you stepped.",
"walkingSteadinessBody": "Walking steadiness estimates how stable and balanced your gait is, classed from OK to low. A lower level points to a higher chance of a fall and is worth discussing with a clinician.",
@@ -3270,36 +3427,36 @@
"doubleSupportTimeBody": "Double support time is the share of each walking cycle when both feet touch the ground at once. A higher percentage usually means a more cautious, less stable gait.",
"stepLengthBody": "Step length is the average distance from one footfall to the next. A shorter step over time can reflect fatigue, caution, or a change in mobility.",
"walkingSpeedBody": "Walking speed is how fast you move on a level walk. It is a well-studied marker of overall health and mobility, and a steady or rising pace is a good sign.",
- "pulseWaveVelocityBody": "Pulse wave velocity measures how fast the pressure wave from each heartbeat travels along your arteries. A faster wave indicates stiffer arteries, which is linked to higher cardiovascular risk. As general guidance, European clinical reference flags carotid-femoral velocity above 10 m/s (ESC/ESH 2018) — but a consumer estimate is a proxy, not interchangeable with that clinical measurement.",
+ "pulseWaveVelocityBody": "Pulse wave velocity measures how fast the pressure wave from each heartbeat travels along your arteries. A faster wave indicates stiffer arteries, which is linked to higher cardiovascular risk. As general guidance, European clinical reference flags carotid-femoral velocity above 10 m/s (ESC/ESH 2018), but a consumer estimate is a proxy, not interchangeable with that clinical measurement.",
"vascularAgeBody": "Vascular age estimates how old your arteries behave compared with your actual age, based on arterial stiffness. A vascular age below your real age suggests healthier blood vessels.",
"environmentalAudioBody": "Environmental sound levels track the loudness of the world around you in decibels. Prolonged exposure above about 70 dB can damage hearing over time, so lower daily averages are safer.",
"headphoneAudioBody": "Headphone audio levels track how loudly you listen through earbuds or headphones, in decibels. Keeping the volume down and taking breaks protects your hearing over the long run.",
"audioEventsBody": "Loud sound events flag moments when noise around you reached a level that can harm hearing with enough exposure. Frequent events are a cue to seek quieter surroundings or hearing protection.",
"daylightBody": "Time in daylight is how long you spend in outdoor light each day. Regular daylight supports a healthy sleep-wake rhythm and mood (CDC 2024), and time outdoors is linked to eye health.",
- "bloodGlucoseBody": "Blood glucose is the amount of sugar in your blood, the body's main energy source. It rises after eating and settles between meals. As general guidance for adults without diabetes, a fasting reading under {prediabetesFrom} {unit} is normal, {prediabetesFrom}–{prediabetesTo} is prediabetes, and {diabetesFrom} or above falls in the diabetes range (ADA 2024). A single high reading does not diagnose — a clinician confirms with a repeat test, and people managing diabetes use different, clinician-set targets.",
+ "bloodGlucoseBody": "Blood glucose is the amount of sugar in your blood, the body's main energy source. It rises after eating and settles between meals. As general guidance for adults without diabetes, a fasting reading under {prediabetesFrom} {unit} is normal, {prediabetesFrom}–{prediabetesTo} is prediabetes, and {diabetesFrom} or above falls in the diabetes range (ADA 2024). A single high reading does not diagnose; a clinician confirms with a repeat test, and people managing diabetes use different, clinician-set targets.",
"skinTemperatureBody": "Skin temperature here is the small nightly change in your wrist temperature from your own baseline, not an absolute fever reading. Shifts can track sleep, environment, illness, or menstrual-cycle phase.",
- "cardioFitnessBody": "VO₂ max is how much oxygen your body can use at peak effort — the best lab index of aerobic fitness, which wearables estimate indirectly. A higher or rising trend is a strong sign of cardiovascular health.",
+ "cardioFitnessBody": "VO₂ max is how much oxygen your body can use at peak effort, the best lab index of aerobic fitness, which wearables estimate indirectly. A higher or rising trend is a strong sign of cardiovascular health.",
"cardioRecoveryBody": "Cardio recovery is how many beats per minute your heart rate falls in the first minute after you stop hard exercise. A bigger drop means your heart settles down faster, which is a sign of good fitness.",
"wristTemperatureBody": "Wrist temperature is measured while you sleep and reported as the difference from your personal baseline, not an absolute value. A steady reading is normal, while a sustained rise can hint at illness, intense training, or a menstrual-cycle change.",
"fallsBody": "This counts the hard falls your wearable detects. Zero is the goal; a rising count is a mobility-risk signal worth raising with a clinician, especially for older adults (CDC STEADI 2024).",
- "sixMinuteWalkBody": "This is an estimate of how far you could walk in six minutes — a long-standing clinical test of mobility and cardiopulmonary endurance. The wearable figure is a proxy, not the supervised clinical test; a steady or rising trend is the useful read.",
- "stairAscentSpeedBody": "This is how fast you climb a flight of stairs. It is a sensitive marker of leg strength and mobility — a steady or rising pace is a good sign, and a falling one is worth watching.",
- "stairDescentSpeedBody": "This is how fast you go down a flight of stairs. Together with ascent speed it tracks leg control and balance — a steady pace is a good sign, and a falling one is worth watching.",
+ "sixMinuteWalkBody": "This is an estimate of how far you could walk in six minutes, a long-standing clinical test of mobility and cardiopulmonary endurance. The wearable figure is a proxy, not the supervised clinical test; a steady or rising trend is the useful read.",
+ "stairAscentSpeedBody": "This is how fast you climb a flight of stairs. It is a sensitive marker of leg strength and mobility. A steady or rising pace is a good sign, and a falling one is worth watching.",
+ "stairDescentSpeedBody": "This is how fast you go down a flight of stairs. Together with ascent speed it tracks leg control and balance. A steady pace is a good sign, and a falling one is worth watching.",
"breathingDisturbancesBody": "A wearable can estimate how often your breathing was interrupted during sleep. A lower index is better; a level flagged as elevated, especially across many nights, is worth raising with a clinician. It is a screening proxy, not a sleep-apnea diagnosis.",
- "stepsBody": "Step count is the total number of steps you take each day — the simplest measure of everyday movement. A steady habit matters more than any single big day, and the round 10,000 target is a marketing slogan, not research.",
- "readinessBody": "Readiness is a single daily read on how prepared your body is to take on the day, blending resting heart rate, heart-rate variability, and sleep. It is a device proxy benchmarked against your own baseline, not a clinical measure — a higher score means recovered, a lower one is a gentle cue to ease off.",
- "sleepScoreBody": "Your sleep score distils a whole night into one number — duration, efficiency, regularity, and the balance of light, deep, and REM. Watch the pattern across weeks rather than chasing a perfect score on any single night.",
+ "stepsBody": "Step count is the total number of steps you take each day, the simplest measure of everyday movement. A steady habit matters more than any single big day, and the round 10,000 target is a marketing slogan, not research.",
+ "readinessBody": "Readiness is a single daily read on how prepared your body is to take on the day, blending resting heart rate, heart-rate variability, and sleep. It is a device proxy benchmarked against your own baseline, not a clinical measure. A higher score means recovered, a lower one is a gentle cue to ease off.",
+ "sleepScoreBody": "Your sleep score distils a whole night into one number: duration, efficiency, regularity, and the balance of light, deep, and REM. Watch the pattern across weeks rather than chasing a perfect score on any single night.",
"recoveryBody": "Recovery reflects how well your body has bounced back, drawing on overnight signals like heart-rate variability and resting heart rate. It is a device-derived proxy read against your own baseline, not a clinical measure, and is not comparable across devices. A strong score is a green light to push; a softer one suggests your system is still rebuilding.",
"strainBody": "Strain captures how much cardiovascular and physical load you have put on your body, from workouts to an active day on your feet. It is a device-derived index judged against your own typical effort, not a clinical measure, and the headline number is not comparable across devices.",
- "stressBody": "Stress here is a device heuristic derived from heart-rate-variability patterns, not a cortisol measurement or how you consciously feel — and not a clinical stress score. Read it against your own baseline; it is not comparable across devices. A lower score points to a calm, well-regulated state; a higher one is an invitation to build in recovery.",
- "recoveryPageBody": "Recovery brings together the overnight signals your wearable records — recharge, strain, load and your day's heart-rate and energy totals — to show how well your body has bounced back. Read the pattern across days rather than any single number; a softer reading is an invitation to ease off, not an alarm.",
+ "stressBody": "Stress here is a device heuristic derived from heart-rate-variability patterns, not a cortisol measurement or how you consciously feel, and not a clinical stress score. Read it against your own baseline; it is not comparable across devices. A lower score points to a calm, well-regulated state; a higher one is an invitation to build in recovery.",
+ "recoveryPageBody": "Recovery brings together the overnight signals your wearable records (recharge, strain, load and your day's heart-rate and energy totals) to show how well your body has bounced back. Read the pattern across days rather than any single number; a softer reading is an invitation to ease off, not an alarm.",
"pulsePressureBody": "Pulse pressure is the gap between your systolic and diastolic readings (systolic minus diastolic). As general guidance, about 40 mmHg is typical at rest; a wide gap above roughly 60 mmHg in middle age can reflect stiffening arteries, while a narrow gap under about 25 mmHg can accompany low stroke volume (StatPearls/NIH 2023). It is a discussion prompt, not a diagnosis.",
"meanArterialPressureBody": "Mean arterial pressure is the average pressure your organs see across a heartbeat, roughly the diastolic plus a third of the gap to systolic. As general guidance, a resting value sits around 70–100 mmHg (StatPearls/NIH 2023). The often-cited ~60 mmHg floor is an acute-care concept, not a consumer self-alarm.",
- "cycleEducationBody": "Your cycle runs through four phases driven by shifting hormones, and its length and regularity are an accessible window on overall health. As general guidance, adult cycles typically run 21–35 days (clinical references cite 24–38), with bleeding around five days (ACOG 2015). The luteal phase is the steady anchor, so ovulation is best estimated by counting back from your next expected period — never a fixed 'day 14'. A predicted fertile window is an estimate that needs several recorded cycles and stays approximate, and basal body temperature confirms ovulation after the fact rather than predicting it. This is not a contraceptive method.",
+ "cycleEducationBody": "Your cycle runs through four phases driven by shifting hormones, and its length and regularity are an accessible window on overall health. As general guidance, adult cycles typically run 21–35 days (clinical references cite 24–38), with bleeding around five days (ACOG 2015). The luteal phase is the steady anchor, so ovulation is best estimated by counting back from your next expected period, never a fixed 'day 14'. A predicted fertile window is an estimate that needs several recorded cycles and stays approximate, and basal body temperature confirms ovulation after the fact rather than predicting it. This is not a contraceptive method.",
"sleepHygieneBody": "A few evidence-based habits support sleep: keep consistent bed and wake times including weekends, get bright morning daylight and dim your evenings, keep the room cool, dark and quiet, and avoid afternoon caffeine, late alcohol and large late meals (CDC 2024). Regularity predicts health at least as strongly as duration, and a wearable's total sleep time is far more trustworthy than its deep, light and REM split."
},
"bpDerived": {
- "pulsePressure": "Pulse pressure (the gap between your readings) is about {value} mmHg — roughly 40 is typical at rest.",
+ "pulsePressure": "Pulse pressure (the gap between your readings) is about {value} mmHg; roughly 40 is typical at rest.",
"meanArterialPressure": "Mean arterial pressure (the average your organs see) is about {value} mmHg, normally around 70–100.",
"caveat": "These are derived from your latest reading as general guidance, not a diagnosis."
},
@@ -3311,7 +3468,7 @@
"mean": "Mean"
},
"showAllValues": "Show all readings",
- "valuesPageTitle": "{metric} — all readings",
+ "valuesPageTitle": "{metric}: all readings",
"valuesPageDescription": "Every reading behind this metric. Edit or remove an entry directly.",
"diversity": {
"trigger": "Measurement tip",
@@ -3351,11 +3508,11 @@
"inBed": "In bed",
"nap": "Nap",
"napCount": "{count} naps",
- "unavailable": "No stage data yet — sleep stages appear here as soon as a connected source delivers them."
+ "unavailable": "No stage data yet. Sleep stages appear here as soon as a connected source delivers them."
},
"empty": {
"title": "No sleep data yet",
- "description": "Connect a source like Apple Health or a wearable — or log sleep manually — and this view fills in automatically.",
+ "description": "Connect a source like Apple Health or a wearable, or log sleep manually, and this view fills in automatically.",
"action": "Manage devices"
},
"debt": {
@@ -3364,21 +3521,21 @@
"caption": "Outstanding balance over the last {nights} nights",
"clearValue": "All caught up",
"clearCaption": "Nothing outstanding over the last {nights} nights",
- "learning": "Still learning your sleep — {counted} nights so far, {remaining} more to read a reliable balance.",
+ "learning": "Still learning your sleep: {counted} nights so far, {remaining} more to read a reliable balance.",
"computedInfo": "This is HealthLog's own estimate: a rolling balance of need minus sleep over recent nights, with catch-up sleep paying it down. It can differ from a wearable's native sleep-debt figure, which uses its own need model."
},
"average": {
"title": "Average per night",
"value": "{hours}h {minutes}m",
"caption": "Mean sleep across the last {nights} nights",
- "learning": "Still learning your sleep — {counted} nights so far, {remaining} more for a reliable average."
+ "learning": "Still learning your sleep: {counted} nights so far, {remaining} more for a reliable average."
},
"chronotype": {
"title": "Chronotype",
- "learning": "Still learning your rhythm — {counted} of {total} free nights. A chronotype needs a few alarm-free mornings before it means anything.",
+ "learning": "Still learning your rhythm: {counted} of {total} free nights. A chronotype needs a few alarm-free mornings before it means anything.",
"advancedToggle": "Advanced",
"socialJetlagValue": "{hours}h {minutes}m social jetlag",
- "socialJetlagCaption": "The gap between your work-day and free-day sleep timing — like a small weekly time-zone shift.",
+ "socialJetlagCaption": "The gap between your work-day and free-day sleep timing, like a small weekly time-zone shift.",
"msfScValue": "Corrected mid-sleep {clock}",
"msfScCaption": "Adjusted for the sleep you catch up on at the weekend (MSFsc).",
"dayTypeNote": "Free days are assumed to be weekends until day-type tracking is added.",
@@ -3567,7 +3724,10 @@
"endInFuture": "It would end in the future. Set the start to when the workout began.",
"distanceInvalid": "Enter the distance as a number within a realistic range.",
"energyInvalid": "Enter active energy as a number up to 20,000 kcal."
- }
+ },
+ "editTitle": "Edit workout",
+ "editAction": "Edit",
+ "updated": "Workout updated"
}
},
"bloodGlucose": {
@@ -3582,11 +3742,11 @@
"clinical": {
"title": "Glucose overview",
"subtitle": "A read on your last {days} days, from {count} readings.",
- "spotCaveat": "These are spot-reading estimates from individual measurements, not a continuous-monitor profile — read them as a direction of travel rather than a clinical verdict.",
+ "spotCaveat": "These are spot-reading estimates from individual measurements, not a continuous-monitor profile. Read them as a direction of travel rather than a clinical verdict.",
"labsLink": "Lab results? Log a blood-draw HbA1c or fasting glucose under Labs.",
"learningTitle": "Still learning your glucose",
- "learningBody": "So far that's {count} over {days}. A little more history and a fuller picture comes into focus — keep logging, or let a connected device fill it in.",
- "learningBodyEmpty": "Once a handful of readings come in — logged by you or synced from a device — your time-in-range, average, and variability take shape here.",
+ "learningBody": "So far that's {count} over {days}. A little more history and a fuller picture comes into focus. Keep logging, or let a connected device fill it in.",
+ "learningBodyEmpty": "Once a handful of readings come in (logged by you or synced from a device), your time-in-range, average, and variability take shape here.",
"readingsCount": "{count} readings",
"readingsCountOne": "1 reading",
"daysCount": "{days} days",
@@ -3624,7 +3784,7 @@
"intro": "Research-grade composites for a closer look. Each folds your readings into a single risk-weighted number.",
"jIndex": {
"label": "J-index",
- "help": "A single score blending your average and its spread — lower reads as tighter control."
+ "help": "A single score blending your average and its spread; lower reads as tighter control."
},
"lbgi": {
"label": "Low-glucose risk (LBGI)",
@@ -3657,7 +3817,7 @@
"description": "Body-water readings appear here once a connected body-composition scale syncs."
}
},
- "navTotalBodyWater": "Body Water",
+ "navTotalBodyWater": "Body water",
"boneMass": {
"title": "Bone Mass",
"description": "Your bone mass over time, in kilograms.",
@@ -3667,7 +3827,7 @@
"description": "Bone-mass readings appear here once a connected body-composition scale syncs."
}
},
- "navBoneMass": "Bone Mass",
+ "navBoneMass": "Bone mass",
"flightsClimbed": {
"title": "Flights Climbed",
"description": "Flights of stairs you climbed each day.",
@@ -3697,7 +3857,7 @@
"description": "Fat-free-mass readings appear here once a connected body-composition scale syncs."
}
},
- "navFatFreeMass": "Fat-Free Mass",
+ "navFatFreeMass": "Fat-free mass",
"fatMass": {
"title": "Fat Mass",
"description": "Your fat mass over time, in kilograms.",
@@ -3717,8 +3877,8 @@
"cta": "Log body fat"
}
},
- "navFatMass": "Fat Mass",
- "navBodyFat": "Body Fat",
+ "navFatMass": "Fat mass",
+ "navBodyFat": "Body fat",
"muscleMass": {
"title": "Muscle Mass",
"description": "Your muscle mass over time, in kilograms.",
@@ -3728,7 +3888,7 @@
"description": "Muscle-mass readings appear here once a connected body-composition scale syncs."
}
},
- "navMuscleMass": "Muscle Mass",
+ "navMuscleMass": "Muscle mass",
"skinTemperature": {
"title": "Skin Temperature",
"description": "Your wrist skin temperature over time, in degrees Celsius.",
@@ -3738,17 +3898,17 @@
"description": "Skin-temperature readings appear here once a wearable syncs overnight wrist temperature."
}
},
- "navSkinTemperature": "Skin Temp",
+ "navSkinTemperature": "Skin temp",
"pulseWaveVelocity": {
"title": "Pulse Wave Velocity",
- "description": "Your arterial pulse-wave velocity over time, in m/s — higher values suggest stiffer arteries.",
+ "description": "Your arterial pulse-wave velocity over time, in m/s. Higher values suggest stiffer arteries.",
"chartTitle": "Pulse wave velocity",
"emptyState": {
"title": "No pulse-wave-velocity data yet",
"description": "Pulse-wave-velocity readings appear here once a compatible Withings device syncs."
}
},
- "navPulseWaveVelocity": "Pulse Wave Velocity",
+ "navPulseWaveVelocity": "Pulse wave velocity",
"vascularAge": {
"title": "Vascular Age",
"description": "Your estimated vascular age over time, in years.",
@@ -3758,7 +3918,7 @@
"description": "Vascular-age estimates appear here once a compatible Withings device syncs."
}
},
- "navVascularAge": "Vascular Age",
+ "navVascularAge": "Vascular age",
"visceralFat": {
"title": "Visceral Fat",
"description": "Your visceral-fat rating over time.",
@@ -3768,7 +3928,7 @@
"description": "Visceral-fat ratings appear here once a compatible body-composition scale syncs."
}
},
- "navVisceralFat": "Visceral Fat",
+ "navVisceralFat": "Visceral fat",
"audioExposureEnv": {
"title": "Environmental Audio",
"description": "Your daily average environmental sound exposure, in dBA.",
@@ -3778,7 +3938,7 @@
"description": "Environmental sound-level readings appear here once Apple Health syncs."
}
},
- "navAudioExposureEnv": "Environmental Audio",
+ "navAudioExposureEnv": "Environmental audio",
"audioExposureHeadphone": {
"title": "Headphone Audio",
"description": "Your daily average headphone audio exposure, in dBA.",
@@ -3788,7 +3948,7 @@
"description": "Headphone audio-level readings appear here once Apple Health syncs."
}
},
- "navAudioExposureHeadphone": "Headphone Audio",
+ "navAudioExposureHeadphone": "Headphone audio",
"timeInDaylight": {
"title": "Time in Daylight",
"description": "Minutes spent in daylight each day.",
@@ -3808,7 +3968,7 @@
"description": "Walking-steadiness readings appear here once Apple Health syncs."
}
},
- "navWalkingSteadiness": "Walking Steadiness",
+ "navWalkingSteadiness": "Walking steadiness",
"audioExposureEvent": {
"title": "Loud Audio Events",
"description": "Days with a loud-environment exposure event.",
@@ -3818,7 +3978,7 @@
"description": "Loud-environment events appear here once Apple Health syncs."
}
},
- "navAudioExposureEvent": "Loud Events",
+ "navAudioExposureEvent": "Loud events",
"respiratoryRate": {
"title": "Respiratory Rate",
"description": "Your respiratory rate over time, in breaths per minute.",
@@ -3828,7 +3988,7 @@
"description": "Respiratory-rate readings appear here once Apple Health syncs sleep or workout breathing."
}
},
- "navRespiratoryRate": "Respiratory Rate",
+ "navRespiratoryRate": "Respiratory rate",
"leanBodyMass": {
"title": "Lean Body Mass",
"description": "Your lean body mass over time, in kilograms.",
@@ -3838,7 +3998,7 @@
"description": "Lean-body-mass readings appear here once a connected body-composition scale syncs."
}
},
- "navLeanBodyMass": "Lean Body Mass",
+ "navLeanBodyMass": "Lean body mass",
"walkingHeartRateAverage": {
"title": "Walking Heart Rate",
"description": "Your average heart rate while walking, in bpm.",
@@ -3848,7 +4008,7 @@
"description": "Walking-heart-rate readings appear here once Apple Health syncs."
}
},
- "navWalkingHeartRateAverage": "Walking Heart Rate",
+ "navWalkingHeartRateAverage": "Walking heart rate",
"walkingAsymmetry": {
"title": "Walking Asymmetry",
"description": "Your walking asymmetry over time, as a percentage.",
@@ -3858,7 +4018,7 @@
"description": "Walking-asymmetry readings appear here once Apple Health syncs."
}
},
- "navWalkingAsymmetry": "Walking Asymmetry",
+ "navWalkingAsymmetry": "Walking asymmetry",
"walkingDoubleSupport": {
"title": "Double Support Time",
"description": "The percentage of each stride spent with both feet on the ground.",
@@ -3868,7 +4028,7 @@
"description": "Double-support readings appear here once Apple Health syncs."
}
},
- "navWalkingDoubleSupport": "Double Support",
+ "navWalkingDoubleSupport": "Double support",
"walkingStepLength": {
"title": "Walking Step Length",
"description": "Your average walking step length over time, in metres.",
@@ -3878,7 +4038,7 @@
"description": "Step-length readings appear here once Apple Health syncs."
}
},
- "navWalkingStepLength": "Step Length",
+ "navWalkingStepLength": "Step length",
"walkingSpeed": {
"title": "Walking Speed",
"description": "Your average walking speed over time, in km/h.",
@@ -3888,10 +4048,9 @@
"description": "Walking-speed readings appear here once Apple Health syncs."
}
},
- "navWalkingSpeed": "Walking Speed",
+ "navWalkingSpeed": "Walking speed",
"mood": {
"logLink": "Log a mood entry",
- "mentalWellbeingLink": "Take a mental-wellbeing check-in",
"heatmapTitle": "Mood calendar",
"heatmapNoEntry": "No entry",
"legendGreat": "Great",
@@ -3902,7 +4061,7 @@
"distributionDaysUnit": "days",
"weekdayTitle": "Average by weekday",
"weekdayNoData": "No data",
- "weekdayBestWorst": "Best day: {best} · Toughest day: {worst}",
+ "weekdayBestWorst": "Best day: {best}, toughest day: {worst}",
"tagsTitle": "Tags and triggers",
"tagAvgMood": "Average mood when tagged",
"correlationsTitle": "Mood and other metrics",
@@ -3918,7 +4077,7 @@
"strengthModerate": "Moderate",
"strengthWeak": "Weak",
"strengthNone": "None",
- "source": "{n} paired days · r = {r}",
+ "source": "{n} paired days, r = {r}",
"sourceLabel": "How this was computed",
"emptyTitle": "Not enough paired data yet",
"emptyDescription": "Log mood and this metric on the same days to see a correlation.",
@@ -3935,7 +4094,7 @@
"inTargetCaption": "of recent days in your good-mood range",
"narrative": {
"title": "What stands out",
- "weekdayDip": "Your mood dips most on {weekday} — {delta} below your week.",
+ "weekdayDip": "Your mood dips most on {weekday}, {delta} below your week.",
"weekdayPeak": "{weekday} is consistently your best day, averaging {value}.",
"trendUp": "Your mood is trending up over the last {days} days (+{delta} vs the prior period).",
"trendDown": "Your mood is trending down over the last {days} days (-{delta} vs the prior period).",
@@ -3944,7 +4103,7 @@
"tagLift": "Days tagged \"{tag}\" average {delta} above your overall mood.",
"tagDrop": "Days tagged \"{tag}\" run {delta} below your average.",
"inTarget": "You've been in your good-mood range {pct}% of the last 30 days.",
- "streak": "You've logged your mood {days} days in a row — keep it up.",
+ "streak": "You've logged your mood {days} days in a row. Keep it up.",
"timeOfDay": "You feel best in the {bucket}, averaging {value}."
},
"timeOfDay": {
@@ -3977,7 +4136,7 @@
"crosstab": {
"title": "Tags and your health metrics",
"description": "How a health metric differs on the days you tagged each activity, compared with the days you did not.",
- "pairLabel": "{tag} · {metric}",
+ "pairLabel": "{metric} on days with {tag}",
"metricActiveEnergy": "Active energy",
"metricSleepDuration": "Sleep",
"metricNextDayRecovery": "Next-day recovery",
@@ -3994,7 +4153,7 @@
"factorCrosstab": {
"title": "Your rated factors and your vitals",
"description": "How a vital differs on the days you rated each factor low compared with the days you rated it high.",
- "pairLabel": "{factor} · {metric}",
+ "pairLabel": "{metric} on days with {factor}",
"metricSleepDuration": "Sleep",
"metricSteps": "Steps",
"metricRestingHeartRate": "Resting heart rate",
@@ -4015,12 +4174,12 @@
"title": "What stands out",
"subheading": "Discovered relationships",
"footer": "Surfaced from {tested} day-to-day pairs tested across all your metrics, after the false-discovery correction.",
- "disclaimer": "Statistically filtered, but still observational — a pattern worth watching, not a cause.",
+ "disclaimer": "Statistically filtered, but still observational: a pattern worth watching, not a cause.",
"moodUp": "Higher {factor} tends to go with higher next-day mood in your data.",
"moodDown": "Higher {factor} tends to go with lower next-day mood in your data.",
"outcomeUp": "Higher mood tends to go with higher next-day {factor} in your data.",
"outcomeDown": "Higher mood tends to go with lower next-day {factor} in your data.",
- "stat": "{n} paired days · r = {r} · q = {q}",
+ "stat": "{n} paired days, r = {r}, q = {q}",
"statLabel": "How this was computed",
"explainerLabel": "How this was computed"
},
@@ -4054,8 +4213,8 @@
"deviationAbove": "Your own rating sits {amount} points above that range.",
"deviationBelow": "Your own rating sits {amount} points below that range.",
"weightedTitle": "What weighed most in this day's comparison",
- "weightedUp": "{label} — counted towards a higher expected value",
- "weightedDown": "{label} — counted towards a lower expected value",
+ "weightedUp": "{label}: counted towards a higher expected value",
+ "weightedDown": "{label}: counted towards a lower expected value",
"caveat": "This is a comparison with your own past days, not a statement about how a day came to go the way it did.",
"learning": "Learning phase: {entries} of {threshold} days recorded. A comparison appears once there are enough of them.",
"noPattern": "Nothing in the {entries} days recorded so far is steady enough to compare a day against. When you also capture a day's context, more of it becomes comparable.",
@@ -4064,15 +4223,15 @@
},
"derived": {
"coverage": {
- "summary": "{present} of {required} inputs · {percent}%",
+ "summary": "{present} of {required} inputs ({percent}%)",
"ratioLabel": "{present}/{required}",
"historyDays": "Based on {count} days of history",
"missing": "Track these to sharpen this: {list}",
- "summaryLabelled": "{label} · {percent}%"
+ "summaryLabelled": "{label} ({percent}%)"
},
"scoreRing": {
"aria": "Score {score} out of 100, {band}",
- "ariaProvisional": "Score not available yet — not enough data",
+ "ariaProvisional": "Score not available yet, not enough data",
"provisionalCaption": "Not enough data yet",
"band": {
"green": "good",
@@ -4086,7 +4245,7 @@
"inputsLabel": "Inputs",
"noInputs": "No inputs backed this value yet.",
"methodLabel": "Method",
- "window": "From your last {days} days · {source} data",
+ "window": "From your last {days} days of {source} data",
"asOf": "as of {date}",
"standardLink": "Standard: {name}"
},
@@ -4096,7 +4255,7 @@
"loadingLabel": "Loading your vitals",
"loadError": "Could not load your vitals right now.",
"infoLabel": "How is this calculated?",
- "building": "Building your typical range — {count} of {target} days",
+ "building": "Building your typical range: {count} of {target} days",
"typicalRange": "Typical for you: {low}–{high}",
"fitnessYounger": "Cardio fitness of someone ~{years} yr younger",
"fitnessOlder": "Cardio fitness ~{years} yr above your age",
@@ -4128,9 +4287,8 @@
"anatomy": {
"outOf": "/100",
"contributorsLabel": "Contributors",
- "contributorValue": "{value} · {weight}% weight",
+ "contributorValue": "{value}, weighted {weight}%",
"contributorMissing": "no data",
- "disclaimer": "Indicative — not a clinical assessment.",
"insufficient": "Not enough data yet to score this. Keep tracking and it will appear here."
},
"composite": {
@@ -4149,73 +4307,71 @@
"READINESS": {
"title": "Readiness",
"description": "A daily wellness proxy from how your signals sit against your own baseline.",
- "method": "A 0–100 blend of resting-HR deviation, HRV (SDNN) deviation, sleep adequacy, respiratory-rate stability and mood stability — each measured against your personal baseline. Weights renormalise over the components your data supports. A wellness proxy, not a recovery or clinical score.",
+ "method": "A 0–100 blend of resting-HR deviation, HRV (SDNN) deviation, sleep adequacy, respiratory-rate stability and mood stability, each measured against your personal baseline. Weights renormalise over the components your data supports. A wellness proxy, not a recovery or clinical score.",
"component": {
"rhr": "Resting heart rate",
"hrv": "HRV (SDNN)",
"sleep": "Sleep",
"respiratory": "Respiratory rate",
"mood": "Mood stability"
- },
- "caveat": "A wellness proxy, not a recovery or clinical score."
+ }
},
"VITALS_BASELINE": {
- "method": "Your personal typical range is the median of your daily values over the window, ± a robust spread (median absolute deviation, scaled). It tracks YOUR readings, not a population norm — so a value inside the band is typical for you."
+ "method": "Your personal typical range is the median of your daily values over the window, ± a robust spread (median absolute deviation, scaled). It tracks YOUR readings, not a population norm, so a value inside the band is typical for you."
},
"HRV_BALANCE": {
"method": "Your recent HRV (SDNN) average placed against your own rolling baseline (median ± a robust spread). Balanced means it sits inside your personal range; the band is built from your data, never a population cut-off."
},
"BMI": {
- "method": "Weight ÷ height², classified against the WHO adult categories (underweight / normal / overweight / obese). A screening ratio only — it does not distinguish muscle from fat."
+ "method": "Weight ÷ height², classified against the WHO adult categories (underweight / normal / overweight / obese). A screening ratio only; it does not distinguish muscle from fat."
},
"FITNESS_AGE": {
- "method": "Your VO₂max placed against age- and sex-referenced cardiorespiratory-fitness percentiles, expressed as a fitness-age delta. The VO₂max is your device's estimate — never recomputed here."
+ "method": "Your VO₂max placed against age- and sex-referenced cardiorespiratory-fitness percentiles, expressed as a fitness-age delta. The VO₂max is your device's estimate, never recomputed here."
},
"VASCULAR_AGE_DELTA": {
"method": "Your device's vascular-age / pulse-wave-velocity estimate compared with your chronological age, expressed as a delta in years. The estimate is the device's; HealthLog only re-frames it.",
- "caveat": "Your device's own estimate, re-framed — descriptive, not a clinical vascular assessment.",
+ "caveat": "Your device's own estimate, re-framed: descriptive, not a clinical vascular assessment.",
"description": "How your device's vascular-age estimate sits against your chronological age."
},
"COINCIDENT_DEVIATION": {
"method": "A flag raised when two or more of your vitals sit outside their personal typical range on the same day. Each band is the robust median ± k·MAD of your own history, so it adapts to you rather than to a population cut-off.",
- "caveat": "An awareness flag from your own baselines — descriptive, not a diagnosis or a clinical alert."
+ "caveat": "An awareness flag from your own baselines: descriptive, not a diagnosis or a clinical alert."
},
"RECOVERY_SCORE": {
- "method": "A nightly 0–100 recovery proxy blended from your HRV, resting heart rate and sleep against your own baseline. Higher means better-recovered. Computed once each night and read back here — never recomputed on the fly.",
- "caveat": "A descriptive wellness proxy, not a training-grade or clinical recovery measure.",
+ "method": "A nightly 0–100 recovery proxy blended from your HRV, resting heart rate and sleep against your own baseline. Higher means better-recovered. Computed once each night and read back here, never recomputed on the fly.",
"description": "A nightly recovery proxy from your HRV, resting heart rate and sleep."
},
"STRESS_SCORE": {
"method": "A nightly 0–100 stress proxy from the shape of your intra-day HRV against your baseline. Higher means more physiological load.",
- "caveat": "An HRV-derived proxy, NOT an EDA / galvanic-skin stress measurement — your watch has no EDA sensor. Descriptive, not clinical.",
"description": "A nightly stress proxy from the shape of your intra-day HRV."
},
"STRAIN_SCORE": {
"method": "A nightly 0–100 strain proxy from your heart-rate-weighted activity load (a TRIMP-style training-impulse model). Higher means a harder day.",
- "anchorPersonal": "Anchored to your own recent training-day load — scored relative to your typical effort, so a genuinely hard day for you reads high.",
+ "anchorPersonal": "Anchored to your own recent training-day load, scored relative to your typical effort, so a genuinely hard day for you reads high.",
"anchorPopulation": "Scored against a general reference while you build training history; once you have enough it switches to your own typical effort.",
- "caveat": "A descriptive activity-load proxy, not a clinical or training-grade load measure.",
- "description": "A nightly activity-load proxy from your heart-rate-weighted effort, relative to your typical effort."
+ "description": "A nightly activity-load proxy from your heart-rate-weighted effort, relative to your typical effort.",
+ "deviceCaption": "Your device's day strain: {value} of {max}",
+ "deviceSource": "Until HealthLog has computed its own value overnight, the card shows your device's day strain, rescaled to 0–100."
},
"WRIST_TEMPERATURE_BASELINE": {
- "method": "Your typical overnight wrist temperature is the median of your nightly readings over the window, ± a robust spread (median absolute deviation, scaled). It tracks YOUR readings — last night is shown as a deviation from your own baseline, not a population norm.",
- "caveat": "A personal-deviation band only — descriptive, not an illness, fever or cycle-phase signal. Interpret it yourself."
+ "method": "Your typical overnight wrist temperature is the median of your nightly readings over the window, ± a robust spread (median absolute deviation, scaled). It tracks YOUR readings: last night is shown as a deviation from your own baseline, not a population norm.",
+ "caveat": "A personal-deviation band only: descriptive, not an illness, fever or cycle-phase signal. Interpret it yourself."
},
"STAIR_ASCENT_SPEED_BASELINE": {
- "method": "Your typical stair-climbing pace is the median of your daily values over the window, ± a robust spread (median absolute deviation, scaled). The band is built from your data — there is no population stair-speed cut-off, because pace depends on stair geometry and leg length.",
- "caveat": "A personal mobility-trend band — descriptive, not a frailty or fall-risk assessment."
+ "method": "Your typical stair-climbing pace is the median of your daily values over the window, ± a robust spread (median absolute deviation, scaled). The band is built from your data. There is no population stair-speed cut-off, because pace depends on stair geometry and leg length.",
+ "caveat": "A personal mobility-trend band: descriptive, not a frailty or fall-risk assessment."
},
"STAIR_DESCENT_SPEED_BASELINE": {
- "method": "Your typical stair-descent pace is the median of your daily values over the window, ± a robust spread (median absolute deviation, scaled). The band is built from your data — there is no population stair-speed cut-off, because pace depends on stair geometry and leg length.",
- "caveat": "A personal mobility-trend band — descriptive, not a frailty or fall-risk assessment."
+ "method": "Your typical stair-descent pace is the median of your daily values over the window, ± a robust spread (median absolute deviation, scaled). The band is built from your data. There is no population stair-speed cut-off, because pace depends on stair geometry and leg length.",
+ "caveat": "A personal mobility-trend band: descriptive, not a frailty or fall-risk assessment."
},
"SIX_MINUTE_WALK_BAND": {
- "method": "Your device's ESTIMATED six-minute-walk distance placed against the Enright & Sherrill reference for your age, height, weight and sex, as a percent of predicted. The distance is the device's estimate — never recomputed here. Without your age, height, weight and sex the percent is hidden and only the distance and trend are shown.",
- "caveat": "An estimate re-framed against a published reference — a functional-capacity awareness signal, not a clinical 6-minute-walk test or a diagnosis. The reference was established for ages 40–80; outside that range the predicted value is an extrapolation."
+ "method": "Your device's ESTIMATED six-minute-walk distance placed against the Enright & Sherrill reference for your age, height, weight and sex, as a percent of predicted. The distance is the device's estimate, never recomputed here. Without your age, height, weight and sex the percent is hidden and only the distance and trend are shown.",
+ "caveat": "An estimate re-framed against a published reference: a functional-capacity awareness signal, not a clinical 6-minute-walk test or a diagnosis. The reference was established for ages 40–80; outside that range the predicted value is an extrapolation."
},
"TRAJECTORY": {
"method": "A least-squares line fitted to your recent daily values and projected forward over a short horizon, with the textbook prediction interval that widens the further it reaches. Computed only when the recent trend is strong enough and your history is long and current enough; otherwise no line is drawn.",
- "caveat": "A projection of your own recent trend — not a prediction, a target, or a clinical forecast. It holds only if the recent pattern continues; the shaded band shows how quickly that certainty fades."
+ "caveat": "A projection of your own recent trend, not a prediction, a target, or a clinical forecast. It holds only if the recent pattern continues; the shaded band shows how quickly that certainty fades."
},
"SAME_TIME_BASELINE": {
"method": "Your running total for today set against the median of the same local hour across your last four weeks, with a robust median ± MAD band around it. Only days with enough recorded detail count towards it, and no comparison is shown until at least fourteen of them are in hand.",
@@ -4230,7 +4386,9 @@
"recovery": "Recovery score",
"recoveryCrossLink": "See your wearable's recovery & strain data",
"stress": "Stress",
- "strain": "Strain"
+ "strain": "Strain",
+ "historyTitle": "History",
+ "historyWindow": "Last {days} days"
},
"coincident": {
"label": "Several vitals off baseline",
@@ -4240,38 +4398,38 @@
"allClear": "All your vitals are within their personal range today.",
"allClearMeta": "Checked {count} of your vitals against your own typical range.",
"watch": "One vital is outside its usual range today.",
- "watchVital": "Outside range: {vital}. Often just a one-off — worth a glance over the next few days.",
+ "watchVital": "Outside range: {vital}. Often just a one-off, worth a glance over the next few days.",
"firedHeadline": "{count} of your vitals are outside their personal range today.",
- "factors": "Possible factors — never a cause: a hard workout, poor sleep, alcohol, altitude, stress, or illness. This is awareness from your own baselines, not a diagnosis.",
- "building": "Building your personal baselines — track a few more days and this will appear.",
+ "factors": "Possible factors (never a cause): a hard workout, poor sleep, alcohol, altitude, stress, or illness. This is awareness from your own baselines, not a diagnosis.",
+ "building": "Building your personal baselines. Track a few more days and this will appear.",
"illnessExplained": "These shifts likely reflect the condition you logged."
},
"trajectory": {
"cardTitle": "Short-horizon projection",
- "headline": "If this pattern continues — the next {days} days",
+ "headline": "If this pattern continues: the next {days} days",
"range": "Projected around {low}–{high} {unit} in {days} days.",
"caveat": "A projection of your own recent trend, not a prediction. The shaded band widens to show how quickly the uncertainty grows.",
- "insufficient": "Not enough of a clear trend to project yet — keep tracking and a projection will appear once the recent pattern is strong enough.",
+ "insufficient": "Not enough of a clear trend to project yet. Keep tracking and a projection will appear once the recent pattern is strong enough.",
"daysNeeded": "About {count} more days of tracking needed before a projection can appear."
},
"sameTime": {
"sectionTitle": "Today so far",
"sectionSubtitle": "Your running totals against what is usual for you at this hour",
- "learning": "Learning your usual day — {count} of {target} days",
+ "learning": "Learning your usual day: {count} of {target} days",
"framing": "Typical by {time}: {typical} {unit}",
"comparisonLabel": "Your typical day"
}
},
"cardioFitness": {
"title": "Cardio Fitness",
- "description": "Your VO₂ max over time — the headline measure of aerobic fitness.",
+ "description": "Your VO₂ max over time, the headline measure of aerobic fitness.",
"chartTitle": "VO₂ max (mL/(kg·min))",
"emptyState": {
"title": "No cardio-fitness data yet",
"description": "VO₂ max readings appear here once Apple Health syncs from your workouts."
}
},
- "navCardioFitness": "Cardio Fitness",
+ "navCardioFitness": "Cardio fitness",
"cardioRecovery": {
"title": "Cardio Recovery",
"description": "How far your heart rate drops one minute after peak exercise.",
@@ -4281,7 +4439,7 @@
"description": "Cardio-recovery readings appear here once Apple Health syncs from a workout."
}
},
- "navCardioRecovery": "Cardio Recovery",
+ "navCardioRecovery": "Cardio recovery",
"wristTemperature": {
"title": "Wrist Temperature",
"description": "Your overnight wrist temperature, tracked against your own baseline.",
@@ -4291,7 +4449,7 @@
"description": "Wrist-temperature readings appear here once Apple Health syncs overnight wear."
}
},
- "navWristTemperature": "Wrist Temp",
+ "navWristTemperature": "Wrist temp",
"falls": {
"title": "Falls",
"description": "Hard falls your Apple Watch detected, counted per day.",
@@ -4311,7 +4469,7 @@
"description": "Six-minute-walk estimates appear here once Apple Health syncs."
}
},
- "navSixMinuteWalk": "6-Min Walk",
+ "navSixMinuteWalk": "6-min walk",
"stairAscentSpeed": {
"title": "Stair Ascent Speed",
"description": "How fast you climb stairs, measured by your iPhone or Watch.",
@@ -4321,7 +4479,7 @@
"description": "Stair-ascent-speed readings appear here once Apple Health syncs."
}
},
- "navStairAscentSpeed": "Stair Ascent",
+ "navStairAscentSpeed": "Stair ascent",
"stairDescentSpeed": {
"title": "Stair Descent Speed",
"description": "How fast you go down stairs, measured by your iPhone or Watch.",
@@ -4331,7 +4489,7 @@
"description": "Stair-descent-speed readings appear here once Apple Health syncs."
}
},
- "navStairDescentSpeed": "Stair Descent",
+ "navStairDescentSpeed": "Stair descent",
"breathingDisturbances": {
"title": "Breathing Disturbances",
"description": "How often your breathing was disturbed during sleep.",
@@ -4344,7 +4502,7 @@
"navBreathingDisturbances": "Breathing",
"rhythmEvents": {
"sectionTitle": "Device health notifications",
- "sectionIntro": "Notifications your watch sent on its own. HealthLog only shows these — it does not detect, confirm, or interpret them.",
+ "sectionIntro": "Notifications your watch sent on its own. HealthLog only shows these. It does not detect, confirm, or interpret them.",
"loadError": "Could not load your device notifications.",
"disclaimer": "These are alerts your device produced using its own built-in detection. They are shown here for your awareness only. This is not a medical assessment by HealthLog, and HealthLog does not diagnose any condition. If a notification concerns you, talk to your doctor.",
"event": {
@@ -4389,13 +4547,13 @@
"detail": "Hidden detail pages are also removed from the top navigation."
},
"customize": "Customize Insights",
- "regenerateRateLimited": "Refresh limit reached — please try again later.",
- "regenerateUnavailable": "No fresh analysis arrived — please try again later.",
- "regenerateError": "Still refreshing — please try again in a moment.",
+ "regenerateRateLimited": "Refresh limit reached. Please try again later.",
+ "regenerateUnavailable": "No fresh analysis arrived. Please try again later.",
+ "regenerateError": "Still refreshing. Please try again in a moment.",
"regenerateSettling": "Your assessment is still being prepared",
"regenerateSettleFailed": "The refresh could not be completed. Please try again later.",
"deviceScore": {
- "learning": "Still learning your baseline — a few more nights will fill in the trend.",
+ "learning": "Still learning your baseline. A few more nights will fill in the trend.",
"average": "30-day average {value}",
"unitScore": "score",
"unitMinutes": "min",
@@ -4408,14 +4566,14 @@
},
"recovery": {
"title": "Recovery",
- "description": "How well your body has bounced back — the recovery, strain and load signals your wearable records overnight.",
+ "description": "How well your body has bounced back: the recovery, strain and load signals your wearable records overnight.",
"scoreCrossLink": "See the composite recovery score",
"emptyTitle": "No recovery data yet",
"empty": "No recovery data yet. Connect a WHOOP, Polar or Oura account and these scores fill in once your device syncs.",
"block": {
- "ansChargeExplainer": "How charged your autonomic nervous system is for the day ahead — a recharge reading drawn from your overnight heart and breathing signals.",
+ "ansChargeExplainer": "How charged your autonomic nervous system is for the day ahead, a recharge reading drawn from your overnight heart and breathing signals.",
"dayStrainExplainer": "The day's whole-body cardiovascular load on your device's strain scale. Read it against your recovery, not as a goal on its own.",
- "workoutStrainExplainer": "How demanding a single workout was on the same strain scale — the cardiovascular cost of that session.",
+ "workoutStrainExplainer": "How demanding a single workout was on the same strain scale: the cardiovascular cost of that session.",
"cardioLoadExplainer": "A training-load proxy that tracks the acute cardiovascular load you have built up. Compare it with your own recent days.",
"averageHeartRateExplainer": "Your average heart rate across the whole day or cycle, as reported by your wearable.",
"maxHeartRateExplainer": "Your peak heart rate across the whole day or cycle, as reported by your wearable.",
@@ -4428,7 +4586,7 @@
},
"resilience": {
"explainer": "How well your body is balancing daily strain with recovery, read from your overnight signals across recent days.",
- "learning": "Still learning your resilience — a few more days will settle the band.",
+ "learning": "Still learning your resilience. A few more days will settle the band.",
"band": {
"limited": "Limited",
"adequate": "Adequate",
@@ -4459,6 +4617,7 @@
"shiftUp": "{vital} has shifted up from your recent normal.",
"shiftDown": "{vital} has shifted down from your recent normal.",
"rangeContext": "Now {value}, usually {low}–{high}.",
+ "rangeContextTimeOfDay": "Now {value}, usually {low}–{high} at this time of day.",
"shiftContext": "Around {before} before, {after} since."
},
"breathingScreening": {
@@ -4472,7 +4631,7 @@
"nights": "{count} nights tracked",
"indexLine": "recent average index {value}",
"eventsFlagged": "{count} events flagged by your device",
- "disclaimer": "This is a screening signal, not a diagnosis — discuss with a clinician."
+ "disclaimer": "This is a screening signal, not a diagnosis. Discuss with a clinician."
},
"labsChanges": {
"sectionTitle": "Since your last panel",
@@ -4488,14 +4647,14 @@
"navWaistToHeight": "Waist-to-height",
"gripStrength": {
"title": "Grip strength",
- "description": "Maximal handgrip force — a robust proxy for whole-body strength. Below the sex-specific cut-off (men 27 kg, women 16 kg) flags probable sarcopenia; your own trend leads the read.",
+ "description": "Maximal handgrip force, a robust proxy for whole-body strength. Below the sex-specific cut-off (men 27 kg, women 16 kg) flags probable sarcopenia; your own trend leads the read.",
"chartTitle": "Grip strength",
"emptyState": {
"title": "No grip strength yet",
- "description": "Log a reading from a hand dynamometer — record the best of a few squeezes — and the trend will appear here.",
+ "description": "Log a reading from a hand dynamometer (record the best of a few squeezes) and the trend will appear here.",
"cta": "Log grip strength"
},
- "coachPrefill": "I haven't logged any grip strength yet — what does it tell me about my muscle health, and how do I measure and improve it?"
+ "coachPrefill": "I haven't logged any grip strength yet. What does it tell me about my muscle health, and how do I measure and improve it?"
},
"pain": {
"title": "Pain (0–10)",
@@ -4506,29 +4665,29 @@
"description": "Log how your pain feels right now on the 0–10 scale and the trend will appear here.",
"cta": "Log a pain score"
},
- "coachPrefill": "I haven't logged any pain scores yet — how should I track pain on the 0–10 scale, and what counts as a meaningful change?"
+ "coachPrefill": "I haven't logged any pain scores yet. How should I track pain on the 0–10 scale, and what counts as a meaningful change?"
},
"waist": {
"title": "Waist circumference",
- "description": "Waist girth in centimetres — a marker of central fat. The WHO increased-risk threshold is sex-specific (men 94 cm, women 80 cm); your own trend leads the read.",
+ "description": "Waist girth in centimetres, a marker of central fat. The WHO increased-risk threshold is sex-specific (men 94 cm, women 80 cm); your own trend leads the read.",
"chartTitle": "Waist circumference",
"emptyState": {
"title": "No waist measurements yet",
"description": "Measure your waist at the midpoint between the lowest rib and the hip bone, at the end of a normal breath, and log it here.",
"cta": "Log waist circumference"
},
- "coachPrefill": "I haven't logged any waist measurements yet — how do I measure my waist correctly, and what does the number mean for my health?"
+ "coachPrefill": "I haven't logged any waist measurements yet. How do I measure my waist correctly, and what does the number mean for my health?"
},
"waistToHeight": {
"title": "Waist-to-height ratio",
- "description": "Your waist divided by your height — the ancestry-robust single number. Keeping it under 0.5 (waist below half your height) is the simple target.",
+ "description": "Your waist divided by your height, the ancestry-robust single number. Keeping it under 0.5 (waist below half your height) is the simple target.",
"chartTitle": "Waist-to-height ratio",
"emptyState": {
"title": "No waist-to-height ratio yet",
"description": "Log a waist-to-height ratio and the trend will appear here. The rule of thumb: keep your waist under half your height.",
"cta": "Log waist-to-height ratio"
},
- "coachPrefill": "I haven't logged a waist-to-height ratio yet — why is it a useful number, and how do I keep it under 0.5?"
+ "coachPrefill": "I haven't logged a waist-to-height ratio yet. Why is it a useful number, and how do I keep it under 0.5?"
},
"ecg": {
"sectionTitle": "ECG recordings",
@@ -4569,9 +4728,9 @@
"emptyCta": "Connect a device"
},
"intradayPulse": {
- "title": "Your day's shape",
- "caption": "Your heart rate through the day, averaged in 10-minute steps.",
- "hourlyNote": "Showing this day's hourly average — recent days show the full 10-minute detail.",
+ "title": "Pulse through the day",
+ "caption": "Averaged in 10-minute steps.",
+ "hourlyNote": "Showing this day's hourly average; recent days show the full 10-minute detail.",
"empty": "Not enough heart-rate readings for this day to show its shape yet.",
"coverageOne": "Based on 1 reading across {hours}h.",
"coverageMany": "Based on {count} readings across {hours}h.",
@@ -4580,10 +4739,10 @@
"previousDay": "Previous day",
"nextDay": "Next day",
"tension": {
- "morning": "A sustained elevated-at-rest stretch this morning — possibly some tension.",
- "afternoon": "A sustained elevated-at-rest stretch this afternoon — possibly some tension.",
- "evening": "A sustained elevated-at-rest stretch this evening — possibly some tension.",
- "night": "A sustained elevated-at-rest stretch overnight — possibly some tension."
+ "morning": "A sustained elevated-at-rest stretch this morning, possibly some tension.",
+ "afternoon": "A sustained elevated-at-rest stretch this afternoon, possibly some tension.",
+ "evening": "A sustained elevated-at-rest stretch this evening, possibly some tension.",
+ "night": "A sustained elevated-at-rest stretch overnight, possibly some tension."
}
},
"navEcg": "ECG",
@@ -4594,6 +4753,19 @@
"settingsBody": "HealthLog works fully without AI. Connect a provider below if you want a daily briefing, short notes on your readings and the Coach.",
"action": "Set up AI",
"dismiss": "Not now"
+ },
+ "bmiHeight": {
+ "coachPrefill": "I haven’t set my height yet. Why does BMI matter, and what should I know before I configure it?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "I haven’t added any medications yet. What should I know before I start tracking medication compliance here?"
+ },
+ "scoreHistory": {
+ "healthTitle": "Health Score history",
+ "healthDescription": "Your Health Score day by day, as it was shown on each day.",
+ "emptyDescription": "No score was recorded in this range yet.",
+ "usualRange": "Shaded: your usual range, {lo} to {hi}.",
+ "seam": "The line breaks where what counts toward the score changed."
}
},
"thresholds": {
@@ -4623,14 +4795,15 @@
"metricBodyFat": "Body fat",
"metricSleep": "Sleep duration",
"metricSteps": "Daily steps",
- "metricGlucoseFasting": "Glucose — fasting",
- "metricGlucosePostprandial": "Glucose — post-meal",
- "metricGlucoseRandom": "Glucose — random",
- "metricGlucoseBedtime": "Glucose — bedtime",
+ "metricGlucoseFasting": "Glucose (fasting)",
+ "metricGlucosePostprandial": "Glucose (post-meal)",
+ "metricGlucoseRandom": "Glucose (random)",
+ "metricGlucoseBedtime": "Glucose (bedtime)",
"metricBodyWater": "Body water",
"metricBoneMass": "Bone mass",
"metricOxygenSaturation": "Oxygen saturation (SpO₂)",
- "unsetExplanation": "No override — using computed default."
+ "unsetExplanation": "No override, using computed default.",
+ "cardTitle": "Ranges per metric"
},
"onboarding": {
"errorGeneric": "Something went wrong while finishing setup. Please try again.",
@@ -4643,11 +4816,11 @@
"steps": {
"dashboardOverview": {
"title": "Your dashboard",
- "body": "The dashboard pulls every module together at a glance. Long-press a tile to reorder or hide it (Settings → Dashboard layout). Each tile is only as telling as the data behind it — the modules ahead fill them."
+ "body": "The dashboard pulls every module together at a glance. Long-press a tile to reorder or hide it (Settings → Dashboard layout). Each tile is only as telling as the data behind it, and the modules ahead fill them."
},
"quickAdd": {
"title": "Logged in seconds",
- "body": "Use “+ Add” to log any reading in seconds — weight, blood pressure, pulse, glucose. Hidden strength: backdate readings you forgot to enter. Everything you log here feeds trends, the health score, and the AI insights."
+ "body": "Use “+ Add” to log any reading in seconds: weight, blood pressure, pulse, glucose. Hidden strength: backdate readings you forgot to enter. Everything you log here feeds trends, the health score, and the AI insights."
},
"measurements": {
"title": "Readings & target ranges",
@@ -4659,7 +4832,7 @@
},
"labs": {
"title": "Lab values",
- "body": "Labs captures biomarkers with reference ranges — blood count, cholesterol, HbA1c. Strength: import and FHIR export. Out-of-range lab values resurface as evidence in the insights and the doctor report."
+ "body": "Labs captures biomarkers with reference ranges: blood count, cholesterol, HbA1c. Strength: import and FHIR export. Out-of-range lab values resurface as evidence in the insights and the doctor report."
},
"illness": {
"title": "Illness journal",
@@ -4675,31 +4848,31 @@
},
"mood": {
"title": "Mood",
- "body": "Mood captures how you feel plus tags in two seconds. Strength: your own tags. Mood trends correlate with sleep, medications, and cycle — the Coach spots the patterns."
+ "body": "Mood captures how you feel plus tags in two seconds. Strength: your own tags. Mood trends correlate with sleep, medications, and cycle, and the Coach spots the patterns."
},
"insights": {
"title": "Insights",
- "body": "Insights is the heart of it: evidence-based, dynamic analysis instead of generic tips — every statement points at your real data. Hidden: the layout is customizable. Insights pulls together everything the modules above provide."
+ "body": "Insights is the heart of it: evidence-based, dynamic analysis instead of generic tips. Every statement points at your real data. Hidden: the layout is customizable. Insights pulls together everything the modules above provide."
},
"coach": {
"title": "Coach",
- "body": "The Coach answers questions about your data in a dialogue, using your history, medications, and lab values as context. Pick your AI provider yourself (your own key or local). It recomputes nothing — it reads the same numbers as the dashboard."
+ "body": "The Coach answers questions about your data in a dialogue, using your history, medications, and lab values as context. Pick your AI provider yourself (your own key or local). It recomputes nothing; it reads the same numbers as the dashboard."
},
"integrations": {
"title": "Integrations",
- "body": "Integrations sync Withings, WHOOP, Polar, Oura, and Apple Health automatically. Weight, sleep, and recovery fill in without manual typing — and feed the insights and Coach in turn."
+ "body": "Integrations sync Withings, WHOOP, Polar, Oura, and Apple Health automatically. Weight, sleep, and recovery fill in without manual typing and feed the insights and Coach in turn."
},
"export": {
"title": "Doctor report",
- "body": "The doctor report bundles readings, medications, lab values, and checkups as PDF or FHIR for the appointment. This is where every module converges — a complete report only comes from well-kept data."
+ "body": "The doctor report bundles readings, medications, lab values, and checkups as PDF or FHIR for the appointment. This is where every module converges: a complete report only comes from well-kept data."
},
"achievements": {
"title": "Achievements",
- "body": "Achievements reward consistency — streaks and milestones. A small motivation loop that makes sticking with it easier."
+ "body": "Achievements reward consistency: streaks and milestones. A small motivation loop that makes sticking with it easier."
},
"wrapUp": {
"title": "That’s the tour",
- "body": "This walkthrough runs once — it won't reopen on its own. Everything it covered stays put, and you can turn features on or off any time under Settings → Modules."
+ "body": "This walkthrough runs once and won't reopen on its own. Everything it covered stays put, and you can turn features on or off any time under Settings → Modules."
}
}
},
@@ -4730,7 +4903,7 @@
"title": "You're all set",
"body": "Welcome aboard. Your dashboard is ready.",
"returnCta": "Open dashboard",
- "learning": "Some insights — glucose, sleep rhythm, blood-pressure trends, cycle — sharpen over the first week or two as your data builds up. Empty tiles now just mean we're still learning your baseline.",
+ "learning": "Some insights (glucose, sleep rhythm, blood-pressure trends, cycle) sharpen over the first week or two as your data builds up. Empty tiles now just mean we're still learning your baseline.",
"connectCta": "Connect a device",
"logCta": "Log your first measurement",
"importCta": "Import existing history",
@@ -4747,33 +4920,33 @@
"anamnesis": {
"title": "About your health (optional)",
"subtitle": "A little context so HealthLog reads your numbers more fairly.",
- "intro": "Share as much or as little as you like — every field is optional and you can edit it anytime in Settings.",
+ "intro": "Share as much or as little as you like. Every field is optional and you can edit it anytime in Settings.",
"conditionsLabel": "Ongoing conditions",
"conditionsPlaceholder": "e.g. high blood pressure, type 2 diabetes",
"conditionsHint": "Helps HealthLog frame your readings in context rather than flagging the expected.",
"allergiesLabel": "Allergies or intolerances",
"allergiesPlaceholder": "e.g. penicillin, lactose",
- "privacyNote": "Stored encrypted and used only to give you better context. We don't ask for your medications here — add those anytime from the Medications page."
+ "privacyNote": "Stored encrypted and used only to give you better context. We don't ask for your medications here. Add those anytime from the Medications page."
},
"ai": {
"panelTitle": "See what insights look like",
- "panelIntro": "Coach, the daily briefing and the per-metric reads turn your own numbers into plain language. Here's the shape of it — before you connect anything.",
+ "panelIntro": "Coach, the daily briefing and the per-metric reads turn your own numbers into plain language. Here's the shape of it, before you connect anything.",
"showSample": "See a sample briefing",
"sampleTitle": "Daily Briefing",
"sampleTag": "Example",
"sampleAriaLabel": "Example daily briefing",
- "sampleBody": "Weight down 1.2 kg over two weeks. Resting pulse steady at 58 bpm. Blood pressure in range on 6 of 7 days — the one high reading followed a late, salty dinner.",
- "sampleCaption": "Example only — not your own data. This is what a briefing reads like once insights are on.",
+ "sampleBody": "Weight down 1.2 kg over two weeks. Resting pulse steady at 58 bpm. Blood pressure in range on 6 of 7 days; the one high reading followed a late, salty dinner.",
+ "sampleCaption": "Example only, not your own data. This is what a briefing reads like once insights are on.",
"ladderTitle": "Three ways to turn it on",
- "ladderLocalTitle": "Local model — AI requests stay on your network",
- "ladderLocalBody": "Point HealthLog at Ollama or LM Studio on your own machine. Free and fully private — the calm default.",
+ "ladderLocalTitle": "Local model: AI requests stay on your network",
+ "ladderLocalBody": "Point HealthLog at Ollama or LM Studio on your own machine. Free and fully private, the calm default.",
"ladderByokTitle": "Bring your own key",
"ladderByokBody": "Use your own OpenAI or Anthropic key. It runs under your account and billing, and the API tier keeps your data out of training by default.",
"ladderOauthTitle": "Sign in with a subscription",
- "ladderOauthBody": "Connect a ChatGPT account — no per-token cost, but the consumer tier's terms may use content for training, so it's a deliberate choice, never the default for your health data.",
- "keylessLine": "HealthLog is fully useful without AI. Your tiles, charts, trends and exports all work — AI only adds the plain-language read on top.",
+ "ladderOauthBody": "Connect a ChatGPT account. No per-token cost, but the consumer tier's terms may use content for training, so it's a deliberate choice, never the default for your health data.",
+ "keylessLine": "HealthLog is fully useful without AI. Your tiles, charts, trends and exports all work. AI only adds the plain-language read on top.",
"setupCta": "Set it up (about a minute)",
- "sharedKeyNote": "This instance already provides a shared AI provider, so insights work for you right now — no setup needed.",
+ "sharedKeyNote": "This instance already provides a shared AI provider, so insights work for you right now, no setup needed.",
"offer": {
"title": "This instance can write your summary in plain language.",
"body": "The operator provides a provider that last worked within the past 24 hours. One tap turns the plain-language read on; your data then goes to that one provider and to nobody else.",
@@ -4990,7 +5163,7 @@
"profileDescription": "Height, date of birth and gender unlock personalised targets.",
"profileCta": "Open profile",
"measurementTitle": "Log your first measurement",
- "measurementDescription": "One reading unlocks the tile — the trend itself draws from your 5th reading.",
+ "measurementDescription": "One reading unlocks the tile; the trend itself draws from your 5th reading.",
"measurementCta": "Add measurement",
"medicationTitle": "Add a medication",
"medicationDescription": "Track adherence and get reminders right when you need them.",
@@ -4999,7 +5172,7 @@
"dataSourceDescription": "Auto-sync weight, BP, sleep and more from Withings, WHOOP, Oura, Polar, Nightscout, Fitbit or Apple Health.",
"dataSourceCta": "Connect",
"notificationsTitle": "Configure notifications",
- "notificationsDescription": "Choose how HealthLog should remind you — Telegram, ntfy or Web Push.",
+ "notificationsDescription": "Choose how HealthLog should remind you: Telegram, ntfy or Web Push.",
"notificationsCta": "Open notifications",
"insightsTitle": "Turn on insights (optional)",
"insightsDescription": "Add a local model or your own AI key to unlock the daily briefing and Coach.",
@@ -5088,11 +5261,6 @@
"brief": "Brief",
"default": "Default",
"detailed": "Detailed",
- "defaultWindow": "Default window",
- "last7days": "Last 7 days",
- "last30days": "Last 30 days",
- "last90days": "Last 90 days",
- "allTime": "All time",
"excludedMetrics": "Excluded metrics",
"dataClusters": "Data clusters",
"customClusters": "Use a custom cluster selection",
@@ -5152,12 +5320,10 @@
"sections": {
"account": {
"title": "Account",
- "description": "Profile, password, passkeys, and your onboarding tour.",
"subtitle": "Your profile, identity, and sign-in details."
},
"integrations": {
"title": "Integrations",
- "description": "Connected services, delivery channels, and source priority in one place.",
"subtitle": "Connect and manage your data sources and devices.",
"connectionsHeading": "Connections",
"channelsHeading": "Delivery channels",
@@ -5166,16 +5332,15 @@
},
"notifications": {
"title": "Notifications",
- "description": "Every reminder in one place — what gets reminded and when, and where it's delivered.",
"subtitle": "Choose which reminders you receive and where they're delivered.",
"remindersHeading": "Reminders",
"matrixLink": "Per-channel notifications",
- "channelsLink": "Delivery channels"
+ "channelsLink": "Delivery channels",
+ "displayHeading": "On your phone"
},
"layout": {
- "title": "Appearance",
- "description": "Choose how the dashboard, insights, and your tracking modules look and are arranged.",
- "backToHub": "Back to Appearance",
+ "title": "Layout",
+ "backToHub": "Back to Layout",
"dashboard": {
"title": "Dashboard",
"description": "Tile layout and order."
@@ -5184,7 +5349,7 @@
"title": "Insights",
"description": "Overview sections and navigation-pill order."
},
- "subtitle": "Personalize how your dashboard, insights, and tracking modules look.",
+ "subtitle": "Choose what your dashboard, insights and tracking modules show, and in which order.",
"medications": {
"title": "Medications",
"description": "List view and manual order."
@@ -5204,6 +5369,10 @@
"mood": {
"title": "Mood tags",
"description": "Groups, tags, and archived tags for the mood picker."
+ },
+ "documents": {
+ "title": "Documents",
+ "description": "View and month arrangement."
}
},
"dashboard": {
@@ -5211,15 +5380,13 @@
},
"thresholds": {
"title": "Targets",
- "description": "Per metric: your own target range for healthy values.",
"subtitle": "Set your personal target ranges per metric."
},
"sources": {
"title": "Source priority",
- "description": "Pick which ingest source wins when more than one records the same metric for the same day.",
"integrationsHint": "Connect or disconnect data sources on the Integrations page.",
"integrationsHintLink": "Open integrations",
- "cardTitle": "Source priority by metric",
+ "cardTitle": "Ranking per metric",
"help": "When several sources deliver the same value on the same day, the top source counts as canonical.",
"resetDefaults": "Reset to defaults",
"resetDefaultsTitle": "Reset the source order?",
@@ -5232,7 +5399,7 @@
"resetSuccess": "Source priority reset to defaults",
"perMetricToggle": "Per-metric overrides ({count})",
"perMetricHelp": "Metrics you have customised away from the default ladder. Use this list to roll any single metric back to its constant default without touching the rest.",
- "perMetricEmpty": "No per-metric overrides set — every metric uses the global ladder above.",
+ "perMetricEmpty": "No per-metric overrides set. Every metric uses the global ladder above.",
"resetMetric": "Reset",
"deviceTypeToggle": "Device-type priority ({count})",
"deviceTypeHelp": "Within a source, which device wins. When an Apple Watch and an iPhone both record steps, the higher-ranked device-type counts as the canonical reading.",
@@ -5257,7 +5424,8 @@
"respiratoryRate": "Respiratory rate",
"recovery": "Recovery",
"stress": "Stress",
- "sleepDebt": "Sleep debt"
+ "sleepDebt": "Sleep debt",
+ "hrvRmssd": "HRV (RMSSD)"
},
"sourceLabels": {
"WITHINGS": "Withings",
@@ -5274,7 +5442,8 @@
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava",
- "EXTERNAL": "External device"
+ "EXTERNAL": "External device",
+ "HEALTH_CONNECT": "Health Connect"
},
"deviceLabels": {
"watch": "Watch",
@@ -5294,28 +5463,25 @@
"daysWithData": "{count} of {days} days",
"moduleHint": "Syncing is controlled by the Nutrient intake module.",
"moduleHintLink": "Manage modules",
- "detail": "Read-only — the source app owns the numbers."
+ "detail": "Read-only: the source app owns the numbers."
},
- "helpDetail": "The others stay stored for the audit trail."
+ "helpDetail": "The others stay stored for the audit trail. Only sources you have connected or hold data from are listed.",
+ "noneInUse": "No source to rank yet. Connect one under Integrations or record a value."
},
"ai": {
"title": "AI provider",
- "description": "Provider, model, and API key for the insights — plus your “about me” context.",
"subtitle": "Choose your AI provider and bring your own key."
},
"api": {
- "title": "API & Tokens",
- "description": "Bearer tokens for your own scripts and apps — log measurements or medication intake from anywhere.",
+ "title": "API & tokens",
"subtitle": "Manage API endpoints and access tokens."
},
"advanced": {
"title": "Advanced",
- "description": "The way out: erase every health record for good, or close the account entirely.",
"subtitle": "Data reset and account deletion."
},
"export": {
- "title": "Export & Import",
- "description": "Download your data as CSV or a full JSON backup — or import from Apple Health, JSON, or CSV. The doctor report now lives under Health Record.",
+ "title": "Export & import",
"otherOptionsHeading": "Other export options",
"downloadFailed": "Couldn't download the export. Please try again.",
"actions": {
@@ -5342,12 +5508,12 @@
"fullBackup": {
"title": "Full backup",
"description": "Your whole record in one JSON file.",
- "scopeNote": "Document metadata and the AI-generated summary, if one exists, are included as plain text; original document files and workout GPS/sample data are not — download those separately from the vault or the workout export.",
+ "scopeNote": "Document metadata and the AI-generated summary, if one exists, are included as plain text; original document files and workout GPS/sample data are not. Download those separately from the vault or the workout export.",
"encryptToggle": "Encrypt with a passphrase",
"passphrasePlaceholder": "Passphrase (min. 12 characters)",
"passphraseHelp": "Derives the archive key with Argon2id and seals it with AES-256-GCM. Save the file with a .hlx extension.",
- "noRecoveryWarning": "There is no recovery — if you lose this passphrase, the archive cannot be opened. Keep it somewhere safe.",
- "detail": "Same shape as the weekly auto-backup. Some record types are not carried yet — the file's manifest names what is in it.",
+ "noRecoveryWarning": "There is no recovery. If you lose this passphrase, the archive cannot be opened. Keep it somewhere safe.",
+ "detail": "Same shape as the weekly auto-backup. Some record types are not carried yet; the file's manifest names what is in it.",
"contents": "Your measurements, medications, mood, cycle, labs, illness episodes, allergies, family history, nutrients, workouts, health profile and custom metrics in one JSON file."
}
},
@@ -5362,7 +5528,7 @@
"dropHint": "or click to choose a file",
"fileInputLabel": "Choose an Apple Health export.zip file",
"choose": "Choose file",
- "idempotencyNote": "Re-uploading the same archive merges — it won't create duplicates. Large archives are processed in the background.",
+ "idempotencyNote": "Re-uploading the same archive merges it and creates no duplicates. Large archives are processed in the background.",
"uploading": "Uploading…",
"uploadFailed": "Upload failed. Please try again.",
"rateLimited": "Too many uploads. Please wait a moment and try again.",
@@ -5371,8 +5537,11 @@
"failureInterrupted": "The import was interrupted before it finished, usually by a server restart or an out-of-memory stop. Your data is safe; upload the export again to retry.",
"failureInsufficientMemory": "The server's memory limit is too low for an archive this size. Ask whoever runs the server to raise the Node.js heap limit (NODE_OPTIONS=--max-old-space-size), then upload the export again.",
"rowsImported": "{count} rows imported so far",
- "doneSummary": "Done — {imported} of {read} records imported, {skipped} clinical records skipped.",
+ "doneSummary": "Done: {imported} of {read} records imported, {skipped} clinical records skipped.",
"doneNothing": "Nothing was imported from that archive: 0 of {read} records landed.",
+ "writtenByHealthLogOne": "{count} record that HealthLog wrote into Apple Health itself was left out, because it is already in your data.",
+ "writtenByHealthLogOther": "{count} records that HealthLog wrote into Apple Health itself were left out, because they are already in your data.",
+ "writtenByHealthLogFew": "{count} records that HealthLog wrote into Apple Health itself were left out, because they are already in your data.",
"ecgResult": "ECGs: {discovered} discovered, {imported} imported, {updated} updated, {skipped} skipped, {failed} failed. ECG failures do not discard supported records imported from export.xml.",
"estimateWarning": "Cumulative totals for {count} day(s) were estimated from the largest export source. Native Apple Health statistics remain authoritative.",
"phase": {
@@ -5381,12 +5550,12 @@
"parsing": "Reading the export…",
"upserting": "Importing records…"
},
- "skipUnsupported": "{count} samples skipped — data types HealthLog doesn't import.",
+ "skipUnsupported": "{count} samples skipped: data types HealthLog doesn't import.",
"skipRefused": "{count} samples couldn't be written (unreadable, out of range, or the save failed).",
"skipBreakdown": "Show breakdown ({count} types)",
"skipBreakdownMore": "+ {count} more types",
"cycleSummary": "{samples} cycle samples merged into {days} day logs.",
- "cycleModuleOff": "{count} cycle samples skipped — cycle tracking is off for this account.",
+ "cycleModuleOff": "{count} cycle samples skipped because cycle tracking is off for this account.",
"cycleDaysFailed": "{count} cycle days couldn't be written: {reason}"
},
"json": {
@@ -5471,8 +5640,8 @@
"failed": "The import failed. Nothing was written.",
"columnsHeading": "What each column does",
"jsonOnly": "JSON only",
- "previewLine": "Rows read: {rows} · would import: {queued} · refused: {refused}",
- "submittedLine": "Rows read: {rows} · queued for import: {queued}",
+ "previewLine": "{rows} rows read, {queued} would be imported, {refused} refused",
+ "submittedLine": "{rows} rows read, {queued} queued for import",
"unmatched": "No medication on record matches: {names}",
"ambiguous": "Matches more than one medication: {names}",
"mirrored": "Mirrored from another app, so no dose can be added: {names}",
@@ -5508,18 +5677,59 @@
"tooManyRows": "That file holds more rows than one import can take. Split it and import the parts."
},
"detail": "One file covers every medication in it."
+ },
+ "healthConnect": {
+ "title": "Health Connect export",
+ "description": "Import the ZIP file the Health Connect app on Android writes when you export your data.",
+ "choose": "Choose ZIP file",
+ "uploading": "Uploading…",
+ "uploadFailed": "The upload failed. Please try again.",
+ "tooLarge": "The file is larger than this server accepts.",
+ "doneSummary": "{count} values imported.",
+ "failed": "The import failed: {reason}",
+ "phase": {
+ "queued": "Waiting to start",
+ "unpacking": "Unpacking the file",
+ "parsing": "Reading the records",
+ "upserting": "Saving the values",
+ "done": "Finished",
+ "failed": "Failed"
+ },
+ "steps": {
+ "export": "On your Android phone, open Health Connect, go to Manage data, then Export and import, and start an export.",
+ "cloud": "Health Connect only saves the export to a cloud storage app such as Google Drive, Dropbox or Nextcloud, never to the phone itself. Pick one there.",
+ "upload": "Download the ZIP file from that cloud to this device and upload it here."
+ },
+ "dropLabel": "Drop the Health Connect ZIP here",
+ "dropHint": "or click to choose it. Up to 1 GB.",
+ "fileInputLabel": "Health Connect export ZIP file",
+ "privacyNote": "The file is deleted from the server as soon as the import ends. Records from apps you already connect directly, such as Withings or Fitbit, are left out so nothing arrives twice. Uploading the same file again changes nothing.",
+ "rateLimited": "Too many uploads in a short time. Wait a minute and try again.",
+ "busy": "An import is already running for your account. Wait until it has finished.",
+ "rowsSoFar": "{count} values saved so far",
+ "doneNothing": "The import finished, but there was nothing new to add.",
+ "workouts": "Workouts",
+ "leftOutConnected": "{count} records from apps you already connect directly were left out.",
+ "nutrientsModuleOff": "Water and nutrient totals were not imported because the Nutrients module is switched off.",
+ "cycleModuleOff": "Period data was not imported because cycle tracking is switched off.",
+ "failure": {
+ "unsupportedVersion": "This Health Connect export is too old to read. Update Health Connect on the phone and export again.",
+ "notHealthConnect": "This file is not a Health Connect export. Choose the ZIP file Health Connect saved.",
+ "unsafeSchema": "This file does not have the structure of a Health Connect export and was not read.",
+ "tooLarge": "The export is larger than this server accepts.",
+ "stagingMissing": "The uploaded file was no longer on the server. Upload it again.",
+ "interrupted": "The import was interrupted by a server restart. Upload the file again."
+ }
}
},
"subtitle": "Export your data or import from a backup."
},
"about": {
"title": "About this app",
- "description": "Version, license, links.",
"subtitle": "Version, license, and project links."
},
"sharing": {
"title": "Sharing",
- "description": "Create a read-only link to share your health record with a clinician.",
"subtitle": "Share read-only access to your data."
},
"insights": {
@@ -5546,7 +5756,7 @@
"error": "Couldn't save. Please try again.",
"toggleable": {
"title": "What you track",
- "description": "Disabled modules drop out of the navigation, tiles, and insights — your data stays untouched."
+ "description": "Disabled modules drop out of the navigation, tiles and insights, while your data stays untouched."
},
"manageIn": "Manage in {section}",
"operatorDisabled": "Disabled server-wide",
@@ -5554,12 +5764,10 @@
},
"gesundheitsakte": {
"title": "Health record",
- "description": "Export your full health record as PDF, FHIR R4, or a combined ZIP package.",
"subtitle": "Export a complete, clinician-ready health record."
},
"coach": {
"title": "Coach",
- "description": "Enable the Coach, tone and scope, stored memories.",
"subtitle": "Tune the Coach, its proactive nudges, and its memory."
},
"security": {
@@ -5567,18 +5775,16 @@
"subtitle": "Two-factor authentication, recovery codes, and passkeys."
},
"privacy": {
- "title": "Data & Privacy",
- "subtitle": "Your data, retention, encryption, and account security in one place.",
- "description": "Export, deletion, retention, encryption status, sessions, and security activity."
+ "title": "Data & privacy",
+ "subtitle": "Your data, retention, encryption, and account security in one place."
},
"mcp": {
- "title": "MCP Connector",
+ "title": "MCP connector",
"subtitle": "Connect an external assistant to your own health data."
},
"anamnesis": {
"title": "Medical history",
- "subtitle": "Manage your allergies and family history.",
- "description": "Structured allergies and family history records."
+ "subtitle": "Your medical background, lifestyle, emergency details and what the AI may read."
},
"environment": {
"title": "Location & weather",
@@ -5600,19 +5806,20 @@
"storedDays": "{count} days of weather stored.",
"home": {
"title": "Home location",
- "description": "A coarse, city-level location — never a precise track. Weather and daylight are fetched for this place on days no location period applies, from the date you set it onward.",
+ "description": "A coarse, city-level location, never a precise track. Weather and daylight are fetched for this place on days no location period applies, from the date you set it onward.",
"current": "Current: {label}",
"none": "No home location set yet.",
"since": "Weather tracked from {date} onward. Earlier days stay empty until you add a location period."
},
"travel": {
"title": "Location periods",
- "description": "Add a place and a date range — a trip, or a stretch of earlier history. The period overrides your home for the days it covers, so weather matches where you actually were. This is how you correct the past.",
+ "description": "Add a place and a date range, such as a trip or a stretch of earlier history. The period overrides your home for the days it covers, so weather matches where you actually were. This is how you correct the past.",
"none": "No location periods yet.",
"add": "Add {label}",
"remove": "Remove location period",
"removeConfirmTitle": "Remove this trip?",
- "removeConfirmBody": "Readings taken during it stay in your record; only the trip entry goes."
+ "removeConfirmBody": "Readings taken during it stay in your record; only the trip entry goes.",
+ "range": "{start} to {end}"
},
"backfill": {
"title": "Backfill past days",
@@ -5622,10 +5829,24 @@
"noResults": "No matching places found.",
"fetchFailedOne": "The last background weather update failed ({when}).",
"fetchFailedFew": "The last {count} background weather updates failed, most recently {when}.",
- "fetchFailedOther": "The last {count} background weather updates failed, most recently {when}."
+ "fetchFailedOther": "The last {count} background weather updates failed, most recently {when}.",
+ "airQuality": {
+ "title": "Air quality and pollen",
+ "description": "Daily air quality, pollen and UV for your location, from the CAMS model via Open-Meteo.",
+ "toggle": "Include air quality and pollen",
+ "operatorDisabled": "Air quality is turned off on this server.",
+ "coverage": "{count} days with air-quality data",
+ "pollenEuropeOnly": "Pollen data covers Europe only.",
+ "historyProgress": "Backfilling air quality: {done} of {total} days",
+ "historyComplete": "Air quality is filled in for every earlier day with entries that the source covers ({total} days). Older days stay empty.",
+ "attribution": "Data sources and licences",
+ "saveError": "The setting could not be saved.",
+ "latest": "Newest day ({date})",
+ "modelNote": "Modelled outdoor values for your area, not your personal exposure."
+ }
},
"score": {
- "title": "Health Score",
+ "title": "Health score",
"subtitle": "Choose which pillars count toward your score.",
"card": {
"title": "What counts toward your score",
@@ -5655,7 +5876,7 @@
"omitted": "{count} pillars are not listed because their tracking is switched off. Turn a module back on and its pillar comes back.",
"omittedLink": "Open modules",
"refusal": {
- "empty": "A health score needs at least one area of health to score. Keep one selected — the score will say which areas it is built from."
+ "empty": "A health score needs at least one area of health to score. Keep one selected; the score will say which areas it is built from."
},
"notice": {
"upgrade": {
@@ -5749,7 +5970,6 @@
},
"changePassword": "Change password",
"changePasswordDescription": "Replace your current password with a new one.",
- "passwordReset": "Password reset",
"currentPassword": "Current password",
"newPassword": "New password",
"confirmNewPassword": "Confirm new password",
@@ -5794,9 +6014,9 @@
"botToken": "Bot Token",
"chatId": "Chat ID",
"enableNotifications": "Enable notifications",
- "telegramStep1": "1. Create a bot via @BotFather in Telegram and copy the token.",
- "telegramStep2": "2. Send /start to your bot to activate the chat.",
- "telegramStep3": "3. Find your Chat ID via @userinfobot or the Bot API.",
+ "telegramStep1": "Create a bot via @BotFather in Telegram and copy the token.",
+ "telegramStep2": "Send /start to your bot to activate the chat.",
+ "telegramStep3": "Find your Chat ID via @userinfobot or the Bot API.",
"ntfy": "ntfy",
"ntfyDescription": "Notifications via ntfy (self-hosted or ntfy.sh).",
"ntfyEnable": "Enable ntfy",
@@ -5805,7 +6025,7 @@
"ntfyAuthToken": "Auth Token (optional)",
"ntfyAuthTokenHint": "Only needed for private topics with access control.",
"webhook": "Webhook",
- "webhookDescription": "POST notifications to any URL — Gotify, Discord, Slack, Matrix, or Home Assistant.",
+ "webhookDescription": "POST notifications to any URL: Discord, Slack, Gotify, Home Assistant, n8n, or any relay that takes JSON.",
"webhookEnable": "Enable webhook",
"webhookUrl": "Webhook URL",
"webhookUrlHint": "The full URL to POST to. An address on a private network needs approval from the server operator.",
@@ -5813,6 +6033,7 @@
"webhookHeaderValue": "Header value (optional)",
"webhookFormat": "Payload format",
"webhookFormatGeneric": "Generic JSON",
+ "webhookFormatGenericHint": "Paste a Discord or Slack webhook URL as it is. Home Assistant and n8n read the fields title, message, eventType and priority.",
"webhookFormatGotify": "Gotify",
"webhookFormatGotifyHint": "Use the URL ending in /message and put the application token in the header X-Gotify-Key.",
"email": "Email",
@@ -5829,7 +6050,6 @@
"webPushSubscribed": "Push notifications enabled!",
"webPushUnsubscribed": "Push notifications disabled.",
"webPushSubscribeFailed": "Activation failed",
- "kiInsightsDescription": "Optional: Save your OpenAI key for daily, automatic evaluations in Insights.",
"codexConnected": "ChatGPT connected successfully! Insights are now active.",
"codexDisconnected": "ChatGPT connection removed.",
"codexConnectionFailed": "ChatGPT connection failed. Please try again.",
@@ -5839,7 +6059,7 @@
"rawDataWarning": "Detailed history is on: every analysis also sends daily averages for the last 90 days, weekly averages for the year before, and monthly averages back to 5 years.",
"regenerateInsights": "Regenerate reports",
"regenerateSuccess": "Reports regenerated successfully",
- "regenerateRateLimit": "Please wait — you've hit the hourly limit for analyses.",
+ "regenerateRateLimit": "Please wait: you've hit the hourly limit for analyses.",
"lastGeneratedAt": "Last generated",
"ai": {
"chatgptConnectedBadge": "ChatGPT connected",
@@ -5849,11 +6069,11 @@
"deviceCodeHeading": "Finish connecting on chatgpt.com",
"deviceCodeStep1": "Open this link on any device:",
"deviceCodeStep2": "Enter this one-time code:",
- "deviceCodeStep3": "Approve the connection — this page updates automatically.",
+ "deviceCodeStep3": "Approve the connection. This page updates automatically.",
"deviceCodeCopy": "copy",
"deviceCodeWaiting": "Waiting for approval…",
"deviceCodeCancel": "Cancel",
- "oauthNotConfigured": "ChatGPT OAuth is not configured on this instance — use your own API key below instead.",
+ "oauthNotConfigured": "ChatGPT OAuth is not configured on this instance. Use your own API key below instead.",
"modelLabel": "Model",
"modelOptionDefault": "— Default —",
"modelOptionCustom": "Custom…",
@@ -5861,7 +6081,7 @@
"anthropicKeyLabel": "Anthropic API key",
"baseUrlLabel": "Base URL",
"localKeyLabel": "API key (optional)",
- "localDescription": "Any OpenAI-compatible endpoint works here — a local model (Ollama, LM Studio) as well as a gateway such as LiteLLM, OpenRouter or vLLM. The base URL ends with /v1; the API key is optional.",
+ "localDescription": "Any OpenAI-compatible endpoint works here: a local model (Ollama, LM Studio) as well as a gateway such as LiteLLM, OpenRouter or vLLM. The base URL ends with /v1; the API key is optional.",
"savedPreview": "(saved {preview})",
"savedShort": "(saved)",
"saveCta": "Save",
@@ -5877,7 +6097,7 @@
"admin-openai": "Admin OpenAI"
},
"title": "Fallback chain",
- "description": "If the primary provider fails, HealthLog walks the chain in order. Drag the rows to reorder, toggle the switch to disable a provider without removing it.",
+ "description": "If the active provider fails, HealthLog tries the next enabled one in this order.",
"moveUp": "Move up",
"moveDown": "Move down",
"removeFromChain": "Remove from chain",
@@ -5890,11 +6110,8 @@
"resetConfirmTitle": "Reset chain to defaults?",
"resetConfirmBody": "The chain reverts to Codex → OpenAI → Anthropic → Local → Admin OpenAI. Your saved credentials are not touched."
},
- "providerCardTitle": "AI provider",
"activeProviderHeading": "Active provider",
- "activeProviderBody": "Pick the provider you want to use first. The form below configures only the selected provider; the fallback chain at the bottom decides what happens if it fails.",
"activeProviderLabel": "Primary provider",
- "providerConfigTitle": "Provider configuration",
"providerSelect": {
"codex": "ChatGPT account (Codex)",
"openai": "OpenAI (your API key)",
@@ -5904,7 +6121,7 @@
"admin-openai": "Admin-provided OpenAI"
},
"compat": {
- "description": "A first-class provider for any endpoint that speaks the OpenAI /v1/chat/completions API: LiteLLM, OpenRouter, vLLM or another gateway. The base URL ends with /v1, the API key is optional, and the model is whatever your gateway routes. Your OpenAI key never reaches this endpoint — this provider has its own.",
+ "description": "A first-class provider for any endpoint that speaks the OpenAI /v1/chat/completions API: LiteLLM, OpenRouter, vLLM or another gateway. The base URL ends with /v1, the API key is optional, and the model is whatever your gateway routes. Your OpenAI key never reaches this endpoint; this provider has its own.",
"keyLabel": "API key (optional)",
"modelHint": "The model name your gateway expects. Left blank, the model saved for your other providers is used; with neither set, this provider is skipped."
},
@@ -5940,25 +6157,24 @@
"notConfigured": "The operator has not configured a shared OpenAI key on this instance."
},
"testProvider": "Test active provider",
- "testSuccess": "OK — {provider} ({model})",
+ "testSuccess": "OK: {provider} ({model})",
"testFailedShort": "Test failed: {message}",
- "testUnexpectedResponse": "AI provider connection failed — unexpected response from the server.",
- "testReasonCredentials": "Provider rejected the credentials — re-authenticate in AI settings.",
- "testReasonRateLimited": "Provider rate-limited the request — try again shortly.",
+ "testUnexpectedResponse": "AI provider connection failed: unexpected response from the server.",
+ "testReasonCredentials": "Provider rejected the credentials. Re-authenticate in AI settings.",
+ "testReasonRateLimited": "Provider rate-limited the request. Try again shortly.",
"testReasonServerError": "The AI provider returned a server error.",
- "testReasonBadRequest": "The provider rejected the request (HTTP {status}) — the endpoint answered, so this is a request-shape or model-name problem, not connectivity.",
+ "testReasonBadRequest": "The provider rejected the request (HTTP {status}). The endpoint answered, so this is a request-shape or model-name problem, not connectivity.",
"testReasonUnreachable": "Could not reach the AI provider.",
"coachMemory": {
"title": "What the Coach remembers",
"description": "Durable facts you have told the Coach.",
- "empty": "The Coach has not stored any facts yet. As you chat, it remembers durable details you share — your goals, conditions, preferences — and you can clear them here at any time.",
+ "empty": "The Coach has not stored any facts yet. As you chat, it remembers durable details you share (your goals, conditions, preferences), and you can clear them here at any time.",
"learnedPrefix": "learned {when}",
"categoryPreference": "Preferences",
"categoryCondition": "Conditions",
"categoryGoal": "Goals",
"categoryConstraint": "Constraints",
"categoryContext": "Context",
- "forget": "Forget",
"forgetAria": "Forget this fact",
"forgetTitle": "Forget this fact?",
"forgetBody": "The Coach stops using it from the next message on. There is no way to bring a forgotten fact back.",
@@ -5968,13 +6184,14 @@
"forgetAllConfirmBody": "This removes every stored fact. The Coach will start fresh the next time you chat. This cannot be undone.",
"forgetAllConfirmAction": "Forget everything",
"cancel": "Cancel",
- "summaryNote": "The Coach may also keep a short rolling summary of each conversation. There is no per-item control for it — deleting a conversation removes its summary.",
+ "summaryNote": "The Coach may also keep a short rolling summary of each conversation. There is no per-item control for it; deleting a conversation removes its summary.",
"loadError": "Could not load what the Coach remembers.",
"forgotToast": "Fact forgotten.",
"forgotError": "Could not forget that fact. Please try again.",
"clearedToast": "Forgot {count} stored facts.",
"clearError": "Could not clear the stored facts. Please try again.",
- "detail": "It uses these to keep its replies relevant. Remove anything you would rather it forgot."
+ "detail": "It uses these to keep its replies relevant. Remove anything you would rather it forgot.",
+ "categoryMedication": "Medications"
},
"coachReminders": {
"title": "Your reminders",
@@ -5988,7 +6205,7 @@
"markDone": "Done",
"dismiss": "Dismiss",
"deleteAria": "Delete this reminder",
- "note": "Reminders are stored encrypted. Delete one any time — it is removed from what the Coach can see.",
+ "note": "Reminders are stored encrypted. Delete one any time and it is removed from what the Coach can see.",
"status": {
"proposed": "Suggested",
"active": "Saved",
@@ -6033,21 +6250,21 @@
"responseTimeoutInvalid": "Enter a number between 10 and 600 seconds.",
"autoRead": {
"title": "Read documents automatically with AI",
- "subLabel": "Documents are read and indexed by your configured AI provider without a per-document tap.",
- "honesty": "When on, each document you upload is sent to your configured AI provider to be read, described, and indexed for search — with no per-document confirmation. Turning it on also reaches backwards: documents you have already uploaded that carry no summary yet are sent too, up to 200 each time you switch it on. This happens on the server and the document's contents leave this machine to that provider. If your configured provider is a subscription connection (a signed-in AI account rather than an API key), that provider's own data settings apply — subscription providers may use the content to improve their models, and no data-processing agreement covers it. Documents read by a self-hosted local model never leave your server. Off by default; turn it on only if you accept this trade for your documents.",
- "confirm": "I understand — turn on",
+ "subLabel": "Your AI provider reads and indexes each new document without a tap per document.",
+ "honesty": "When on, each document you upload is sent to your configured AI provider to be read, described, and indexed for search, with no per-document confirmation. Turning it on also reaches backwards: documents you have already uploaded that carry no summary yet are sent too, up to 200 each time you switch it on. This happens on the server and the document's contents leave this machine to that provider. If your configured provider is a subscription connection (a signed-in AI account rather than an API key), that provider's own data settings apply: subscription providers may use the content to improve their models, and no data-processing agreement covers it. Documents read by a self-hosted local model never leave your server. Off by default; turn it on only if you accept this trade for your documents.",
+ "confirm": "I understand, turn on",
"cancel": "Cancel"
},
"centralCodex": {
"title": "Use the server's shared AI access",
- "subLabel": "Route AI requests through the connection the operator set up for this server, alongside your own providers as a fallback.",
- "honesty": "The shared access is a single signed-in AI account the operator connected for everyone on this server — not your own key. Requests you make on it are trained on by the provider by default and no data-processing agreement covers them, the account's hourly and weekly limits are shared with every other user who opted in, and your own configured providers are still tried first. Turn it on only if you accept this trade.",
- "confirm": "I understand — use connection",
+ "subLabel": "Send AI requests through the server's shared connection, with your own providers as the fallback.",
+ "honesty": "The shared access is a single signed-in AI account the operator connected for everyone on this server, not your own key. Requests you make on it are trained on by the provider by default and no data-processing agreement covers them, the account's hourly and weekly limits are shared with every other user who opted in, and your own configured providers are still tried first. Turn it on only if you accept this trade.",
+ "confirm": "I understand, use connection",
"cancel": "Cancel"
},
"consent": {
"title": "AI consent",
- "activeSince": "Given on {date}. Your health data may be sent to the configured provider for the AI features.",
+ "activeSince": "Given on {date}: your health data may be sent to the configured provider for the AI features.",
"withdrawn": "Withdrawn. Unless another consent covers them, the Coach and Insights no longer use the AI access this server provides, and no online model reads your documents. On your own key or a local model, the Coach and Insights keep working.",
"withdraw": "Withdraw consent",
"confirmBody": "Unless another consent you gave still covers them, these stop straight away: the Coach and Insights where they use the AI access this server provides, and reading documents with an online model. On your own key or a local model, the Coach and Insights keep working. Unless another consent still covers Insights, the notes a model wrote for you are deleted: status notes, the daily briefing, model-written period summaries, arrival lines and workout notes. Your Coach conversations, what the Coach remembered, your plans and your document summaries stay, and you can read or delete them yourself.",
@@ -6058,7 +6275,7 @@
"error": "The withdrawal could not be saved. Please try again.",
"documentReading": {
"title": "Consent to reading documents",
- "activeSince": "Given on {date}. Documents, lab report scans and medication descriptions may be sent to your AI service for reading.",
+ "activeSince": "Given on {date}: documents, lab scans and medication text may go to your AI service for reading.",
"confirmBody": "Documents, lab report scans and medication descriptions are no longer sent for reading. Nothing you already have is deleted."
}
},
@@ -6080,7 +6297,21 @@
"title": "Coach plans",
"description": "The plans you agreed with the Coach, to read or remove.",
"open": "Open plans"
- }
+ },
+ "reasoning": {
+ "label": "Reasoning",
+ "hint": "Reasoning models such as Gemma or Qwen think before they answer. Off leaves the whole answer budget for the answer itself.",
+ "options": {
+ "default": "Default",
+ "none": "Off",
+ "low": "Low",
+ "medium": "Medium",
+ "high": "High"
+ },
+ "coachNote": "In the Coach, the thinking depth from the Coach settings applies."
+ },
+ "providerCardDescription": "The provider HealthLog asks first, and its key and model.",
+ "runtimeTitle": "Check and refresh"
},
"dashboard": {
"units": {
@@ -6104,8 +6335,8 @@
"withingsClientId": "Client ID",
"withingsClientSecret": "Client Secret",
"withingsCredentialsSaved": "Credentials saved",
- "withingsCredentialsSavedPlaceholder": "Saved — enter new to replace",
- "withingsCredentialsSavedPlaceholderSecret": "Saved — enter new to replace",
+ "withingsCredentialsSavedPlaceholder": "Saved. Enter new to replace",
+ "withingsCredentialsSavedPlaceholderSecret": "Saved. Enter new to replace",
"withingsSaveCredentials": "Save",
"configured": "Configured",
"withingsSync": "Sync now",
@@ -6120,7 +6351,7 @@
},
"withingsFullSyncResult": "{count} measurements fully synchronized",
"withingsSyncFailed": "Sync failed",
- "withingsOauthConnected": "Withings connected — the first sync starts in the background.",
+ "withingsOauthConnected": "Withings connected. The first sync starts in the background.",
"withingsOauthFailed": "Withings connection failed",
"withingsOauthError": {
"csrf1": "The security check failed because the browser cookie was missing. Please start the connection again in the same browser.",
@@ -6145,8 +6376,8 @@
"whoopClientId": "Client ID",
"whoopClientSecret": "Client Secret",
"whoopCredentialsSaved": "Credentials saved",
- "whoopCredentialsSavedPlaceholder": "Saved — enter new to replace",
- "whoopCredentialsSavedPlaceholderSecret": "Saved — enter new to replace",
+ "whoopCredentialsSavedPlaceholder": "Saved. Enter new to replace",
+ "whoopCredentialsSavedPlaceholderSecret": "Saved. Enter new to replace",
"whoopSaveCredentials": "Save",
"whoopSync": "Sync now",
"whoopFullSync": "Sync all data",
@@ -6161,7 +6392,7 @@
"whoopConnect": "Connect with WHOOP",
"whoopNoCredentials": "Please enter your API credentials above to connect WHOOP.",
"whoopBackfillInProgress": "Importing your WHOOP history in the background…",
- "whoopOauthConnected": "WHOOP connected — the first sync starts in the background.",
+ "whoopOauthConnected": "WHOOP connected. The first sync starts in the background.",
"whoopOauthFailed": "WHOOP connection failed",
"whoopOauthError": {
"csrf1": "The security check failed because the browser cookie was missing. Please start the connection again in the same browser.",
@@ -6175,14 +6406,14 @@
},
"fitbit": "Fitbit",
"fitbitTag": "Fitbit & Pixel",
- "fitbitDescription": "Connect Fitbit to sync your Fitbit and Pixel Watch vitals — weight, heart rate, and more.",
+ "fitbitDescription": "Connect Fitbit to sync your Fitbit and Pixel Watch vitals: weight, heart rate, and more.",
"fitbitExperimentalBadge": "Experimental",
"fitbitCredentials": "API Credentials",
"fitbitClientId": "Client ID",
"fitbitClientSecret": "Client Secret",
"fitbitCredentialsSaved": "Credentials saved",
- "fitbitCredentialsSavedPlaceholder": "Saved — enter new to replace",
- "fitbitCredentialsSavedPlaceholderSecret": "Saved — enter new to replace",
+ "fitbitCredentialsSavedPlaceholder": "Saved. Enter new to replace",
+ "fitbitCredentialsSavedPlaceholderSecret": "Saved. Enter new to replace",
"fitbitSaveCredentials": "Save",
"fitbitSync": "Sync now",
"fitbitFullSync": "Sync all data",
@@ -6197,18 +6428,18 @@
"fitbitConnect": "Connect with Fitbit",
"fitbitNoCredentials": "Please enter your API credentials above to connect Fitbit.",
"fitbitBackfillInProgress": "Importing your Fitbit history in the background…",
- "fitbitOauthConnected": "Fitbit connected — the first sync starts in the background.",
+ "fitbitOauthConnected": "Fitbit connected. The first sync starts in the background.",
"fitbitOauthFailed": "Fitbit connection failed",
"googleHealth": "Google Health",
"googleHealthTag": "Fitbit & Pixel",
"googleHealthBetaBadge": "Beta",
- "googleHealthDescription": "Connect Fitbit, Pixel Watch and Fitbit Air through your Google account — weight, heart rate, sleep and activity. This replaces the classic Fitbit connection, which Google retires in September 2026.",
+ "googleHealthDescription": "Connect Fitbit, Pixel Watch and Fitbit Air through your Google account: weight, heart rate, sleep and activity. This replaces the classic Fitbit connection, which Google retires in September 2026.",
"googleHealthCredentials": "API Credentials",
"googleHealthClientId": "Client ID",
"googleHealthClientSecret": "Client Secret",
"googleHealthCredentialsSaved": "Credentials saved",
- "googleHealthCredentialsSavedPlaceholder": "Saved — enter new to replace",
- "googleHealthCredentialsSavedPlaceholderSecret": "Saved — enter new to replace",
+ "googleHealthCredentialsSavedPlaceholder": "Saved. Enter new to replace",
+ "googleHealthCredentialsSavedPlaceholderSecret": "Saved. Enter new to replace",
"googleHealthSaveCredentials": "Save",
"googleHealthSync": "Sync now",
"googleHealthSyncCheckStatus": "Refresh status",
@@ -6242,9 +6473,9 @@
"googleHealthConnect": "Connect with Google",
"googleHealthNoCredentials": "Please enter your API credentials above to connect Google Health.",
"googleHealthBackfillInProgress": "Importing your Google Health history in the background…",
- "googleHealthReauthBanner": "Google has expired the connection — this happens periodically while your OAuth app stays in Testing mode. Reconnect to resume syncing.",
+ "googleHealthReauthBanner": "Google has expired the connection. This happens periodically while your OAuth app stays in Testing mode. Reconnect to resume syncing.",
"googleHealthReconnect": "Reconnect",
- "googleHealthOauthConnected": "Google Health connected — the first sync starts in the background.",
+ "googleHealthOauthConnected": "Google Health connected. The first sync starts in the background.",
"googleHealthOauthFailed": "Google Health connection failed",
"googleHealthOauthError": {
"csrf1": "The security check failed because the browser cookie was missing. Please start the connection again in the same browser.",
@@ -6279,10 +6510,10 @@
},
"integrationPill": {
"connected": "Connected",
- "errorReconnect": "Error — reconnect",
+ "errorReconnect": "Error: reconnect",
"warningServerError": "Sync failing",
"failureCount": "{count}/{threshold}",
- "parkedReconnect": "Paused — reconnect",
+ "parkedReconnect": "Paused: reconnect",
"notConnected": "Not connected",
"justNow": "just now",
"minutesAgo": "{count} min ago",
@@ -6297,7 +6528,7 @@
},
"integrationFreshness": {
"title": "Data received",
- "summary": "{count} data types · newest {relative}",
+ "summary": "{count} data types, newest {relative}",
"quiet": "{count} quiet",
"workouts": "Workouts"
},
@@ -6310,32 +6541,34 @@
"whoopViewData": "View your recovery",
"fitbitViewData": "View your sleep",
"googleHealthViewData": "View your steps",
- "apiTokens": "API Tokens",
- "apiTokensDescription": "Review and revoke API tokens for external medication intake.",
+ "apiTokens": "API tokens",
+ "apiTokensDescription": "Review and revoke tokens for measurements, workouts, documents and medication intake.",
"measurementsToken": {
"title": "Measurement tokens",
"description": "Mint a token so a scale, a watch bridge or your home automation can add readings.",
"detail": "The token is shown once.",
- "scopeNote": "It can only add new readings to your own record — it cannot read your data, change or delete anything, or create another token. Readings are marked as coming from an external device, so you can tell them apart later, and you can still correct them yourself.",
- "createFailed": "The token could not be created. Please try again."
+ "scopeNote": "It can only add new readings to your own record. It cannot read your data, change or delete anything, or create another token. Readings are marked as coming from an external device, so you can tell them apart later, and you can still correct them yourself.",
+ "createFailed": "The token could not be created. Please try again.",
+ "namePlaceholder": "Name, e.g. Home Assistant"
},
"workoutsToken": {
"title": "Workout tokens",
"description": "Mint a token so a watch bridge or a sync script can add your workouts.",
"detail": "The token is shown once.",
"scopeNote": "It can only add new workouts to your own record. It cannot read your data, change or delete anything, or create another token. Workouts it adds are marked as coming from an external device, and when your phone recorded the same session, you see the phone's copy.",
- "createFailed": "The token could not be created. Please try again."
+ "createFailed": "The token could not be created. Please try again.",
+ "namePlaceholder": "Name, e.g. Garmin"
},
"documentsToken": {
"title": "Document tokens",
"description": "Mint a token so Paperless-ngx, Papra or the import script can add documents to your vault.",
"detail": "The token is shown once.",
"scopeNote": "It can only add new documents to your own vault. It cannot list, open, download, change or delete anything, and it cannot create another token. What it gets back is whether a file was stored, not how you filed it.",
- "createFailed": "The token could not be created. Please try again."
+ "createFailed": "The token could not be created. Please try again.",
+ "namePlaceholder": "Name, e.g. Paperless-ngx"
},
- "tokenMintMovedDescription": "Tokens are issued where they apply: above for measurements and documents, or in a medication's own API endpoint for that one medication.",
- "tokenNamePlaceholder": "Token name (e.g., iPhone Shortcut)",
- "tokenCreated": "Token created — copy it now! It won't be shown again.",
+ "tokenMintMovedDescription": "Tokens are issued where they apply: above for measurements, workouts and documents, or in a medication's own API endpoint for that one medication.",
+ "tokenCreated": "Token created. Copy it now! It won't be shown again.",
"tokenCopy": "Copy token",
"tokenCopied": "Token copied to clipboard",
"tokenCopyFailed": "Could not copy the token",
@@ -6362,7 +6595,7 @@
"apiEndpointPath": "Path",
"apiEndpointAuth": "Authentication",
"apiEndpointExample": "Body example",
- "dangerZone": "Delete All Data",
+ "dangerZone": "Delete all data",
"dangerZoneDescription": "Erases your whole health record and every integration connection.",
"dangerZoneConfirm": "Delete all data?",
"dangerZoneConfirmDescription": "This permanently deletes everything in your record: measurements, medications, mood entries, lab results, cycle entries, documents, AI conversations and every integration connection, together with everything derived from them and anything you deleted earlier. Share links you gave to a doctor stop working immediately. Your account stays, along with the credentials you sign in with and display settings such as language, timezone and units. This cannot be undone.",
@@ -6374,7 +6607,7 @@
"deleteAccountCta": "Delete account",
"deleteAccountConfirmTitle": "Delete account permanently?",
"deleteAccountConfirmDescription": "Your account, all health data, passkeys, sessions, and audit entries will be permanently deleted. You will be signed out immediately after.",
- "deleteAccountSuccess": "Account deleted — signing you out.",
+ "deleteAccountSuccess": "Account deleted. Signing you out.",
"deleteAccountFailed": "Account could not be deleted",
"deleteAccountFinal": "Delete permanently",
"saved": "Saved",
@@ -6397,7 +6630,7 @@
"testing": "Testing…",
"ok": "Connected (latency {latency} ms)",
"errors": {
- "credentials_rejected": "Credentials rejected — check your token",
+ "credentials_rejected": "Credentials rejected. Check your token",
"rate_limited": "Rate-limited by the upstream",
"timeout": "Request timed out",
"upstream_error": "Upstream returned an error",
@@ -6408,7 +6641,7 @@
"private_origin_not_approved": "Private network target. The operator has to list this origin in NOTIFICATION_PRIVATE_ORIGINS",
"private_origin_not_grantable": "This address is link-local, a metadata or an unspecified address; no operator grant can open it. Use the address the relay listens on",
"url_invalid": "URL is invalid",
- "redirected": "Endpoint redirects — check the URL",
+ "redirected": "Endpoint redirects. Check the URL",
"endpoint_not_found": "Endpoint not found at the URL",
"credentials_unreadable": "Credentials cannot be decrypted",
"upstream_invalid_json": "Upstream sent invalid JSON",
@@ -6419,7 +6652,7 @@
},
"notificationStatus": {
"title": "Delivery status",
- "description": "State of each channel — “Auto-disabled” means retries stopped after permanent errors.",
+ "description": "State of each channel; “Auto-disabled” means retries stopped after permanent errors.",
"emptyDescription": "Set up a channel under Settings › Integrations › Delivery channels.",
"stateActive": "Active",
"stateAutoDisabled": "Auto-disabled",
@@ -6445,7 +6678,7 @@
},
"healthRecord": {
"title": "Export health record",
- "description": "A complete record for your doctor — as a PDF, as FHIR, or both.",
+ "description": "A complete record for your doctor: as a PDF, as FHIR, or both.",
"format": "Format",
"format_pdf": "PDF",
"format_fhir": "Structured (FHIR)",
@@ -6477,7 +6710,7 @@
"createTitle": "New share link",
"createDescription": "The link is read-only and time-boxed.",
"label": "Label",
- "labelPlaceholder": "e.g. Dr. Smith — annual check-up",
+ "labelPlaceholder": "e.g. Dr. Smith, annual check-up",
"range": "Days of history",
"rangeHint": "How far back the shared window reaches.",
"expiry": "Expires in (days)",
@@ -6497,7 +6730,7 @@
"statusRevoked": "Revoked",
"statusExpired": "Expired",
"inactiveTitle": "Revoked and expired ({count})",
- "shownOnce": "Shown once only. Capture the QR code and passphrase now — neither can be recovered later.",
+ "shownOnce": "Shown once only. Capture the QR code and passphrase now; neither can be recovered later.",
"qrAlt": "QR code containing the share link and passphrase",
"linkLabel": "Link",
"passphraseLabel": "Passphrase",
@@ -6508,12 +6741,12 @@
"attachTitle": "Attach documents",
"attachButton": "Choose documents",
"attachCount": "{count} of {max} selected",
- "attachFrozen": "The document set is fixed once you create the link — documents can't be added or removed afterwards.",
+ "attachFrozen": "The document set is fixed once you create the link. Documents can't be added or removed afterwards.",
"attachRemove": "Remove {title}",
"exifNote": "Camera metadata (EXIF/GPS) is removed from JPEG, PNG and WebP images when shared. PDF, TIFF, HEIC and Office files pass through unchanged.",
"documentCount": "Attached documents: {count}",
"qrScanTitle": "Scan to open on another device",
- "qrScanHint": "Point a phone camera at the code to open the shared record — the passphrase is already included.",
+ "qrScanHint": "Point a phone camera at the code to open the shared record. The passphrase is already included.",
"pickerTitle": "Attach documents",
"pickerDescription": "Search your vault and select documents to share. Up to {max}.",
"pickerSearch": "Search documents",
@@ -6526,7 +6759,8 @@
"statusNeedsReselect": "Pick contents again",
"reselectReason": "This link served a scope that was never chosen, so it was closed. Create a new one and pick what it should contain.",
"reselectAction": "Create a replacement",
- "createDetail": "The token is shown once on creation — store it then; it cannot be recovered."
+ "createDetail": "The token is shown once on creation. Store it then; it cannot be recovered.",
+ "activeDescription": "Links you have shared, when they expire and how often they were opened."
},
"genderOther": "Other",
"nightscout": "Nightscout",
@@ -6534,10 +6768,10 @@
"nightscoutDescription": "Connect your own Nightscout site to sync continuous glucose readings into the glucose panel.",
"nightscoutViewData": "View your glucose",
"nightscoutUrl": "Nightscout URL",
- "nightscoutUrlSavedPlaceholder": "Saved — enter a new URL to replace it",
+ "nightscoutUrlSavedPlaceholder": "Saved. Enter a new URL to replace it",
"nightscoutToken": "API token",
"nightscoutTokenOptional": "Optional for a public site",
- "nightscoutTokenSavedPlaceholder": "Saved — enter a new token to replace it",
+ "nightscoutTokenSavedPlaceholder": "Saved. Enter a new token to replace it",
"nightscoutTokenHelp": "Paste an access token from your Nightscout admin tools. Leave empty if your site is publicly readable.",
"nightscoutPrivateOperatorTitle": "Private Nightscout access requires server operator approval.",
"nightscoutPrivateOperatorDescription": "Ask your server operator to approve this exact Nightscout origin.",
@@ -6558,7 +6792,7 @@
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
"polarCredentialsSaved": "Credentials saved",
- "polarCredentialsSavedPlaceholder": "Saved — enter new to replace",
+ "polarCredentialsSavedPlaceholder": "Saved. Enter new to replace",
"polarSaveCredentials": "Save",
"polarUnavailable": "Polar is not configured on this server. Add your own Polar OAuth credentials above, or ask your administrator to configure the shared app.",
"polarConnect": "Connect Polar",
@@ -6568,7 +6802,7 @@
"polarDisconnect": "Disconnect",
"polarDisconnectTitle": "Disconnect Polar?",
"polarDisconnectDescription": "Future syncs stop. Readings already imported stay in your history.",
- "polarOauthConnected": "Polar connected — the first sync starts in the background.",
+ "polarOauthConnected": "Polar connected. The first sync starts in the background.",
"polarOauthFailed": "Polar connection failed",
"polarOauthError": {
"csrf1": "The security check failed because the browser cookie was missing. Please start the connection again in the same browser.",
@@ -6587,7 +6821,7 @@
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
"ouraCredentialsSaved": "Credentials saved",
- "ouraCredentialsSavedPlaceholder": "Saved — enter new to replace",
+ "ouraCredentialsSavedPlaceholder": "Saved. Enter new to replace",
"ouraSaveCredentials": "Save",
"ouraUnavailable": "Oura is not configured on this server. Add your own Oura OAuth credentials above, or ask your administrator to configure the shared app.",
"ouraConnect": "Connect Oura",
@@ -6597,7 +6831,7 @@
"ouraDisconnect": "Disconnect",
"ouraDisconnectTitle": "Disconnect Oura?",
"ouraDisconnectDescription": "Future syncs stop. Readings already imported stay in your history.",
- "ouraOauthConnected": "Oura connected — the first sync starts in the background.",
+ "ouraOauthConnected": "Oura connected. The first sync starts in the background.",
"ouraOauthFailed": "Oura connection failed",
"ouraOauthError": {
"csrf1": "The security check failed because the browser cookie was missing. Please start the connection again in the same browser.",
@@ -6607,6 +6841,7 @@
"nocreds": "Oura is not configured on this server. Ask your administrator to add the Oura OAuth credentials.",
"token": "The token exchange with Oura failed. Please start the connection again.",
"rate_limited": "Too many connection attempts. Wait a moment and try again.",
+ "issuer": "The sign-in answer did not come from Oura's own authorization server, so the connection was stopped for your safety. Please start the connection again. If this keeps happening, tell your administrator.",
"generic": "Unknown error while connecting. Please start the connection again."
},
"integrationSetupGuide": "Setup guide",
@@ -6618,10 +6853,17 @@
"savedOn": "Coach activated.",
"savedOff": "Coach hidden.",
"saveError": "Could not save the change. Please try again."
+ },
+ "memory": {
+ "edit": "Edit",
+ "source": {
+ "user": "from you",
+ "coach": "from the Coach"
+ }
}
},
"integrationRedirectGuide": {
- "intro": "This connects through your own {provider} developer app — about 5 minutes to set up:",
+ "intro": "This connects through your own {provider} developer app, about 5 minutes to set up:",
"step1": "Create an app in the {provider} developer portal.",
"step2": "Set its redirect / callback URL to:",
"step3": "Paste the client ID and client secret it gives you above."
@@ -6681,7 +6923,7 @@
},
"recovery": {
"saveNow": "Save these recovery codes now",
- "saveNowHint": "Each code works once. They are shown only this time — store them somewhere safe. If you lose your authenticator, a recovery code is the only way back in.",
+ "saveNowHint": "Each code works once. They are shown only this time. Store them somewhere safe. If you lose your authenticator, a recovery code is the only way back in.",
"download": "Download",
"remainingOne": "{count} recovery code remaining.",
"remainingOther": "{count} recovery codes remaining.",
@@ -6709,7 +6951,7 @@
},
"passkeyNudge": {
"title": "Add a passkey for faster sign-in",
- "body": "Passkeys let you sign in with your fingerprint, face, or device PIN — no password to remember."
+ "body": "Passkeys let you sign in with your fingerprint, face, or device PIN, no password to remember."
},
"trustedDevices": {
"title": "Trusted devices",
@@ -6745,12 +6987,11 @@
},
"stored": {
"title": "What's stored",
- "measurements": "Measurements (weight, blood pressure, pulse, glucose, sleep, body composition)",
- "medications": "Medications and intake history",
- "moodLabs": "Mood entries and lab results",
- "coach": "Coach conversations",
+ "measurements": "Measurements: weight, blood pressure, pulse and every other value you or a device record",
"integrations": "Integration tokens (encrypted)",
- "security": "Account and security activity (sessions, sign-in log)"
+ "security": "Account and security activity (sessions, sign-in log)",
+ "profile": "Your profile, medical history and health-record details",
+ "modules": "Records of each module: {list}"
},
"retention": {
"title": "Retention",
@@ -6781,13 +7022,11 @@
"mcp": {
"enableTitle": "Remote MCP endpoint",
"enableDescription": "Let an MCP-capable assistant (Claude.ai, ChatGPT) read your health data over a scoped token.",
- "enabled": "Enabled",
- "disabled": "Disabled",
"enableToggleLabel": "Enable the MCP endpoint",
"endpointLabel": "Connector URL",
"connectDescription": "Paste this URL into your assistant's custom-connector setup, then authorize with the token below or sign in when prompted.",
"tokensTitle": "Connector tokens",
- "tokensDescription": "Mint a token to connect an assistant — read-only unless you allow logging below.",
+ "tokensDescription": "Mint a token to connect an assistant, read-only unless you allow logging below.",
"scopeNote": "New tokens can only read your own records. Write access (health:write) is granted per token with the toggle below. A token can never reach admin functions.",
"writeScopeToggleLabel": "Allow logging (health:write)",
"writeScopeNote": "Adds the health:write scope so the assistant can log new measurements and mood entries to your account. Every write is confirmed first. The token still can't delete or edit existing entries, change medications, or reach REST or admin functions. Leave off for read-only access.",
@@ -6801,7 +7040,8 @@
"connectionRevokeDescription": "This ends the assistant's access immediately and stops it from renewing. It will need to be re-authorized to reconnect.",
"connectionRevokeFailed": "Could not disconnect the assistant.",
"connectionsDetail": "Revoking a connection ends its access immediately, including its ability to refresh.",
- "tokensDetail": "The token is shown once."
+ "tokensDetail": "The token is shown once.",
+ "tokenNamePlaceholder": "Name, e.g. Claude Desktop"
},
"strava": "Strava",
"stravaDescription": "Connect your Strava account to sync your activities into your workout history.",
@@ -6810,7 +7050,7 @@
"stravaClientId": "Client ID",
"stravaClientSecret": "Client Secret",
"stravaCredentialsSaved": "Credentials saved",
- "stravaCredentialsSavedPlaceholder": "Saved — enter new to replace",
+ "stravaCredentialsSavedPlaceholder": "Saved. Enter new to replace",
"stravaSaveCredentials": "Save",
"stravaUnavailable": "Strava is not configured on this server. Add your own Strava API credentials above, or ask your administrator to configure the shared app.",
"stravaConnect": "Connect Strava",
@@ -6820,7 +7060,7 @@
"stravaDisconnect": "Disconnect",
"stravaDisconnectTitle": "Disconnect Strava?",
"stravaDisconnectDescription": "Future syncs stop. Activities already imported stay in your history.",
- "stravaOauthConnected": "Strava connected — the first sync starts in the background.",
+ "stravaOauthConnected": "Strava connected. The first sync starts in the background.",
"stravaOauthFailed": "Strava connection failed",
"stravaOauthError": {
"csrf1": "The security check failed because the browser cookie was missing. Please start the connection again in the same browser.",
@@ -6848,26 +7088,33 @@
"title": "Sync progress",
"receivedLabel": "Records received",
"oldestLabel": "Oldest reading reached",
- "flowing": "Data arrived within the last hour — the iPhone app is still delivering.",
- "waiting": "Waiting for the iPhone app to send more data. Delivery continues when the app is open or when iOS grants it background time — a quiet stretch during the first big backfill is normal, not an error."
+ "flowing": "Data arrived within the last hour. The iPhone app is still delivering.",
+ "waiting": "Waiting for the iPhone app to send more data. Delivery continues when the app is open or when iOS grants it background time. A quiet stretch during the first big backfill is normal, not an error."
},
"delivery": {
"title": "Delivery",
"lastTriggerLabel": "Latest sync arrived",
"lastBackgroundLabel": "Last background sync",
- "backgroundNever": "Never — every sync so far arrived with the app open",
+ "backgroundNever": "Never: every sync so far arrived with the app open",
"noSyncYet": "No sync has arrived yet, so there is nothing to report here.",
"trigger": {
"foreground": "with the app open",
"background": "in the background",
"push": "from a push wake",
- "unknown": "trigger not reported by the app"
+ "unknown": "trigger not reported by the app",
+ "manual": "from Sync all"
}
+ },
+ "freshness": {
+ "title": "By data type",
+ "lastReceived": "Last received {relative}",
+ "lastNewValue": "Last new value {relative}",
+ "never": "Not received yet"
}
},
"garminInfo": {
"title": "Garmin",
- "body": "Garmin has no direct connection here. Connect via Apple Health (iOS) or Google Health Connect (Android). Garmin's proprietary scores — Body Battery, Training Load, HRV Status, VO₂ max — are not shared by Garmin to either platform.",
+ "body": "Garmin has no direct connection here. Connect via Apple Health (iOS) or Google Health Connect (Android). Garmin's proprietary scores (Body Battery, Training Load, HRV Status, VO₂ max) are not shared by Garmin to either platform.",
"docsLink": "Read the guide"
},
"glucoseReference": {
@@ -6923,6 +7170,12 @@
"description": "This shares or exports your health data. Confirm it's you to continue.",
"signInAgain": "Sign in again, then try once more. Your account has no password or passkey to confirm here.",
"failed": "That did not work. Check what you entered and try again."
+ },
+ "passwordTitle": "Password",
+ "tokenNameLabel": "Token name",
+ "displayCard": {
+ "title": "Display",
+ "description": "Language, units and formats, saved as soon as you change them."
}
},
"admin": {
@@ -6942,27 +7195,27 @@
"loadingStatus": "Loading status…",
"systemStatusLoadError": "Failed to load system status.",
"systemStatusRetry": "Retry",
- "adminSettingsLoadError": "Failed to load admin settings. Toggles below may not reflect the live state — refresh the page.",
+ "adminSettingsLoadError": "Failed to load admin settings. Toggles below may not reflect the live state. Refresh the page.",
"hostMetrics": {
"title": "Host load",
"load1": "Load (1m)",
"memUsedPercent": "Memory",
"diskBusyPercent": "Disk I/O",
"last2hours": "Last 2 hours",
- "empty": "Sampling started — data appears in 1–2 minutes.",
+ "empty": "Sampling started. Data appears in 1–2 minutes.",
"loadError": "Failed to load host metrics. Try refreshing the page."
},
"aiServerKey": {
"title": "AI server key",
"description": "An operator key for accounts that have not configured one of their own.",
- "byokHint": "A user's own key (BYOK, ChatGPT login or local provider) always takes precedence — the server key is the last step in the provider chain.",
+ "byokHint": "A user's own key (BYOK, ChatGPT login or local provider) always takes precedence; the server key is the last step in the provider chain.",
"consentHint": "Requests over the server key require the user's active AI consent; without a consent receipt nothing is sent.",
"baseUrlHint": "The base URL selects the API: an Anthropic URL such as https://api.anthropic.com/v1 is understood, any other host is called on the OpenAI-compatible API.",
"statusConfigured": "Configured",
"statusNotConfigured": "Not configured",
"keyLabel": "API key",
"keyPlaceholder": "sk-...",
- "keyPlaceholderSet": "Stored ({preview}) — enter a new key to replace it",
+ "keyPlaceholderSet": "Stored ({preview}). Enter a new key to replace it",
"modelLabel": "Model",
"baseUrlLabel": "Base URL",
"save": "Save",
@@ -6996,13 +7249,16 @@
"typeCodex": "ChatGPT sign-in (personal)",
"typeLocal": "Local model (personal)",
"typeGateway": "OpenAI-compatible gateway (personal)",
- "spendToday": "Today ({date}): {total} tokens, {operator} of them on the operator's own key."
+ "spendToday": "Today, on each account's own calendar day: {total} tokens, {operator} of them on the operator's own key (accounts with spend: {accounts}).",
+ "statusPaused": "Paused",
+ "pausedHint": "Paused after repeated failures for {count} users until {at}. It is tried again automatically then.",
+ "modelNotServed": "The endpoint no longer offers the model {model}. Choose a model it lists in the AI settings."
},
"aiQuality": {
"title": "Insights Quality",
"loading": "Loading feedback summary…",
"loadError": "Failed to load Insights quality summary. Try refreshing the page.",
- "empty": "No feedback yet. The aggregator runs daily at 04:00 — once users start rating recommendations, helpful-rate bands appear here.",
+ "empty": "No feedback yet. The aggregator runs daily at 04:00. Once users start rating recommendations, helpful-rate bands appear here.",
"windowLabel": "Last {days} days",
"generatedAtLabel": "Generated",
"colSeverity": "Severity",
@@ -7017,7 +7273,7 @@
"title": "Coach Feedback",
"loading": "Loading Coach feedback summary…",
"loadError": "Failed to load Coach feedback summary. Try refreshing the page.",
- "empty": "No Coach feedback yet. The aggregator runs daily at 04:00 — once users rate Coach replies, helpful-rate bands appear here.",
+ "empty": "No Coach feedback yet. The aggregator runs daily at 04:00. Once users rate Coach replies, helpful-rate bands appear here.",
"windowLabel": "Last {days} days",
"generatedAtLabel": "Generated",
"colPromptVersion": "Prompt version",
@@ -7032,17 +7288,17 @@
"workerRunning": "Running",
"workerStopped": "Stopped",
"lastReminderCheck": "Last Reminder Check",
- "appSettings": "Application Settings",
+ "appSettings": "Application settings",
"appSettingsDescription": "Global defaults for registration, language, and the timezone of new accounts.",
"registrationEnabled": "Allow registration",
"registrationEnabledDescription": "When disabled, new users cannot register.",
- "registrationOpenHint": "Registration is open — anyone who can reach this instance can create an account. Close it here once everyone who needs an account has one.",
+ "registrationOpenHint": "Registration is open: anyone who can reach this instance can create an account. Close it here once everyone who needs an account has one.",
"defaultLanguage": "Default language",
"defaultLanguageDescription": "Used for new users and as fallback.",
"defaultUserTimezone": "Default timezone for new accounts",
- "defaultUserTimezoneDescription": "Used for new accounts that don't carry a browser-detected zone. Does not affect existing users.",
+ "defaultUserTimezoneDescription": "Used for new accounts without a browser-detected zone; existing accounts keep theirs.",
"defaultUserTimezoneFallback": "Europe/Berlin (default)",
- "servicesGlobal": "Services (global)",
+ "servicesGlobal": "Channels and API",
"servicesGlobalDescription": "Disabled services are not available to any user.",
"telegramGlobal": "Allow Telegram notifications.",
"ntfyGlobal": "Allow ntfy notifications.",
@@ -7050,19 +7306,21 @@
"integrationWebPush": "Web Push",
"offlineGeoLabel": "Geo data",
"offlineGeoEnabled": "Offline (GeoLite2)",
- "offlineGeoFallback": "Online ({host}) — mount GeoLite2 databases and set GEOLITE2_DIR",
+ "offlineGeoFallback": "Online ({host}). Mount GeoLite2 databases and set GEOLITE2_DIR",
+ "offlineGeoOff": "Off",
+ "offlineGeoChosen": "Online ({host})",
"apiGlobal": "Allow API endpoints and API tokens.",
"umamiTitle": "Umami",
- "umamiDescription": "Web analytics with Umami",
+ "umamiDescription": "Web analytics with Umami.",
"glitchtipTitle": "GlitchTip",
- "glitchtipDescription": "Error tracking with GlitchTip",
+ "glitchtipDescription": "Error tracking with GlitchTip.",
"webPushVapidTitle": "Web Push VAPID",
- "umamiEnabled": "Enable Umami tracking.",
+ "umamiEnabled": "Enable Umami tracking",
"umamiScriptUrl": "Umami script URL",
"umamiScriptUrlPlaceholder": "https://analytics.example.com/script.js",
"umamiWebsiteId": "Umami website ID",
"umamiWebsiteIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "glitchtipEnabled": "Enable GlitchTip error tracking.",
+ "glitchtipEnabled": "Enable GlitchTip error tracking",
"glitchtipDsn": "GlitchTip DSN",
"glitchtipDsnPlaceholder": "https://@glitchtip.example.com/",
"glitchtipEnvironment": "GlitchTip environment",
@@ -7085,7 +7343,7 @@
"webPushVapidGenerating": "Generating…",
"webPushVapidGenerated": "VAPID keys generated. Save the subject to finish.",
"webPushVapidGenerateFailed": "Could not generate VAPID keys.",
- "webPushVapidGenerateConfirm": "VAPID keys are already configured. Generating a new pair invalidates every browser push subscription — each device must re-subscribe. Continue?",
+ "webPushVapidGenerateConfirm": "VAPID keys are already configured. Generating a new pair invalidates every browser push subscription, and each device must re-subscribe. Continue?",
"webPushVapidGenerateConfirmTitle": "Regenerate VAPID keys?",
"webPushVapidRegenerate": "Regenerate",
"configured": "Configured",
@@ -7113,18 +7371,23 @@
"documentAi": {
"title": "Reading documents",
"description": "AI reading of stored documents, lab report scans and medication extraction from text. The largest share of data leaving the server."
+ },
+ "reasoning": {
+ "enabled": "Allow deeper thinking",
+ "max": "Highest thinking depth",
+ "description": "Models think before they answer: the Coach, the daily briefing and the period summaries. More precise, uses more tokens."
}
},
"modules": {
"title": "Module availability",
"description": "Turn a module off for the whole server.",
"saveError": "Could not update module availability.",
- "detail": "A module disabled here is off for every account regardless of personal preference. Core domains (weight, blood pressure, pulse, medications) are always on and cannot be disabled.",
- "coachFollowsSwitch": "Follows the Coach switch under Assistant."
+ "detail": "Weight, blood pressure and pulse are core readings and always stay on.",
+ "coachFollowsSwitch": "Follows the Coach switch in the Coach section."
},
"invites": {
- "title": "Invites",
- "description": "Registration invite links — they work even while registration is closed.",
+ "title": "Invite links",
+ "description": "Each link admits a signup even while registration is closed.",
"create": "Create invite",
"createTitle": "Create invite",
"createDescription": "The link admits a signup even while registration is closed.",
@@ -7136,7 +7399,7 @@
"createdToast": "Invite created",
"createError": "Could not create the invite.",
"tokenTitle": "Invite link",
- "tokenOnceHint": "Copyable only now — copy the link or have the QR code scanned.",
+ "tokenOnceHint": "Copyable only now. Copy the link or have the QR code scanned.",
"hashExplainer": "Only a hash of the link is stored, so it cannot be shown again later. If it gets lost, revoke the invite and create a new one.",
"copyLink": "Copy link",
"copiedToast": "Link copied",
@@ -7170,8 +7433,8 @@
"revokedToast": "Invite revoked.",
"revokeError": "Could not revoke the invite."
},
- "userManagement": "User Management",
- "userManagementDescription": "Manage accounts: roles, password resets, and forced logouts.",
+ "userManagement": "Accounts",
+ "userManagementDescription": "Roles, password resets and forced logouts for every account.",
"userName": "Name",
"userEmail": "Email",
"userRole": "Role",
@@ -7192,20 +7455,19 @@
"passwordReset": "Password has been reset",
"reset": "Reset",
"loadingUsers": "Loading users…",
- "apiTokens": "API Tokens",
+ "apiTokens": "API tokens",
"apiTokensDescription": "All active and revoked API tokens across all accounts.",
"tokenUser": "User",
"tokenName": "Name",
"tokenPermissions": "Permissions",
"tokenPermissionAll": "All",
- "tokenPermissionAllTooltip": "Wildcard permission — this token can call every API route.",
+ "tokenPermissionAllTooltip": "Wildcard permission: this token can call every API route.",
"tokenStatus": "Status",
"tokenLastUsed": "Last used",
"tokenCreated": "Created",
"tokenNeverUsed": "Never",
"tokensEmptyTitle": "No API tokens yet",
"tokensEmptyDescription": "Tokens are issued when a native client signs in or via Settings → Account.",
- "loginOverview": "Login Overview",
"loginOverviewDescription": "Sign-in and auth events across all accounts, filterable and exportable as CSV.",
"allAuthEvents": "All auth events",
"failedOnly": "Failed only",
@@ -7238,6 +7500,7 @@
"finalDelete": "Delete permanently",
"deletedResult": "Deleted {rows} rows across {tables} tables. User accounts and sign-in credentials were kept.",
"authRegister": "Registration",
+ "authClaim": "Profile taken over",
"authLogin": "Login",
"authLoginPasskey": "Passkey login",
"authLoginPassword": "Password login",
@@ -7248,7 +7511,7 @@
"authTokenAutoissueNative": "Native client token issued",
"authTokenRefresh": "Token refreshed",
"authTokenRevoke": "Token revoked",
- "medicationReminders": "Medication Reminders",
+ "medicationReminders": "Medication reminders",
"medicationRemindersDescription": "Thresholds for reminder badge color transitions on medication cards.",
"notificationHealth": {
"title": "Notification health",
@@ -7303,7 +7566,7 @@
},
"section": {
"system-status": {
- "title": "System Status",
+ "title": "System status",
"subtitle": "Worker, database, and integration health at a glance."
},
"general": {
@@ -7312,7 +7575,7 @@
},
"services": {
"title": "Services",
- "subtitle": "Enable or disable notification channels and integrations system-wide."
+ "subtitle": "Turn notification channels and the API on or off for the whole server."
},
"integrations": {
"title": "Integrations",
@@ -7349,11 +7612,11 @@
"subtitle": "Mint, track, and revoke registration invite links."
},
"api-tokens": {
- "title": "API Tokens",
+ "title": "API tokens",
"subtitle": "Active bearer tokens issued to native clients and integrations."
},
"login-overview": {
- "title": "Login Overview",
+ "title": "Login overview",
"subtitle": "Audit trail of authentication and admin events."
},
"auditLog": {
@@ -7374,9 +7637,9 @@
"empty": "No audit entries match the current filters."
},
"app-logs": {
- "title": "App Logs",
+ "title": "App logs",
"subtitle": "Tail the most recent structured wide-events emitted by this process.",
- "processNote": "Showing app-process logs only — worker-process logs go to Loki when that integration is configured.",
+ "processNote": "Showing app-process logs only; worker-process logs go to Loki when that integration is configured.",
"filterTraceId": "Trace ID",
"filterAction": "Action contains…",
"filterLevel": "Level",
@@ -7398,7 +7661,8 @@
"refresh": "Refresh",
"eventDetails": "Event details",
"closeDetails": "Close details",
- "processDetail": "Buffer is volatile and capped at 500 entries."
+ "processDetail": "Buffer is volatile and capped at 500 entries.",
+ "cardTitle": "Recent events"
},
"backups": {
"title": "Backups",
@@ -7410,7 +7674,7 @@
"loading": "Loading backups…",
"loadError": "Failed to load backups.",
"emptyTitle": "No backups yet",
- "emptyDescription": "Backups run automatically every Sunday at 03:00 — or trigger one now to create the first snapshot.",
+ "emptyDescription": "Backups run automatically every Sunday at 03:00, or trigger one now to create the first snapshot.",
"colUser": "User",
"colType": "Type",
"typeWeeklyAuto": "Weekly (auto)",
@@ -7425,6 +7689,12 @@
"scheduleFailedDescription": "{when}: {reason}",
"scheduleFailedNoReason": "no reason recorded",
"scheduleRemedy": "Run a backup now and check that a fresh row appears. If it fails again, the worker log names the account it stopped on.",
+ "activity": {
+ "running": "Running since {when}.",
+ "interruptedTitle": "A run stopped partway and never came back",
+ "interruptedAccount": "{username}: started {when}, never finished",
+ "interruptedRemedy": "The app most likely restarted or ran out of memory while writing this copy. The next run takes these accounts last, so every other account gets its copy first. If it keeps happening, give the container more memory."
+ },
"download": "Download",
"downloadAria": "Download backup for {username}",
"downloadFailed": "Download failed",
@@ -7440,11 +7710,11 @@
"restoreTitle": "Restore from backup",
"restoreDescription": "This will replace ALL current data for {username} with the snapshot from {when}. Existing measurements, medications, intake events, mood entries, notification channels, push subscriptions and queued Telegram deletions will be deleted.",
"restoreInstanceSettingsLabel": "Also restore instance settings",
- "restoreInstanceSettingsHint": "Registration, MFA requirement, default locale, module availability, notification and AI configuration — taken from the snapshot and applied to everybody on this installation. Leave it clear to restore this account alone.",
+ "restoreInstanceSettingsHint": "Registration, MFA requirement, default locale, module availability, notification and AI configuration, taken from the snapshot and applied to everybody on this installation. Leave it clear to restore this account alone.",
"restorePromptLabel": "Type RESTORE to continue",
"previewTitle": "What this backup contains",
"previewLoading": "Reading contents…",
- "previewUnavailable": "Contents could not be read — restoring is still possible.",
+ "previewUnavailable": "Contents could not be read. Restoring is still possible.",
"previewStillReading": "This backup was made by an earlier version, so its contents are being read in full, which takes a while for a large record. Open this dialog again in a few minutes to see them. Restoring does not depend on it.",
"previewEmpty": "The file contains no records.",
"previewOther": "Other records",
@@ -7487,7 +7757,7 @@
"restoreSkippedMedicationTarget": "Medication target analyte",
"restoreSkippedScheduleRevisionLink": "Schedule era succession",
"restoreSkippedAccountSetting": "Account setting",
- "restoreMissingTitle": "Restore refused — the file is incomplete",
+ "restoreMissingTitle": "Restore refused: the file is incomplete",
"restoreMissingDescription": "The file's own manifest says it carries the section below, and it does not. Nothing was changed: a restore replaces a whole class of records, so putting back a section that is not in the file would have emptied it.",
"restoreMissingRemedy": "Use a disaster-recovery snapshot, or export the account again and upload the new file. A section the manifest declares as omitted is a different thing and restores normally.",
"restoreMissingDocuments": "Documents",
@@ -7555,10 +7825,15 @@
"restoreFailureKeyMissing": "The backup needs encryption key {keys}, which this server cannot open it with. Add the key back to ENCRYPTION_KEYS, restart, and start the restore again.",
"restoreFailureForeignReference": "The file points at records that belong to another account. Nothing was written.",
"previewKeyMissing": "This backup cannot be restored on this server: it needs encryption key {keys}. Add the key back to ENCRYPTION_KEYS and restart first.",
- "restoreSkippedSymptomEpisodeReference": "Symptom entry linked to an illness the file does not contain (kept without the link)"
+ "restoreSkippedSymptomEpisodeReference": "Symptom entry linked to an illness the file does not contain (kept without the link)",
+ "restoreSkippedCycleCiphertext": "Cycle note or label sealed with a key this server does not hold (day or symptom kept without it)",
+ "restoreSkippedMoodLabelCiphertext": "Mood tag or group name sealed with a key this server does not hold (kept without its name)",
+ "restoreSkippedVisitCiphertext": "Visit or practitioner text sealed with a key this server does not hold (kept without it)",
+ "restoreSkippedVaccinationCiphertext": "Vaccination note sealed with a key this server does not hold (dose kept without it)",
+ "restoreSkippedEnvironmentLocationCiphertext": "Location of an environment period or day sealed with a key this server does not hold (kept sealed, those days are not refreshed)"
},
"danger-zone": {
- "title": "Danger Zone",
+ "title": "Danger zone",
"subtitle": "Destructive system-wide operations."
},
"about": {
@@ -7572,7 +7847,6 @@
"encryption": {
"title": "Encryption",
"subtitle": "Encryption coverage and key-rotation status for data at rest.",
- "loading": "Loading encryption status…",
"loadError": "Failed to load encryption status.",
"coverageTitle": "Coverage",
"coverageDescription": "How many encrypted rows sit under each key id.",
@@ -7607,7 +7881,7 @@
"completed": "Completed",
"failed": "Failed"
},
- "coverageDetail": "Only key ids and counts are shown — never key material.",
+ "coverageDetail": "Only key ids and counts are shown, never key material.",
"backupsTitle": "Keys the backups still need",
"backupsDescription": "A retired key stays needed while any backup below still lists it.",
"backupsStoredLine": "Key {key}: {count} stored copies, the oldest from {when}",
@@ -7617,12 +7891,45 @@
"backupsNone": "No backup has recorded the keys it needs yet.",
"backupsStillNeed": "Every row is rotated, but backups still need {keys}",
"backupsUnrecordedBadge": "Every row is rotated, but older backups may still need a retired key",
- "backupsDetail": "Rotation re-encrypts every row, but a backup keeps the key its content was written under, and restoring it needs that key. Keep a retired key in ENCRYPTION_KEYS until nothing below lists it."
+ "backupsDetail": "Rotation re-encrypts every row, but a backup keeps the key its content was written under, and restoring it needs that key. Keep a retired key in ENCRYPTION_KEYS until nothing below lists it.",
+ "columnsShow": "Show the table ({count} with rows)",
+ "columnsHide": "Hide the table",
+ "columnsShowAll": "Show all {count} columns",
+ "columnsShowWithRows": "Only columns with rows",
+ "columnsNoneWithRows": "No encrypted column holds rows yet."
+ },
+ "maintenance": {
+ "title": "Measurement table maintenance",
+ "description": "Gives back the space old sync leftovers held, once the cleanup has run.",
+ "body": "It vacuums the measurements table and then rebuilds its indexes, one at a time. Reading and saving keep working while it runs. Pick a quiet hour, and have free disk for one more copy of the largest index.",
+ "tableSize": "Table with indexes",
+ "indexSize": "Indexes",
+ "loading": "Checking where the maintenance stands…",
+ "loadError": "Could not read the maintenance status.",
+ "unavailable": "No background worker is connected, so the maintenance cannot run here.",
+ "purgePending": "The cleanup of old sync leftovers is still running. The maintenance can start once it has finished.",
+ "state": {
+ "idle": "Not run yet.",
+ "queued": "Waiting for the background worker.",
+ "running": "Running since {since}. This can take a while on a large instance.",
+ "completed": "Finished on {at}.",
+ "refusedPurge": "Did not start, because the cleanup was still running. Start it again once the cleanup has finished.",
+ "alreadyRunning": "Did not start, because another run was already working.",
+ "failed": "Stopped on {at}. The app log names the step, and the runbook explains what to check."
+ },
+ "start": "Start maintenance",
+ "confirmTitle": "Start the maintenance now?",
+ "confirmBody": "It runs in the background, one step at a time, and can take a while. If you have not taken a backup today, take one first.",
+ "confirm": "Start",
+ "cancel": "Cancel",
+ "started": "Maintenance started.",
+ "alreadyQueued": "A maintenance run is already queued or running.",
+ "startFailed": "Could not start the maintenance."
}
},
"carrier": "Carrier",
"mfaRequired": "Require a second factor",
- "mfaRequiredDescription": "Every account without an active second factor is sent to enrollment after sign-in. This prompts web users to enroll — it does not block direct API or native (Bearer) access for accounts not yet enrolled.",
+ "mfaRequiredDescription": "Accounts without a second factor are sent to set one up after a web sign-in; API and native (Bearer) access stays open.",
"mfaEnforcedOn": "MFA on",
"mfaEnforcedOff": "MFA off",
"mfaEnforcedOnHint": "A second factor is required for this user. Click to stop requiring it.",
@@ -7640,7 +7947,7 @@
"centralCodex": {
"title": "Shared AI connection",
"description": "Connect one signed-in ChatGPT account that any user on this server can opt into for AI features.",
- "honesty": "This is a single shared, signed-in AI account — not an API key. Every user who opts in sends requests through it, so the account's hourly and weekly limits are shared across all of them, and the same limits already feed the Coach. Subscription providers may use the content to improve their models and no data-processing agreement covers it, so it is never the default and stays below no-training providers for document reads. Only connect an account you are comfortable sharing this way.",
+ "honesty": "This is a single shared, signed-in AI account, not an API key. Every user who opts in sends requests through it, so the account's hourly and weekly limits are shared across all of them, and the same limits already feed the Coach. Subscription providers may use the content to improve their models and no data-processing agreement covers it, so it is never the default and stays below no-training providers for document reads. Only connect an account you are comfortable sharing this way.",
"statusConnected": "Connected",
"statusDisconnected": "Not connected",
"statusExpired": "Expired",
@@ -7652,13 +7959,13 @@
"connectedSince": "Connected since {when}",
"connected": "Shared AI connection linked",
"disconnected": "Shared AI connection removed",
- "error": "Something went wrong — please try again"
+ "error": "Something went wrong. Please try again"
},
"failingJobs": {
"title": "Background jobs",
"description": "Queues whose jobs gave up after exhausting their retries.",
"none": "No job has failed in the last {hours} hours.",
- "unavailable": "No job queue to read — this deployment runs without the background worker.",
+ "unavailable": "No job queue to read. This deployment runs without the background worker.",
"countOne": "{count} failure",
"countFew": "{count} failures",
"countOther": "{count} failures",
@@ -7720,7 +8027,9 @@
"restartNote": "Restart HealthLog after the change.",
"lastResort": "Last resort, only if you are certain the key is right and none of the three applies: set {setting} and restart. HealthLog then serves anyway and only logs the finding. This disables a safety check: with a wrong key every encrypted value fails and new data is written under that key.",
"codes": "For the operator: API requests answer 503 with error code {code}, and /api/health reports reason {reason}."
- }
+ },
+ "loginOverviewCardTitle": "Sign-in events",
+ "userColumn": "User"
},
"achievements": {
"title": "Achievements",
@@ -7736,7 +8045,9 @@
"noneUnlockedYet": "No achievements unlocked yet.",
"pointsValue": "{points} points",
"metricCount": "{count}",
- "metricDays": "{count} days",
+ "metricDaysOne": "{count} day",
+ "metricDaysFew": "{count} days",
+ "metricDaysOther": "{count} days",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -7759,14 +8070,6 @@
"title": "Intake legend",
"description": "Record 300 taken medication events."
},
- "overIntake1": {
- "title": "Double take",
- "description": "Take medication at least once more than planned."
- },
- "skippedIntake1": {
- "title": "Stepped back",
- "description": "Skip at least one planned intake."
- },
"passkeyCreated1": {
"title": "Passkey set up",
"description": "Create your first passkey."
@@ -7792,19 +8095,19 @@
"description": "Take all medications on time for 1 day."
},
"compliance801": {
- "title": "80% adherence · 1d",
+ "title": "80% adherence for 1 day",
"description": "Reach at least 80% 30-day adherence for 1 day."
},
"bmiGreen1": {
- "title": "BMI green · 1d",
+ "title": "BMI green for 1 day",
"description": "Keep your BMI in the normal range for 1 day."
},
"bpGreen1": {
- "title": "BP green · 1d",
+ "title": "BP green for 1 day",
"description": "Keep your blood pressure in the normal range for 1 day."
},
"pulseGreen1": {
- "title": "Pulse green · 1d",
+ "title": "Pulse green for 1 day",
"description": "Keep your resting pulse in the normal range for 1 day."
},
"onTimePerfect7": {
@@ -7812,19 +8115,19 @@
"description": "Take all medications on time for 7 consecutive days."
},
"compliance807": {
- "title": "80% adherence · 7d",
+ "title": "80% adherence for 7 days",
"description": "Reach at least 80% 30-day adherence for 7 consecutive days."
},
"bmiGreen7": {
- "title": "BMI green · 7d",
+ "title": "BMI green for 7 days",
"description": "Keep your BMI in the normal range for 7 consecutive days."
},
"bpGreen7": {
- "title": "BP green · 7d",
+ "title": "BP green for 7 days",
"description": "Keep your blood pressure in the normal range for 7 consecutive days."
},
"pulseGreen7": {
- "title": "Pulse green · 7d",
+ "title": "Pulse green for 7 days",
"description": "Keep your resting pulse in the normal range for 7 consecutive days."
},
"onTimePerfect30": {
@@ -7832,19 +8135,19 @@
"description": "Take all medications on time for 30 consecutive days."
},
"compliance8030": {
- "title": "80% adherence · 30d",
+ "title": "80% adherence for 30 days",
"description": "Reach at least 80% 30-day adherence for 30 consecutive days."
},
"bmiGreen30": {
- "title": "BMI green · 30d",
+ "title": "BMI green for 30 days",
"description": "Keep your BMI in the normal range for 30 consecutive days."
},
"bpGreen30": {
- "title": "BP green · 30d",
+ "title": "BP green for 30 days",
"description": "Keep your blood pressure in the normal range for 30 consecutive days."
},
"pulseGreen30": {
- "title": "Pulse green · 30d",
+ "title": "Pulse green for 30 days",
"description": "Keep your resting pulse in the normal range for 30 consecutive days."
},
"onTimePerfect180": {
@@ -7852,19 +8155,19 @@
"description": "Take all medications on time for 180 consecutive days."
},
"compliance80180": {
- "title": "80% adherence · 180d",
+ "title": "80% adherence for 180 days",
"description": "Reach at least 80% 30-day adherence for 180 consecutive days."
},
"bmiGreen180": {
- "title": "BMI green · 180d",
+ "title": "BMI green for 180 days",
"description": "Keep your BMI in the normal range for 180 consecutive days."
},
"bpGreen180": {
- "title": "BP green · 180d",
+ "title": "BP green for 180 days",
"description": "Keep your blood pressure in the normal range for 180 consecutive days."
},
"pulseGreen180": {
- "title": "Pulse green · 180d",
+ "title": "Pulse green for 180 days",
"description": "Keep your resting pulse in the normal range for 180 consecutive days."
},
"onTimePerfect360": {
@@ -7872,19 +8175,19 @@
"description": "Take all medications on time for 360 consecutive days."
},
"compliance80360": {
- "title": "80% adherence · 360d",
+ "title": "80% adherence for 360 days",
"description": "Reach at least 80% 30-day adherence for 360 consecutive days."
},
"bmiGreen360": {
- "title": "BMI green · 360d",
+ "title": "BMI green for 360 days",
"description": "Keep your BMI in the normal range for 360 consecutive days."
},
"bpGreen360": {
- "title": "BP green · 360d",
+ "title": "BP green for 360 days",
"description": "Keep your blood pressure in the normal range for 360 consecutive days."
},
"pulseGreen360": {
- "title": "Pulse green · 360d",
+ "title": "Pulse green for 360 days",
"description": "Keep your resting pulse in the normal range for 360 consecutive days."
},
"moodFirst": {
@@ -7968,15 +8271,15 @@
"description": "Switched the app language at least once."
},
"missFree7": {
- "title": "No dose missed · 7d",
+ "title": "No dose missed in 7 days",
"description": "7 consecutive days without a missed dose."
},
"missFree30": {
- "title": "No dose missed · 30d",
+ "title": "No dose missed in 30 days",
"description": "30 consecutive days without a missed dose."
},
"missFree90": {
- "title": "No dose missed · 90d",
+ "title": "No dose missed in 90 days",
"description": "90 consecutive days without a missed dose."
},
"measurementWeeks4": {
@@ -7985,10 +8288,10 @@
},
"selfContextComplete": {
"title": "Self-report maintained",
- "description": "All four self-report fields filled in — the Coach knows your context."
+ "description": "All four self-report fields filled in. The Coach knows your context."
},
"sleepLog7": {
- "title": "Sleep log · 7d",
+ "title": "Sleep log for 7 days",
"description": "Sleep data recorded on 7 consecutive days."
}
},
@@ -8007,7 +8310,7 @@
},
"hiddenCard": {
"title": "Hidden achievement",
- "description": "Keep tracking — you might just stumble across it.",
+ "description": "Keep tracking. You might just stumble across it.",
"ariaLabel": "Hidden locked achievement"
},
"hiddenUnlockToast": {
@@ -8016,7 +8319,7 @@
"dashboardCard": {
"title": "Recent unlocks",
"viewAll": "View all",
- "empty": "No achievements yet — keep logging to unlock your first."
+ "empty": "No achievements yet. Keep logging to unlock your first."
}
},
"notifications": {
@@ -8078,9 +8381,6 @@
"enabledToast": "Proactive Coach nudges enabled.",
"disabledToast": "Proactive Coach nudges disabled.",
"saveError": "Could not save. Please try again later.",
- "suggestionsLabel": "Example suggestions",
- "suggestionsDesc": "Show the occasional ready-made example question on the Coach screen.",
- "suggestionsAria": "Toggle example Coach suggestions",
"aiComposedLabel": "Let the Coach phrase the nudge",
"aiComposedDesc": "When a provider is available, the proactive nudge is written by the model instead of the fixed template. Off by default.",
"aiComposedAria": "Toggle AI-composed proactive nudge"
@@ -8151,7 +8451,46 @@
"loginRequired": "Please sign in to manage notifications.",
"someChannelsGloballyDisabled": "One or more channels are turned off in Settings and won’t receive notifications.",
"reminderSettingsLink": "Reminder settings",
- "channelsLink": "Channels"
+ "channelsLink": "Channels",
+ "handover": {
+ "claimedTitle": "{name} now has their own account",
+ "claimedEnd": "{name} took over their health record. Your access has ended.",
+ "claimedRead": "{name} took over their health record. You can still view it, but no longer change it.",
+ "claimedManage": "{name} took over their health record. You can still manage it until they decide otherwise.",
+ "changedTitle": "{name} changed your access",
+ "changedEnd": "Your access to the health record of {name} has ended.",
+ "changedRead": "You can now view the health record of {name}, but not change it.",
+ "changedManage": "You can manage the health record of {name}.",
+ "unnamed": {
+ "claimedTitle": "The person you looked after now has their own account",
+ "claimedEnd": "The person you looked after took over their health record. Your access has ended.",
+ "claimedRead": "The person you looked after took over their health record. You can still view it, but no longer change it.",
+ "claimedManage": "The person you looked after took over their health record. You can still manage it until they decide otherwise.",
+ "changedTitle": "Your access to a health record changed",
+ "changedEnd": "Your access to the health record of the person you looked after has ended.",
+ "changedRead": "You can now view the health record of the person you looked after, but not change it.",
+ "changedManage": "You can manage the health record of the person you looked after."
+ }
+ },
+ "links": {
+ "open": "Open in HealthLog"
+ },
+ "quickActions": {
+ "moodGood": "Good",
+ "moodOkay": "Okay",
+ "moodBad": "Bad"
+ },
+ "content": {
+ "title": "Health figures in notifications",
+ "description": "Whether a notification may show values such as your morning score.",
+ "detail": "Off, a notification only says what it is about, so a lock screen shows no health figures. On, it adds the figures, for example the morning score in the briefing. Tapping a notification opens the right place in the app either way.",
+ "toggleAria": "Show health figures in notifications",
+ "statusDetailed": "Figures shown",
+ "statusGeneric": "Without figures",
+ "detailedToast": "Notifications now include health figures.",
+ "genericToast": "Notifications no longer include health figures.",
+ "saveError": "Could not save. Please try again later."
+ }
},
"i18n": {
"maintainershipBanner": {
@@ -8182,7 +8521,7 @@
}
},
"edit": {
- "title": "Edit target range — {metric}",
+ "title": "Edit target range: {metric}",
"description": "Adjust the in-range band used across charts, insights and the doctor report. Values are in {unit}.",
"derivedHint": "This card is derived from other metrics. Edit the underlying targets (e.g. blood pressure or weight) instead.",
"minLabel": "Min",
@@ -8206,15 +8545,19 @@
"currentValue": "Current: {value} {unit}",
"targetRangeValue": "Target: {min}–{max} {unit}",
"sourceLabel": "Source: {source}",
+ "sourceNames": {
+ "moodEntries": "Mood entries",
+ "sevenDays": "7 days"
+ },
"inTarget": "Within target range",
"aboveTarget": "{delta} {unit} above target",
"belowTarget": "{delta} {unit} below target",
"diastolic": "Diastolic",
- "glucoseFasting": "Glucose — fasting",
- "glucosePostprandial": "Glucose — post-meal",
- "glucoseRandom": "Glucose — random",
- "glucoseBedtime": "Glucose — bedtime",
- "glucoseUnspecified": "Glucose — untagged",
+ "glucoseFasting": "Glucose (fasting)",
+ "glucosePostprandial": "Glucose (post-meal)",
+ "glucoseRandom": "Glucose (random)",
+ "glucoseBedtime": "Glucose (bedtime)",
+ "glucoseUnspecified": "Glucose (untagged)",
"status": {
"underweight": "Underweight",
"normal": "Normal",
@@ -8300,7 +8643,7 @@
"askMedicationName": "Please specify a medication, e.g. taken Ramipril.",
"medicationNotFoundExact": "Medication not found. Please send the name exactly as in HealthLog.",
"helpHeader": "Available commands:",
- "helpBody": "/help — show this help\n/add — record an intake\n/start — start the bot\n\nText commands:\ntaken <name> — confirm an intake\n\nButtons in reminders:\n• Taken — confirm intake\n• 🕐 1h / 🕐 3h — postpone reminder\n• ⏭ Skip — skip intake\n• ✓ Confirm — confirm a missed intake",
+ "helpBody": "/help: show this help\n/add: record an intake\n/start: start the bot\n\nText commands:\ntaken <name>: confirm an intake\n\nButtons in reminders:\n• Taken: confirm intake\n• 🕐 1h / 🕐 3h: postpone reminder\n• ⏭ Skip: skip intake\n• ✓ Confirm: confirm a missed intake",
"measureValuePrompt": "Reply with the value to log it (optional).",
"buttonMeasureValue": "Value…",
"measureValueSaved": "Reading saved.",
@@ -8325,9 +8668,9 @@
},
"lowStockReminders": {
"title": "Low supply: {medName}",
- "bodyToday": "{medName}: {units} units left — less than one day of supply.",
- "bodyReorder": "{medName}: {units} units left. Supply runs out around {runsOutOn} — reorder by {reorderBy}.",
- "bodyLastDose": "{medName}: {units} units left — about your final dose. Supply runs out around {runsOutOn}.",
+ "bodyToday": "{medName}: {units} units left, less than one day of supply.",
+ "bodyReorder": "{medName}: {units} units left. Supply runs out around {runsOutOn}. Reorder by {reorderBy}.",
+ "bodyLastDose": "{medName}: {units} units left, about your final dose. Supply runs out around {runsOutOn}.",
"bodyExpired": "{medName}: no usable supply left. {units} units are marked expired.",
"bodyAsNeeded": "{medName}: only {units} units left. Reorder before you need the next dose."
},
@@ -8354,11 +8697,11 @@
"questionConditions": "Do you have any chronic conditions the Coach should know about?",
"questionFocus": "Is there anything specific the Coach should pay attention to?",
"questionAllergies": "Do you have any allergies or intolerances?",
- "questionAboutMe": "Want to tell the Coach a bit about yourself — routines, goals, context?"
+ "questionAboutMe": "Want to tell the Coach a bit about yourself: routines, goals, context?"
},
"clinicianView": {
"title": "Shared health record",
- "period": "Reporting period: {start} – {end}",
+ "period": "Reporting period: {start}–{end}",
"expires": "This link expires on {date}.",
"provenance": "This is a read-only summary shared by the person it belongs to, exported from their self-tracked HealthLog data. Values are self-recorded and may include device-synced readings; treat them as patient-reported, not as a clinical examination.",
"labs": "Lab values",
@@ -8382,7 +8725,7 @@
"strain": "Strain score",
"score": "Wellness score"
},
- "footer": "Shared from HealthLog — a self-hosted personal health tracker.",
+ "footer": "Shared from HealthLog, a self-hosted personal health tracker.",
"unlock": {
"title": "Enter passphrase",
"description": "This shared health record is protected. Enter the passphrase you were given to view it.",
@@ -8417,7 +8760,11 @@
},
"course": "Course {n}",
"courseRange": "{start} to {end}",
- "courseSince": "since {start}"
+ "courseSince": "since {start}",
+ "unavailable": {
+ "title": "Link expired or invalid",
+ "description": "This share link has expired, was revoked, or is incomplete. Ask the person who shared it for a new link."
+ }
},
"cycle": {
"symptomCategory": {
@@ -8466,11 +8813,11 @@
"generic": "Your {metric} runs about {delta} {unit} {dir} in your luteal phase than your follicular phase.",
"dirHigher": "higher",
"dirLower": "lower",
- "empty": "Not enough cycles and readings yet to surface a phase pattern. Keep logging — this fills in as your history grows."
+ "empty": "Not enough cycles and readings yet to surface a phase pattern. Keep logging; this fills in as your history grows."
},
"crosstab": {
"description": "How each measurement compares between your luteal and follicular phases, on your own data.",
- "pairLabel": "{metric} · luteal vs follicular",
+ "pairLabel": "{metric}: luteal vs follicular",
"detail": "Luteal {lutealAvg} {unit} ({lutealDays} days) vs follicular {follicularAvg} {unit} ({follicularDays} days).",
"metricRestingHeartRate": "Resting heart rate",
"metricHrv": "Heart-rate variability",
@@ -8506,7 +8853,7 @@
},
"context": {
"MENSTRUAL": "Hormone levels are at their lowest, which is why these days can feel like a quiet reset. Some people notice cramps, lighter sleep or a wish to slow down; others sail through with little change. All of that sits within the normal range, and how it feels can shift from one cycle to the next.",
- "FOLLICULAR": "With oestrogen on the rise, many people describe these days as bright and open — sleep often comes more easily and the mind feels clearer. Basal body temperature usually stays in its lower band until just before ovulation. The length of this phase varies most from person to person, and from cycle to cycle.",
+ "FOLLICULAR": "With oestrogen on the rise, many people describe these days as bright and open: sleep often comes more easily and the mind feels clearer. Basal body temperature usually stays in its lower band until just before ovulation. The length of this phase varies most from person to person, and from cycle to cycle.",
"OVULATORY": "Oestrogen peaks just before the egg is released, often alongside a brief lift in mood and a small rise in basal body temperature in the day or two that follow. Cervical fluid tends to become clearer and more stretchy around this window. A short twinge on one side is something some people notice; many feel nothing at all, and both are perfectly normal.",
"LUTEAL": "Progesterone leads the second half and basal body temperature usually settles a little higher than earlier in the cycle. Some people notice shifts in sleep, appetite, mood or energy as the days go on, while others feel steady throughout. The pattern is personal, and it often looks different from one month to the next."
},
@@ -8593,8 +8940,8 @@
"ariaUnknown": "No active cycle yet",
"caption": "of your cycle",
"firstPeriodCta": "Log my first period",
- "periodOverdue": "Your period is later than usual — the day count pauses until the next period is logged.",
- "periodOverdueDays": "Your period is {days} days later than usual — the day count pauses until the next period is logged.",
+ "periodOverdue": "Your period is later than usual. The day count pauses until the next period is logged.",
+ "periodOverdueDays": "Your period is {days} days later than usual. The day count pauses until the next period is logged.",
"logPeriodCta": "Log period"
},
"calendar": {
@@ -8606,7 +8953,7 @@
"prevMonth": "Previous month",
"nextMonth": "Next month",
"loading": "Loading your calendar…",
- "learning": "Learning your cycle — log at least three full cycles and the fertile window, ovulation and phases fill in. Until then the calendar shows only what you've logged.",
+ "learning": "Learning your cycle. Log at least three full cycles and the fertile window, ovulation and phases fill in. Until then the calendar shows only what you've logged.",
"legendSymptom": "Symptoms",
"legendOvulationConfirmed": "Confirmed ovulation",
"flowSpotting": "spotting",
@@ -8619,7 +8966,7 @@
"predictions": {
"title": "What comes next",
"nextPeriod": "Next period",
- "nextPeriodWindow": "{low} – {high}",
+ "nextPeriodWindow": "{low}–{high}",
"fertileWindow": "Fertile window",
"ovulation": "Estimated ovulation",
"confidenceHigh": "High",
@@ -8628,7 +8975,7 @@
"stillLearning": "Still learning",
"stillLearningBody": "Log at least three full cycles and your predictions sharpen. Until then this is a rough estimate.",
"rawMode": "Prediction is off",
- "rawModeBody": "You turned predictions off — only your logged data is shown, with no forecast.",
+ "rawModeBody": "You turned predictions off. Only your logged data is shown, with no forecast.",
"none": "No prediction yet. Log a period to get started.",
"ovulationConfirmed": "Ovulation (confirmed)"
},
@@ -8661,7 +9008,7 @@
"symptoms": "Symptoms",
"temperature": "Basal body temperature",
"temperatureDisturbed": "Mark reading as disturbed",
- "temperatureDisturbedHint": "Fever, illness, alcohol or a late measurement — excluded from the temperature evaluation.",
+ "temperatureDisturbedHint": "Fever, illness, alcohol or a late measurement: excluded from the temperature evaluation.",
"ovulationTest": "Ovulation test",
"mucus": "Cervical mucus",
"cervix": "Cervix",
@@ -8690,7 +9037,7 @@
"temperatureSection": "Temperature & ovulation",
"intimacySection": "Intimacy & contraception",
"testsSection": "Tests",
- "phaseDayHeader": "Day {day} · {phase}",
+ "phaseDayHeader": "Day {day}, {phase}",
"bbtHint": "Take it first thing in the morning, before getting up.",
"titleForDate": "Log for {date}",
"descriptionToday": "Today. Flow, symptoms and notes.",
@@ -8729,7 +9076,7 @@
"remindersDescription": "Turn period reminders on per channel in notification settings.",
"remindersLink": "Notification settings",
"fertileReminder": "Fertile-window reminder",
- "fertileReminderDescription": "Get a heads-up a few days before your estimated fertile window. Off by default — turn it on per channel in notification settings."
+ "fertileReminderDescription": "Get a heads-up a few days before your estimated fertile window. Off by default; turn it on per channel in notification settings."
},
"contraceptive": {
"NONE": "None",
@@ -8757,15 +9104,15 @@
"fieldInfo": {
"bbt": "Your resting temperature, taken first thing in the morning before getting up. It rises slightly (about 0.2–0.3 °C) after ovulation.",
"bbtLabel": "What is basal body temperature?",
- "mucus": "Vaginal discharge changes across the cycle — from dry or sticky to creamy, then watery and egg-white-like around the most fertile days.",
+ "mucus": "Vaginal discharge changes across the cycle, from dry or sticky to creamy, then watery and egg-white-like around the most fertile days.",
"mucusLabel": "What is cervical mucus?",
- "ovulationTest": "A home urine test that detects the LH surge — the hormone spike about 24–36 h before ovulation. A positive flags the most fertile window.",
+ "ovulationTest": "A home urine test that detects the LH surge, the hormone spike about 24–36 h before ovulation. A positive flags the most fertile window.",
"ovulationTestLabel": "What is an ovulation test?",
"spotting": "Light bleeding outside the period.",
"spottingLabel": "What is spotting between periods?",
"pregnancyTest": "Result of a home pregnancy (hCG) test.",
"pregnancyTestLabel": "What is a pregnancy test?",
- "progesteroneTest": "A test confirming ovulation has occurred — progesterone rises afterward.",
+ "progesteroneTest": "A test confirming ovulation has occurred; progesterone rises afterward.",
"progesteroneTestLabel": "What is a progesterone test?",
"intercourse": "Whether sexual intercourse took place on this day.",
"intercourseLabel": "What does intercourse track?",
@@ -8818,14 +9165,14 @@
"postpone": {
"menuItem": "Postpone…",
"title": "Postpone due date",
- "description": "Only moves the current due date — the regular rhythm stays unchanged.",
+ "description": "Only moves the current due date; the regular rhythm stays unchanged.",
"chip": "+{days} days",
"dateLabel": "New date",
"confirm": "Postpone"
},
"skip": {
"heading": "Skip this due date",
- "explainer": "Recorded honestly as skipped — not as done. The interval restarts from today.",
+ "explainer": "Recorded honestly as skipped, not as done. The interval restarts from today.",
"action": "Skip",
"confirmTitle": "Skip this due date?"
},
@@ -8867,10 +9214,10 @@
"VISCERAL_FAT": "Visceral fat",
"BODY_MASS_INDEX": "Body mass index (BMI)",
"WAIST_CIRCUMFERENCE": "Waist circumference",
- "PHQ9_SCORE": "PHQ-9 · mood",
- "GAD7_SCORE": "GAD-7 · worry",
- "WHO5_SCORE": "WHO-5 · wellbeing",
- "SCI_SCORE": "SCI · sleep"
+ "PHQ9_SCORE": "PHQ-9 (mood)",
+ "GAD7_SCORE": "GAD-7 (worry)",
+ "WHO5_SCORE": "WHO-5 (wellbeing)",
+ "SCI_SCORE": "SCI (sleep)"
},
"typeGroups": {
"vitals": "Vitals",
@@ -8966,7 +9313,7 @@
"localOcr": {
"heading": "Read scans on this device",
"description": "Reads the photo on your device and sends only the extracted text to your provider.",
- "detail": "Useful when your AI provider cannot read images. Less accurate — review every value before saving."
+ "detail": "Useful when your AI provider cannot read images. Less accurate: review every value before saving."
},
"sort": {
"heading": "Sort order",
@@ -9010,7 +9357,7 @@
"sourceRangePlaceholder": "e.g. 3.5 - 5.0 or < 5",
"sourceRangeAdd": "+ Add the range from the report",
"sourceRangeHint": "Optional. Applies to this reading only; your saved range stays as it is.",
- "sourceRangeUnparsed": "Kept as written — no numeric range read from it."
+ "sourceRangeUnparsed": "Kept as written, no numeric range read from it."
},
"backToLabs": "Back to lab results",
"hasNote": "Note",
@@ -9028,7 +9375,7 @@
"emptyDescription": "Add a value to start tracking this biomarker over time.",
"title": "Biomarker",
"genericDescription": "A lab biomarker you track. Add readings to follow how it trends against its reference range.",
- "valuesTitle": "{marker} — all readings",
+ "valuesTitle": "{marker}: all readings",
"valuesDescription": "Every reading for this biomarker. Edit or remove an entry directly.",
"backToMarker": "Back to {marker}",
"latestResult": "Latest result"
@@ -9048,7 +9395,7 @@
},
"biomarker": {
"manageTitle": "Biomarkers",
- "managerDescription": "Define a marker once — its unit and range are reused for every reading.",
+ "managerDescription": "Define a marker once; its unit and range are reused for every reading.",
"define": "Define",
"defineFirst": "Define your first biomarker",
"defineTitle": "Define biomarker",
@@ -9067,7 +9414,7 @@
"form": {
"seedLabel": "Start from a common marker",
"seedPlaceholder": "Choose a marker (optional)",
- "seedHint": "Picking one fills the fields below — adjust the range to match your lab.",
+ "seedHint": "Picking one fills the fields below. Adjust the range to match your lab.",
"name": "Name",
"namePlaceholder": "e.g. LDL cholesterol",
"unit": "Unit",
@@ -9081,7 +9428,7 @@
"upperPlaceholder": "Upper bound",
"context": "Context",
"contextPlaceholder": "Optional note about this marker",
- "rangeHint": "Either bound is optional — leave one blank for an open-ended range (e.g. LDL < 116).",
+ "rangeHint": "Either bound is optional. Leave one blank for an open-ended range (e.g. LDL < 116).",
"requiredError": "Name and unit are required.",
"rangeOrderError": "The lower bound must not exceed the upper bound.",
"save": "Save biomarker",
@@ -9137,7 +9484,7 @@
"bloodCount": "Blood count"
},
"desc": {
- "total-cholesterol": "Total cholesterol is the sum of the cholesterol carried across all lipoprotein particles in the blood, including the LDL, HDL, and remnant fractions. Cholesterol itself is an essential building block for cell membranes, hormones, and bile acids. As a single number it is a coarse summary, because both a healthy HDL share and a high LDL share lift the total — so it is usually read alongside the individual fractions. It is tracked mainly as part of long-term cardiovascular risk assessment.",
+ "total-cholesterol": "Total cholesterol is the sum of the cholesterol carried across all lipoprotein particles in the blood, including the LDL, HDL, and remnant fractions. Cholesterol itself is an essential building block for cell membranes, hormones, and bile acids. As a single number it is a coarse summary, because both a healthy HDL share and a high LDL share lift the total, so it is usually read alongside the individual fractions. It is tracked mainly as part of long-term cardiovascular risk assessment.",
"ldl": "LDL cholesterol is the cholesterol carried by low-density lipoprotein particles, the fraction that delivers cholesterol to tissues but can also deposit it in artery walls. Over years this deposition contributes to atherosclerotic plaque, which is why LDL is one of the most closely watched lipid markers. Lower long-term exposure generally tracks with lower cardiovascular risk. It is interpreted by risk category rather than against one fixed normal value.",
"hdl": "HDL cholesterol is the cholesterol carried by high-density lipoprotein particles, which collect surplus cholesterol from tissues and return it to the liver for recycling or removal. Because of this transport role it is often described as the protective fraction. Very low levels tend to accompany higher cardiovascular risk, though raising it artificially has not reliably reduced risk, so it is read as a marker rather than a target. It is usually interpreted together with LDL and triglycerides.",
"triglycerides": "Triglycerides are the main form of fat circulating in the blood, supplied both by the diet and produced by the liver, and they serve as a key energy store. Levels rise after meals, so the sample is usually taken fasting for comparability. Persistently high triglycerides are associated with metabolic and cardiovascular risk and often travel with insulin resistance and low HDL. They are read as one component of the overall lipid picture.",
@@ -9146,27 +9493,27 @@
"tsh": "TSH (thyroid-stimulating hormone) is released by the pituitary gland to signal the thyroid to produce its hormones, and it adjusts up or down to keep them in balance. Because of this feedback loop, TSH is the most sensitive first-line screen of thyroid function: it tends to move before the thyroid hormones themselves drift. A raised or lowered TSH points toward an under- or over-active thyroid and usually prompts measurement of the thyroid hormones directly. Pregnancy and age shift the expected range.",
"ft3": "Free T3 (triiodothyronine) is the unbound, biologically active form of the more potent thyroid hormone, the fraction available to act on tissues. Most circulating T3 is bound to carrier proteins and inactive, so the free portion is what reflects true hormone availability. It is measured to characterise thyroid function, especially alongside TSH and free T4 when results are unclear. Levels are strongly assay-dependent and read in clinical context.",
"ft4": "Free T4 (thyroxine) is the unbound form of the main hormone the thyroid releases into the blood, serving as a reservoir that tissues convert to the more active T3 as needed. The free fraction reflects what is actually available to the body, independent of carrier-protein levels. It is usually measured together with TSH to separate thyroid-gland problems from pituitary signalling. Pregnancy and certain medications can shift it.",
- "ferritin": "Ferritin is the body's main iron-storage protein, and its blood level broadly mirrors total iron reserves. A low value points toward depleted stores, while a high value can reflect iron overload — or simply inflammation, because ferritin also rises as an acute-phase reactant. For that reason it is often interpreted alongside other iron markers and signs of inflammation. It is one of the core measures used to assess iron status.",
- "transferrin-saturation": "Transferrin saturation is the percentage of transferrin — the protein that ferries iron through the blood — that is currently loaded with iron. It links the amount of iron in transit to the body's iron-carrying capacity, giving a snapshot of iron availability that complements ferritin's view of storage. A low saturation suggests iron is in short supply, while a high value can indicate overload. It is read together with ferritin and the full iron panel.",
+ "ferritin": "Ferritin is the body's main iron-storage protein, and its blood level broadly mirrors total iron reserves. A low value points toward depleted stores, while a high value can reflect iron overload, or simply inflammation, because ferritin also rises as an acute-phase reactant. For that reason it is often interpreted alongside other iron markers and signs of inflammation. It is one of the core measures used to assess iron status.",
+ "transferrin-saturation": "Transferrin saturation is the percentage of transferrin (the protein that ferries iron through the blood) that is currently loaded with iron. It links the amount of iron in transit to the body's iron-carrying capacity, giving a snapshot of iron availability that complements ferritin's view of storage. A low saturation suggests iron is in short supply, while a high value can indicate overload. It is read together with ferritin and the full iron panel.",
"vitamin-d": "Vitamin D is measured as 25-hydroxyvitamin D, the circulating storage form and the standard marker of overall vitamin D status. The body makes it in skin exposed to sunlight and absorbs some from food, and it supports calcium balance, bone health, and immune function. Levels vary with season, latitude, skin tone, and supplementation. It is tracked to gauge whether stores are adequate over time.",
- "vitamin-b12": "Vitamin B12 (cobalamin) is an essential vitamin obtained mainly from animal-source foods and stored largely in the liver. It is required for red-blood-cell formation, DNA synthesis, and the health of the nervous system. Prolonged shortfall — from low intake or impaired absorption — can lead to anaemia and nerve-related symptoms. It is measured to assess nutritional status, particularly where intake or absorption may be limited.",
+ "vitamin-b12": "Vitamin B12 (cobalamin) is an essential vitamin obtained mainly from animal-source foods and stored largely in the liver. It is required for red-blood-cell formation, DNA synthesis, and the health of the nervous system. Prolonged shortfall, from low intake or impaired absorption, can lead to anaemia and nerve-related symptoms. It is measured to assess nutritional status, particularly where intake or absorption may be limited.",
"folate": "Folate (vitamin B9) is a B-vitamin found in leafy greens, legumes, and fortified foods, central to red-blood-cell formation and DNA synthesis. It works closely with vitamin B12, and a shortfall in either can produce a similar type of anaemia. Adequate folate is especially important around early pregnancy for healthy fetal development. It is measured to assess nutritional status and the cause of certain anaemias.",
"hs-crp": "High-sensitivity CRP measures C-reactive protein, a marker the liver releases in response to inflammation, using an assay refined to detect the low concentrations relevant to cardiovascular risk. Unlike standard CRP used for acute infection, the high-sensitivity version captures the subtle, chronic inflammation linked to vascular disease. Because any recent infection or injury can raise it, it is interpreted away from acute illness and often confirmed with a repeat sample. It is read as one contributor to overall cardiovascular risk.",
"creatinine": "Creatinine is a waste product of normal muscle metabolism that the kidneys filter out of the blood at a fairly steady rate. Because production is relatively constant, its blood level serves as a practical gauge of how well the kidneys are clearing waste. Values are also influenced by muscle mass, hydration, and diet, so it is usually interpreted alongside the estimated filtration rate. It is a core marker of kidney function.",
"egfr": "eGFR (estimated glomerular filtration rate) estimates how much blood the kidneys filter each minute, calculated from creatinine together with age and sex. It translates a raw creatinine value into a more directly interpretable measure of kidney function. A declining eGFR over time is the main way chronic kidney changes are staged and tracked. As an estimate it is read as a trend rather than a single exact figure.",
"alt": "ALT (alanine aminotransferase) is an enzyme concentrated in liver cells, released into the blood when those cells are stressed or damaged. Because it is relatively liver-specific, it is one of the primary markers used to detect liver injury or inflammation. Levels can rise with fatty liver, viral hepatitis, alcohol, or certain medications. It is usually read together with AST and the other liver-panel markers.",
- "ast": "AST (aspartate aminotransferase) is an enzyme found in the liver but also in muscle, heart, and red blood cells, released when any of those tissues is injured. Because it is less liver-specific than ALT, the two are interpreted together — their ratio can hint at the source and nature of the problem. Vigorous exercise or muscle injury can raise it independently of the liver. It is one of the standard liver-panel enzymes.",
- "ggt": "GGT (gamma-glutamyl transferase) is an enzyme found in the liver and the bile ducts that rises particularly when bile flow is obstructed or with alcohol exposure. It is often used to clarify why another liver marker is elevated — for example, to distinguish a liver or bile-duct source. It is sensitive but not very specific, so it is read in context with the rest of the liver panel. It is one of several markers used to assess liver and biliary health.",
+ "ast": "AST (aspartate aminotransferase) is an enzyme found in the liver but also in muscle, heart, and red blood cells, released when any of those tissues is injured. Because it is less liver-specific than ALT, the two are interpreted together; their ratio can hint at the source and nature of the problem. Vigorous exercise or muscle injury can raise it independently of the liver. It is one of the standard liver-panel enzymes.",
+ "ggt": "GGT (gamma-glutamyl transferase) is an enzyme found in the liver and the bile ducts that rises particularly when bile flow is obstructed or with alcohol exposure. It is often used to clarify why another liver marker is elevated, for example, to distinguish a liver or bile-duct source. It is sensitive but not very specific, so it is read in context with the rest of the liver panel. It is one of several markers used to assess liver and biliary health.",
"sodium": "Sodium is the principal electrolyte in the fluid outside the body's cells and the main driver of fluid balance and blood volume. The body regulates it tightly through thirst, hormones, and the kidneys, so its blood level reflects the balance between water and salt more than dietary intake alone. Abnormal values usually signal a disturbance of water balance rather than of sodium itself. It is a core part of the basic metabolic and electrolyte panel.",
"potassium": "Potassium is the main electrolyte inside the body's cells and is essential for nerve signalling and the steady rhythm of the heart muscle. Its blood level is held within a narrow range by the kidneys and hormonal signals, because both high and low values can disturb the heart's electrical activity. Levels can be affected by kidney function, hydration, and several common medications. It is a core measure in the electrolyte panel.",
"hemoglobin": "Haemoglobin is the iron-containing protein in red blood cells that binds oxygen in the lungs and carries it to the tissues. Its concentration is the main measure of the blood's oxygen-carrying capacity and the central marker for detecting anaemia. Low values can reflect blood loss, iron or vitamin shortfalls, or reduced production, while high values can follow dehydration or low-oxygen conditions. It is a cornerstone of the complete blood count.",
"hematocrit": "Haematocrit is the proportion of blood volume made up of red blood cells, expressed as a percentage. It moves closely with haemoglobin and gives a complementary view of red-cell mass and the blood's oxygen-carrying capacity. Hydration affects it, since plasma-volume changes shift the proportion even when red-cell numbers are stable. It is read alongside haemoglobin and the rest of the blood count.",
"wbc": "The white blood cell count is the total number of immune cells circulating in the blood, the body's mobile defence against infection. It rises in response to infection, inflammation, or stress and can fall when the bone marrow is suppressed or in some immune conditions. Because it sums several distinct cell types, a notable change is often followed up with a differential that breaks it down. It is a core component of the complete blood count.",
"platelets": "Platelets are small cell fragments in the blood that clump together and form the initial plug that stops bleeding from a damaged vessel. Their count reflects the balance between production in the bone marrow and consumption or breakdown. A low count can raise bleeding tendency, while a high count can follow inflammation or other stimuli. It is part of the complete blood count and a basic gauge of clotting capacity.",
- "apob": "Apolipoprotein B is the structural protein carried by each of the atherogenic lipoprotein particles, so measuring it counts those particles directly rather than the cholesterol they carry. Because one ApoB molecule sits on each LDL, VLDL, and related particle, it can capture cardiovascular risk that an LDL-cholesterol value misses — particularly when particle number and cholesterol content diverge. For this reason it is increasingly used to refine lipid-based risk assessment. It is interpreted alongside the standard lipid panel.",
+ "apob": "Apolipoprotein B is the structural protein carried by each of the atherogenic lipoprotein particles, so measuring it counts those particles directly rather than the cholesterol they carry. Because one ApoB molecule sits on each LDL, VLDL, and related particle, it can capture cardiovascular risk that an LDL-cholesterol value misses, particularly when particle number and cholesterol content diverge. For this reason it is increasingly used to refine lipid-based risk assessment. It is interpreted alongside the standard lipid panel.",
"lp-a": "Lipoprotein(a) is an LDL-like particle with an extra attached protein, and its level is largely set by genetics and stays fairly stable through life. Elevated Lp(a) is an independent, inherited risk factor for cardiovascular disease and aortic-valve narrowing. Because it changes little, it is usually measured once to establish a person's lifelong risk rather than tracked repeatedly. It is reported in molar units and read as part of overall cardiovascular risk.",
- "fasting-insulin": "Fasting insulin is the level of insulin — the hormone that moves glucose from the blood into cells — measured after an overnight fast. Paired with fasting glucose, it helps estimate insulin resistance, the state in which the body needs more insulin to keep glucose in range. A high fasting insulin alongside normal glucose can be an early sign that this compensation is under way. It is read as part of a metabolic-health picture rather than in isolation.",
- "omega-3-index": "The omega-3 index is the share of EPA and DHA — the two long-chain marine omega-3 fatty acids — among the fatty acids in red-blood-cell membranes. Because red cells turn over slowly, it reflects long-term omega-3 intake rather than a recent meal. A higher index has been associated in observational studies with lower cardiovascular risk. It is tracked as a stable marker of omega-3 status that responds to diet over months."
+ "fasting-insulin": "Fasting insulin is the level of insulin (the hormone that moves glucose from the blood into cells) measured after an overnight fast. Paired with fasting glucose, it helps estimate insulin resistance, the state in which the body needs more insulin to keep glucose in range. A high fasting insulin alongside normal glucose can be an early sign that this compensation is under way. It is read as part of a metabolic-health picture rather than in isolation.",
+ "omega-3-index": "The omega-3 index is the share of EPA and DHA (the two long-chain marine omega-3 fatty acids) among the fatty acids in red-blood-cell membranes. Because red cells turn over slowly, it reflects long-term omega-3 intake rather than a recent meal. A higher index has been associated in observational studies with lower cardiovascular risk. It is tracked as a stable marker of omega-3 status that responds to diet over months."
},
"apob": "Apolipoprotein B (ApoB)",
"lp-a": "Lipoprotein(a)",
@@ -9184,7 +9531,7 @@
"acceptImageOnly": "JPEG, PNG, or WebP",
"extracting": "Reading your report…",
"readingOnDevice": "Reading on your device…",
- "localModeHint": "Read on your device, then sent as text. Less accurate — review every value.",
+ "localModeHint": "Read on your device, then sent as text. Less accurate: review every value.",
"rangeFromScan": "Reference range taken from the scan",
"rangeVerify": "Verify the reference range",
"reviewTitle": "Review before saving",
@@ -9199,12 +9546,12 @@
"dateLabel": "Date",
"refLowLabel": "Reference low",
"refHighLabel": "Reference high",
- "newBiomarker": "New biomarker — will be created",
+ "newBiomarker": "New biomarker, will be created",
"linksExisting": "Links to existing {name}",
"duplicateWarning": "A matching reading already exists",
"lowConfidence": "Please double-check this value",
"lowConfidenceBadge": "Low confidence",
- "valueUnreadable": "Value unreadable — enter it manually",
+ "valueUnreadable": "Value unreadable. Enter it manually",
"saveSelected": "Save {count} selected",
"discardAll": "Discard all",
"savedToast": "{count} lab results saved",
@@ -9320,6 +9667,10 @@
"vaccinations": {
"label": "Vaccinations",
"description": "The vaccination record, structured: dose, series, next booster."
+ },
+ "timeline": {
+ "label": "Timeline",
+ "description": "Years at a glance: conditions, medications and visits beside your readings, plus your own life events."
}
},
"moduleOff": {
@@ -9351,8 +9702,8 @@
},
"form": {
"date": "Date",
- "catalogLabel": "Vaccine (catalogue)",
- "identityHint": "Pick from the catalogue or type it in below — either is enough.",
+ "catalogLabel": "Vaccine",
+ "identityHint": "Pick a vaccine from the list or type it in below. Either is enough.",
"catalogNone": "Choose a vaccine",
"catalogSearch": "Search vaccines",
"catalogNoMatch": "No catalogue entry found",
@@ -9429,7 +9780,7 @@
},
"empty": {
"title": "No vaccination logged yet",
- "description": "Transcribe your vaccination card line by line — a date and the vaccine are enough."
+ "description": "Transcribe your vaccination card line by line. A date and the vaccine are enough."
},
"catalog": {
"tetanus": "Tetanus",
@@ -9472,10 +9823,31 @@
"overdue": "Booster overdue since {date}",
"dueSoon": "Booster due {date}",
"current": "Next booster {date}"
+ },
+ "custom": {
+ "title": "Own vaccines",
+ "add": "Add own vaccine",
+ "name": "Name",
+ "components": "Protects against",
+ "seriesDoses": "Doses in the primary series",
+ "boosterMonths": "Booster every (months)",
+ "saved": "Vaccine saved",
+ "deleted": "Vaccine removed",
+ "deleteConfirm": "Remove this vaccine? Doses already logged keep their name.",
+ "saveFailed": "The vaccine could not be saved.",
+ "nameTaken": "You already have a vaccine with this name.",
+ "editTitle": "Edit own vaccine",
+ "description": "A vaccine the catalogue does not list, counted like any catalogue entry.",
+ "componentsHint": "Choose every disease it protects against.",
+ "boosterHint": "Leave empty if there is no booster.",
+ "protectsAgainst": "Protects against {list}.",
+ "deleteTitle": "Remove own vaccine?",
+ "sectionDescription": "Vaccines you added yourself because the catalogue does not list them.",
+ "catalogGroup": "Catalogue"
}
},
"illness": {
- "title": "Illness journal",
+ "title": "Illness",
"subtitle": "A retrospective log of your illness and condition episodes.",
"settings": {
"viewDescription": "Choose whether episodes show as cards or as a compact list.",
@@ -9562,7 +9934,7 @@
},
"correlation": {
"title": "How it unfolded",
- "learning": "Still learning — log a few more days to see patterns here.",
+ "learning": "Still learning. Log a few more days to see patterns here.",
"preOnsetTitle": "How it announced itself",
"preOnsetBody": "Signals that drifted from your baseline before you marked the onset.",
"nadirTitle": "What changed",
@@ -9589,7 +9961,7 @@
"countOne": "You logged 1 episode in this period.",
"countOther": "You logged {count} episodes in this period.",
"typicalGap": "Typical recovery gap: {days} days between feeling better and your numbers settling.",
- "typicalGapWithDriver": "Typical recovery gap: {days} days — you tend to feel better before your {vital} settles.",
+ "typicalGapWithDriver": "Typical recovery gap: {days} days; you tend to feel better before your {vital} settles.",
"typicalGapLearning": "A few more resolved episodes and a typical recovery gap will appear here.",
"none": "No episodes logged in this period.",
"analyze": "Analyse the recovery gap"
@@ -9650,8 +10022,8 @@
"stop": "I measure enough",
"accepted": "Reminder set",
"dismissed": "Okay, I won't suggest this again",
- "stopped": "Got it — I'll stop suggesting reminders",
- "failed": "Couldn't set that up — please try again",
+ "stopped": "Got it, I'll stop suggesting reminders",
+ "failed": "Couldn't set that up. Please try again",
"cadence": {
"weightDaily": "Weigh yourself daily, around the same time",
"bp722": "Measure your blood pressure twice a day for a week",
@@ -9663,7 +10035,7 @@
"dismiss": "Not now",
"applied": "Added",
"dismissed": "Dismissed",
- "failed": "Couldn't add that — please try again",
+ "failed": "Couldn't add that. Please try again",
"checkup": {
"title": "Add a check-up reminder?"
},
@@ -9691,9 +10063,9 @@
"reviewPrefix": "Check-in: {when}",
"accept": "Accept plan",
"decline": "Decline",
- "accepted": "Plan accepted — the Coach will keep it in mind.",
+ "accepted": "Plan accepted. The Coach will keep it in mind.",
"declined": "Proposal declined.",
- "failed": "That didn't work — please try again.",
+ "failed": "That didn't work. Please try again.",
"markMet": "Goal met",
"abandon": "End plan",
"remove": "Remove",
@@ -9714,8 +10086,8 @@
}
},
"refusal": {
- "outOfScope": "I can only help with the health metrics in your log — measurements, medications, mood, trends. I cannot answer questions outside that scope.",
- "promptInjection": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data — please rephrase your question in those terms.",
+ "outOfScope": "I can only help with the health metrics in your log: measurements, medications, mood, trends. I cannot answer questions outside that scope.",
+ "promptInjection": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data. Please rephrase your question in those terms.",
"conversationPoisoned": "An earlier message in this conversation contains wording that overrides my instructions. Please start a new conversation."
},
"noRecordedData": "I don't have any recorded readings for what you asked about, so there is nothing I can give you a figure from. Once you log some, ask me again and I'll work from your own numbers.",
@@ -9726,9 +10098,6 @@
"showResult": "Opening an earlier table",
"reuse": "Using the table from before",
"headerActive": "{label}…",
- "headerDoneOne": "Looked at 1 source",
- "headerDoneFew": "Looked at {count} sources",
- "headerDoneOther": "Looked at {count} sources",
"listLabel": "What the Coach looked at",
"toggleShow": "Show what the Coach looked at",
"toggleHide": "Hide what the Coach looked at",
@@ -9754,7 +10123,9 @@
"illness": "Illness and recovery",
"cycle": "Cycle",
"correlations": "Connections between metrics",
- "snapshot": "Record summary"
+ "snapshot": "Record summary",
+ "environment": "Weather and air",
+ "day": "One day"
},
"window": {
"last7days": "last 7 days",
@@ -9878,7 +10249,8 @@
"relatedMetric": "See also: {metric}",
"continue": "Keep looking",
"settingLabel": "Suggest follow-up questions",
- "settingHint": "Show up to three follow-up suggestions under the Coach's latest answer."
+ "settingHint": "Show up to three follow-up suggestions under the Coach's latest answer.",
+ "changeAssumption": "Instead: {value}"
},
"clarify": {
"cardLabel": "The Coach has a question",
@@ -9891,6 +10263,12 @@
"last90days": "Last 90 days",
"lastYear": "Last 12 months",
"allTime": "All time"
+ },
+ "comparison": {
+ "baseline90d": "Compare with my usual 90 days"
+ },
+ "anchor": {
+ "illness": "Since the illness of {date}"
}
},
"reuse": {
@@ -9925,7 +10303,7 @@
},
"mentalHealth": {
"pageTitle": "Mental wellbeing",
- "pageDescription": "Four validated, voluntary self-checks — the trend over time says more than any single score.",
+ "pageDescription": "Four validated, voluntary self-checks. The trend over time says more than any single score.",
"attributionLabel": "Attribution",
"attributionShow": "Source and licence for {instrument}",
"choosePrompt": "Choose a check-in",
@@ -9936,10 +10314,10 @@
"saved": "Saved",
"error": "Something went wrong. Please try again.",
"instrument": {
- "phq9": "PHQ-9 · mood",
- "gad7": "GAD-7 · worry",
- "who5": "WHO-5 · well-being",
- "sci": "SCI · sleep"
+ "phq9": "PHQ-9 (mood)",
+ "gad7": "GAD-7 (worry)",
+ "who5": "WHO-5 (well-being)",
+ "sci": "SCI (sleep)"
},
"instrumentSub": {
"phq9": "Depression screen, 9 questions",
@@ -9960,7 +10338,7 @@
"2": "Very difficult",
"3": "Extremely difficult"
},
- "functionalOptionalHint": "Optional — you can submit without answering this question.",
+ "functionalOptionalHint": "Optional: you can submit without answering this question.",
"result": {
"title": "Your result",
"totalLabel": "Total score",
@@ -10150,7 +10528,7 @@
},
"openDetail": "Show details",
"historyLoadError": "Your assessment history could not be loaded",
- "historyLoadErrorHint": "This does not mean you have no results — the list could not be fetched. Try again.",
+ "historyLoadErrorHint": "This does not mean you have no results; the list could not be fetched. Try again.",
"privacyNote": "Your answers are stored encrypted and never reach the AI Coach."
},
"records": {
@@ -10363,7 +10741,7 @@
"UNKNOWN": "Not stated"
},
"contactsPlaceholder": "Name and phone number of who to call",
- "implantsPlaceholder": "e.g. pacemaker, insulin pump, metal implant",
+ "implantsPlaceholder": "E.g. pacemaker, insulin pump, metal implant",
"notePlaceholder": "Anything a clinician should know in an emergency",
"reportHint": "This appears on the first page of your doctor report.",
"save": "Save",
@@ -10380,12 +10758,34 @@
"daylight": "Daylight",
"precip": "Precipitation",
"pressureMean": "Barometric pressure",
- "pressureDelta": "Pressure swing"
+ "pressureDelta": "Pressure swing",
+ "pm25": "Fine particles (PM2.5)",
+ "pm10": "Particles (PM10)",
+ "no2": "Nitrogen dioxide",
+ "ozone8h": "Ozone (8-hour high)",
+ "europeanAqi": "European air quality index",
+ "uvIndexMax": "UV index (high)",
+ "dustMax": "Saharan dust",
+ "pollenMax": "Pollen (highest)"
+ },
+ "chip": {
+ "day": "Weather on {date}",
+ "pollenHigh": "High pollen: {kinds}",
+ "hotNight": "Warm night",
+ "veryPoorAir": "Very poor air"
+ },
+ "pollen": {
+ "alder": "alder",
+ "birch": "birch",
+ "grass": "grass",
+ "mugwort": "mugwort",
+ "olive": "olive",
+ "ragweed": "ragweed"
}
},
"documents": {
"title": "Documents",
- "subtitle": "Every letter, report, and scan in one searchable place — encrypted at rest.",
+ "subtitle": "Every letter, report and scan in one searchable place, encrypted at rest.",
"pageUpload": "Upload",
"ownerOnlyUpload": "Documents can only be added by the record owner.",
"kind": {
@@ -10405,11 +10805,13 @@
"searchPlaceholder": "Search title or filename…",
"typeGroup": "Document type",
"conditionGroup": "Condition",
+ "procedureGroup": "Procedures and surgeries",
"yearGroup": "Year",
"clear": "Clear filters",
"typeAll": "All types",
"typeCount": "{count} types",
"conditionAll": "All conditions",
+ "procedureAll": "All procedures",
"yearAll": "All years"
},
"card": {
@@ -10425,15 +10827,15 @@
},
"error": {
"fileTooLarge": "This file is larger than the {maxSize} limit.",
- "quotaExceeded": "Storage is full — {used} of {quota} used.",
+ "quotaExceeded": "Storage is full: {used} of {quota} used.",
"unsupportedType": "This file type is not supported.",
"purged": "This document was permanently removed and cannot be restored.",
"duplicateExists": "An identical document is already stored.",
- "rateLimited": "Too many uploads right now — try again in a few minutes.",
+ "rateLimited": "Too many uploads right now. Try again in a few minutes.",
"generic": "Upload failed. Please try again."
},
"toast": {
- "duplicate": "Already stored — highlighting the existing document.",
+ "duplicate": "Already stored. The existing document is highlighted.",
"deleted": "Document deleted",
"restoreFailed": "Couldn't restore the document."
},
@@ -10456,7 +10858,7 @@
},
"empty": {
"title": "Your document vault is empty",
- "description": "Store doctor letters, lab reports, imaging, prescriptions, and vaccination records — encrypted, and searchable by type, condition, and year.",
+ "description": "Store doctor letters, lab reports, imaging, prescriptions and vaccination records. They are encrypted and searchable by type, condition and year.",
"action": "Upload a first document",
"noMatchesTitle": "No matching documents",
"noMatchesDescription": "Nothing matches the current search and filters."
@@ -10487,6 +10889,7 @@
"saveError": "Couldn't save the change.",
"deleteError": "Couldn't delete the document.",
"download": "Download",
+ "openInNewTab": "Open in new tab",
"delete": "Delete",
"titleLabel": "Title",
"titlePlaceholder": "Add a title",
@@ -10496,9 +10899,9 @@
"linkCondition": "Link",
"noConditions": "Not linked to a condition",
"previewUnavailable": "No inline preview for this file type",
- "uploadedMeta": "Added {date} · {size}",
+ "uploadedMeta": "Added {date}, {size}",
"importedFrom": "Imported from {system}",
- "importedFromWithId": "Imported from {system} · ID {id}",
+ "importedFromWithId": "Imported from {system}, ID {id}",
"sourceOther": "another system",
"share": "Share",
"labValuesLink": "View {count} lab values",
@@ -10565,7 +10968,7 @@
"errorNotIndexed": "Read the document first, then extract.",
"errorLocalOcrDisabled": "Turn on local OCR in the AI settings first.",
"errorProvider": "No AI provider is set up for this.",
- "errorTextImageOnly": "Local OCR reads images only — not this file.",
+ "errorTextImageOnly": "Local OCR reads images only, not this file.",
"errorConsent": "Reading documents with AI needs your consent first.",
"errorUnavailable": "Reading documents with AI is not available here right now."
},
@@ -10598,7 +11001,7 @@
"CONDITION": "Condition",
"MEDICATION_STATEMENT": "Medication"
},
- "statedRange": "stated range {range}",
+ "statedRange": "Stated range {range}",
"needsCheck": "Check this value against the document, then save it.",
"verify": "Check",
"verifySave": "Save value",
@@ -10714,6 +11117,21 @@
"finishedHint": "Search again or change a filter to pick more documents.",
"maxReached": "You can import up to {max} documents at a time.",
"noMatchHint": "Try part of the name, or clear the tag and dates."
+ },
+ "customize": "Customize view",
+ "layout": {
+ "viewDescription": "Preview cards with thumbnails, or a compact list.",
+ "arrangementHeading": "Arrangement",
+ "arrangementDescription": "Each month as its own block, or everything flowing across the full width.",
+ "stacked": "Months stacked",
+ "flow": "Continuous",
+ "saveFailed": "Couldn't save the view."
+ },
+ "deleteConfirm": {
+ "titleOne": "Delete this document?",
+ "titleMany": "Delete {count} documents?",
+ "body": "You can undo this right afterwards. After 30 days it is removed for good.",
+ "action": "Delete"
}
},
"selfDescription": {
@@ -10800,7 +11218,7 @@
"genericDescription": "A measurement you track. Add values to follow it over time.",
"emptyTitle": "No values yet",
"emptyDescription": "Add your first value to start charting this metric.",
- "valuesTitle": "{metric} — all values",
+ "valuesTitle": "{metric}: all values",
"valuesDescription": "Every logged value for this metric.",
"backToMetric": "Back to {metric}"
}
@@ -10847,35 +11265,41 @@
"lastAttemptReason": {
"unitMismatch": "The values that arrived didn't match the units this app expects for these nutrients, so they couldn't be stored.",
"valueOutOfRange": "The amounts that arrived were higher than this app can plausibly store, so they couldn't be saved.",
- "dayInvalid": "The entries carried a date the server couldn't accept. This can happen when the clock or time zone on the sending side is wrong — it's worth checking.",
+ "dayInvalid": "The entries carried a date the server couldn't accept. This can happen when the clock or time zone on the sending side is wrong, so it's worth checking.",
"upsertFailed": "These entries reached the server, but a problem on this end kept them from being saved. There's nothing you need to do.",
"unknown": "These entries arrived but couldn't be stored."
}
},
"hydration": {
- "referenceMeta": "Reference intake {value} {unit}/day · EFSA"
+ "referenceMeta": "Reference intake {value} {unit}/day (EFSA)"
},
"caffeine": {
- "ceilingMeta": "Safe-level ceiling {value} {unit}/day · EFSA"
+ "ceilingMeta": "Safe-level ceiling {value} {unit}/day (EFSA)"
},
"micronutrients": {
"title": "Vitamins & minerals",
- "rowMeta": "{days} of {window} days · ref {value} {unit} (EFSA)",
+ "rowMeta": "{days} of {window} days, ref {value} {unit} (EFSA)",
"rowMetaNoRef": "{days} of {window} days",
"footer": "{tracked} of {total} tracked micronutrients have data in this window.",
"emptyTitle": "No micronutrient data synced yet",
"loadError": "Your nutrient data could not be loaded"
},
- "sparseNote": "A low number here usually means sparse logging, not a deficiency — Apple Health day totals reflect what your apps logged, not total dietary intake."
+ "sparseNote": "A low number here usually means sparse logging, not a deficiency. Apple Health day totals reflect what your apps logged, not total dietary intake."
},
"daily": {
"line": {
- "score": "Your health score today is {score}.",
- "allClear": "Nothing needs your attention today."
+ "allClear": "Nothing needs your attention today.",
+ "scoreReady": "Your health score for today is ready."
},
"push": {
"title": "Your morning briefing",
- "bodyProvisional": "{line} · Last night's sleep isn't in yet."
+ "bodyProvisional": "{line}\nLast night's sleep isn't in yet.",
+ "titleScore": "Morning score {score}",
+ "band": {
+ "green": "Looking steady today.",
+ "yellow": "A few things are worth a look today.",
+ "red": "Some areas need attention today."
+ }
},
"item": {
"doseWindow": {
@@ -10893,27 +11317,27 @@
},
"syncIssue": {
"title": "Sync needs attention",
- "body": "{integration} isn't syncing — reconnect to keep your data current."
+ "body": "{integration} isn't syncing. Reconnect to keep your data current."
},
"coachCheckin": {
"title": "Time to check in on a plan",
- "body": "It's been about a week since you set this plan — keep it, adjust it, or let it go. No pressure either way.",
- "bodyPlan": "About a week ago you set: “{plan}”. Keep it, adjust it, or let it go — no pressure either way.",
+ "body": "It's been about a week since you set this plan. Keep it, adjust it, or let it go. No pressure either way.",
+ "bodyPlan": "About a week ago you set: “{plan}”. Keep it, adjust it, or let it go. No pressure either way.",
"adjustPrompt": "I'd like to adjust my check-in plan.",
"adjustPromptPlan": "I'd like to adjust this plan: “{plan}”"
},
"tensionWindow": {
"title": "Possible tension today",
"body": {
- "morning": "Your heart rate sat above its resting baseline for a sustained stretch this morning while you were still — possibly some tension.",
- "afternoon": "Your heart rate sat above its resting baseline for a sustained stretch this afternoon while you were still — possibly some tension.",
- "evening": "Your heart rate sat above its resting baseline for a sustained stretch this evening while you were still — possibly some tension.",
- "night": "Your heart rate sat above its resting baseline for a sustained stretch overnight while you were still — possibly some tension."
+ "morning": "Your heart rate sat above its resting baseline for a sustained stretch this morning while you were still, possibly some tension.",
+ "afternoon": "Your heart rate sat above its resting baseline for a sustained stretch this afternoon while you were still, possibly some tension.",
+ "evening": "Your heart rate sat above its resting baseline for a sustained stretch this evening while you were still, possibly some tension.",
+ "night": "Your heart rate sat above its resting baseline for a sustained stretch overnight while you were still, possibly some tension."
}
},
"ecgNewRecording": {
"title": "New ECG recording",
- "body": "Your device recorded a new ECG — it's ready to view.",
+ "body": "Your device recorded a new ECG. It's ready to view.",
"bodyVerdict": "Your device recorded a new ECG and reported: {verdict}.",
"verdict": {
"irregular": "a possible irregular rhythm",
@@ -10933,9 +11357,9 @@
},
"upcomingVisit": {
"title": "Appointment coming up",
- "bodyToday": "{what} — today",
- "bodyTomorrow": "{what} — tomorrow",
- "bodyDayAfterTomorrow": "{what} — the day after tomorrow"
+ "bodyToday": "{what} today",
+ "bodyTomorrow": "{what} tomorrow",
+ "bodyDayAfterTomorrow": "{what} the day after tomorrow"
}
},
"action": {
@@ -10954,10 +11378,34 @@
"scoreLabel": "Health score",
"deltaVsBaseline": "{delta} vs your baseline",
"deltaFlat": "In line with your baseline",
- "sleepPending": "Last night's sleep isn't in yet — today's read is provisional.",
+ "sleepPending": "Last night's sleep isn't in yet, so today's read is provisional.",
"worthALook": "Worth a look",
"allClear": "Nothing needs your attention today.",
- "ringLink": "Open {metric} details"
+ "ringLink": "Open {metric} details",
+ "heading": "Today",
+ "steadyWeeksOne": "Steady for {count} week",
+ "steadyWeeksFew": "Steady for {count} weeks",
+ "steadyWeeksOther": "Steady for {count} weeks",
+ "valueWithUnit": "{value} {unit}",
+ "rangeWithUnit": "{low} to {high} {unit}",
+ "rangeAtThisTime": "{range} for this time of day",
+ "vital": {
+ "RESTING_HEART_RATE": "Resting heart rate",
+ "HEART_RATE_VARIABILITY": "Heart-rate variability",
+ "RESPIRATORY_RATE": "Respiratory rate",
+ "OXYGEN_SATURATION": "Oxygen saturation",
+ "BODY_TEMPERATURE": "Body temperature",
+ "SKIN_TEMPERATURE": "Skin temperature",
+ "BLOOD_PRESSURE_SYS": "Systolic blood pressure",
+ "BLOOD_PRESSURE_DIA": "Diastolic blood pressure",
+ "BLOOD_GLUCOSE": "Blood glucose",
+ "PULSE": "Pulse",
+ "WEIGHT": "Weight",
+ "generic": "A vital"
+ },
+ "steadyAtLeastWeeksOne": "Steady for at least {count} week",
+ "steadyAtLeastWeeksFew": "Steady for at least {count} weeks",
+ "steadyAtLeastWeeksOther": "Steady for at least {count} weeks"
},
"milestone": {
"metric": {
@@ -10969,7 +11417,7 @@
},
"record": {
"title": "A new {metric} best",
- "body": "A personal best for your {metric} today — quietly noted, nothing to do."
+ "body": "A personal best for your {metric} today. Quietly noted, nothing to do."
},
"returnToBaseline": {
"title": "Back in range: {metric}",
@@ -10993,6 +11441,68 @@
"body": "A full month with your {metric} in your usual range. Beautifully steady."
}
}
+ },
+ "lead": {
+ "vitalOutside": {
+ "above": "{metric} is at {value}, above your usual range of {range}.",
+ "below": "{metric} is at {value}, below your usual range of {range}."
+ },
+ "vitalsOutside": {
+ "aboveOne": "{metric} is at {value}, above your usual range of {range}, and {count} more vital is outside its range too.",
+ "aboveFew": "{metric} is at {value}, above your usual range of {range}, and {count} more vitals are outside their range too.",
+ "aboveOther": "{metric} is at {value}, above your usual range of {range}, and {count} more vitals are outside their range too.",
+ "belowOne": "{metric} is at {value}, below your usual range of {range}, and {count} more vital is outside its range too.",
+ "belowFew": "{metric} is at {value}, below your usual range of {range}, and {count} more vitals are outside their range too.",
+ "belowOther": "{metric} is at {value}, below your usual range of {range}, and {count} more vitals are outside their range too."
+ },
+ "sleep": {
+ "more": "Last night you slept {duration}, {diff} more than usual.",
+ "less": "Last night you slept {duration}, {diff} less than usual.",
+ "usual": "Last night you slept {duration}, close to your usual.",
+ "plain": "Last night you slept {duration}."
+ },
+ "vitalsInRangeOne": "Your latest vital sits inside its usual range.",
+ "vitalsInRangeFew": "All {count} of your latest vitals sit inside their usual range.",
+ "vitalsInRangeOther": "All {count} of your latest vitals sit inside their usual range.",
+ "vitalInRange": "{metric} is at {value}, inside your usual range of {range}."
+ },
+ "todayFact": {
+ "restMode": {
+ "label": "Rest mode",
+ "value": "Day {day}"
+ },
+ "medications": {
+ "label": "Medications",
+ "value": "{taken} of {scheduled} taken"
+ },
+ "appointment": {
+ "label": "Appointment",
+ "today": "Today {time}, {what}",
+ "tomorrow": "Tomorrow {time}, {what}"
+ },
+ "sleep": {
+ "label": "Last night",
+ "plain": "{duration}",
+ "usual": "{duration}, close to your usual",
+ "more": "{duration}, {diff} more than usual",
+ "less": "{duration}, {diff} less than usual"
+ },
+ "vitals": {
+ "label": "Vitals",
+ "allInOne": "{count} checked, in your range",
+ "allInFew": "{count} checked, all in your range",
+ "allInOther": "{count} checked, all in your range",
+ "oneOut": {
+ "above": "{metric} above your range",
+ "below": "{metric} below your range"
+ },
+ "someOut": "{count} of {total} outside your range"
+ },
+ "cycle": {
+ "label": "Cycle",
+ "value": "{phase}, day {day}",
+ "dayOnly": "Day {day}"
+ }
}
},
"reportSelection": {
@@ -11069,7 +11579,7 @@
"canManage": "Can read the record, add to it, and change or remove what is in it",
"entireRecord": "Covers the entire record",
"sections": "Covers: {sections}",
- "noSections": "Covers nothing — this access opens no part of the record",
+ "noSections": "Covers nothing: this access opens no part of the record",
"invited": "Invited",
"accepted": "Accepted",
"expires": "Lapses",
@@ -11122,7 +11632,7 @@
"errorScope": "Those sections cannot be shared like that. Pick at least one section, or share the entire record.",
"errorManageBrowserOnly": "Manage access can only be offered in a browser.",
"errorStepUp": "Confirm your second factor, then try again.",
- "errorOffline": "That did not reach the server. Your invitation is still here — check your connection and try again."
+ "errorOffline": "That did not reach the server. Your invitation is still here. Check your connection and try again."
},
"given": {
"title": "Who can open my record",
@@ -11177,7 +11687,7 @@
"managed": {
"title": "Records you look after",
"description": "Health records with no login of their own.",
- "explainer": "A managed profile is a health record for somebody who does not sign in themselves — a child, or a person you care for. It has no login and no e-mail address, and you become its first guardian. You can add more guardians later, and any of you can switch into the record to keep it up to date.",
+ "explainer": "A managed profile is a health record for somebody who does not sign in themselves, such as a child or a person you care for. It has no login and no e-mail address, and you become its first guardian. You can add more guardians later, and any of you can switch into the record to keep it up to date.",
"nameLabel": "Name",
"namePlaceholder": "The name you will recognise this record by",
"nameHint": "Up to 80 characters. Shown wherever this record appears.",
@@ -11199,7 +11709,7 @@
"creating": "Creating…",
"created": "{name} is set up. You can switch into that record from the account menu.",
"errorStepUp": "Confirm your second factor, then try again.",
- "errorOffline": "That did not reach the server. Nothing has changed — check your connection and try again.",
+ "errorOffline": "That did not reach the server. Nothing has changed. Check your connection and try again.",
"errorInvalid": "The server refused those details. Check the name, the date and the timezone.",
"errorFailed": "The profile could not be created.",
"edit": "Edit",
@@ -11210,7 +11720,7 @@
"editErrorFailed": "The changes could not be saved.",
"delete": "Delete profile",
"deleteTitle": "Delete {name}?",
- "deleteBody": "Everything recorded for {name} goes with the profile — readings, medications, documents and notes. This cannot be undone.",
+ "deleteBody": "Everything recorded for {name} goes with the profile: readings, medications, documents and notes. This cannot be undone.",
"deleteGuardiansOne": "{count} person looks after this record today and will lose it.",
"deleteGuardiansFew": "{count} people look after this record today and will lose it.",
"deleteGuardiansOther": "{count} people look after this record today and will lose it.",
@@ -11239,7 +11749,47 @@
"guardianErrorFailed": "The invitation could not be sent.",
"errorMfaRequired": "This needs a second factor, and your account does not have one yet. Set one up under Settings → Security, then try again.",
"rosterLoadError": "Could not load who looks after this record.",
- "errorRateLimit": "Too many profiles created in the last hour. Wait a while and try again."
+ "errorRateLimit": "Too many profiles created in the last hour. Wait a while and try again.",
+ "handover": {
+ "action": "Hand over",
+ "title": "Hand over to {name}",
+ "description": "Give {name} a sign-in of their own. Nothing is copied or moved: the record becomes their account.",
+ "guardiansLabel": "Access after the handover",
+ "guardiansHint": "Your proposal. {name} sees it and decides after signing in.",
+ "access": {
+ "end": "No access",
+ "read": "View only",
+ "manage": "Manage"
+ },
+ "expiryLabel": "Link valid for",
+ "expiry1": "1 day",
+ "expiry7": "7 days",
+ "expiry14": "14 days",
+ "minorHint": "{name} is under 16. Keeping view access is worth considering, and so is talking the step through together.",
+ "irreversible": "Once {name} uses the link, the handover cannot be undone. The record is theirs, and they decide who keeps access.",
+ "afterwards": "{name} confirms the disclaimer and any AI consent personally. Reminders for this record stop reaching you.",
+ "replaceNote": "Creating a new link withdraws the open one.",
+ "create": "Create link",
+ "creating": "Creating…",
+ "linkOnce": "This link is shown only now. Give it to {name} in person or through a channel you trust, or let them scan the code.",
+ "qrAlt": "QR code of the handover link for {name}",
+ "urlLabel": "Handover link",
+ "copy": "Copy",
+ "copied": "Link copied",
+ "copyError": "The link could not be copied. Select it and copy it by hand.",
+ "linkExpires": "Valid until {date}.",
+ "done": "Done",
+ "pending": "A handover link is open until {date}.",
+ "replace": "New link",
+ "revoke": "Withdraw link",
+ "revoked": "Link withdrawn",
+ "revokeFailed": "The link could not be withdrawn. Try again.",
+ "errorFailed": "The handover link could not be created.",
+ "errorRosterChanged": "The list of guardians changed in the meantime. Close this and open it again.",
+ "oidcOnly": "Handing over a profile is not available while this server allows single sign-on only.",
+ "loadError": "The handover status could not be loaded.",
+ "entryHint": "When {name} is ready to look after this record themselves."
+ }
},
"toast": {
"savedTo": "Saved to {name}'s record"
@@ -11358,7 +11908,13 @@
"symptomEventDelete": "{name} deleted a symptom entry",
"symptomDefinitionCreate": "{name} added a symptom",
"symptomDefinitionUpdate": "{name} changed a symptom",
- "symptomDefinitionDelete": "{name} removed a symptom"
+ "symptomDefinitionDelete": "{name} removed a symptom",
+ "customVaccineCreate": "{name} added an own vaccine",
+ "customVaccineUpdate": "{name} edited an own vaccine",
+ "customVaccineDelete": "{name} removed an own vaccine",
+ "lifeEventCreate": "{name} added a life event",
+ "lifeEventUpdate": "{name} changed a life event",
+ "lifeEventDelete": "{name} removed a life event"
},
"section": {
"measurements": {
@@ -11396,7 +11952,7 @@
},
"actionError": {
"retry": "Try again",
- "offline": "That did not reach the server. Nothing has changed — check your connection and try again.",
+ "offline": "That did not reach the server. Nothing has changed. Check your connection and try again.",
"rateLimited": "Too many attempts in a row. Wait a minute and try again.",
"acceptExpired": "That invitation has lapsed. Ask for a new one.",
"acceptWithdrawn": "That invitation was withdrawn.",
@@ -11408,12 +11964,28 @@
"notFound": "That access is no longer there.",
"revokeFailed": "The access could not be ended.",
"renounceFailed": "The access could not be handed back.",
- "profileNotFound": "That profile is not there any more — it may already have been deleted, or your access to it may have ended.",
+ "profileNotFound": "That profile is not there any more. It may already have been deleted, or your access to it may have ended.",
"deleteProfileFailed": "The profile could not be deleted.",
"removeGuardianFailed": "That access could not be ended."
},
"anamnesis": {
"relativeNote": "A family history entry describes this person's relatives. Add it only when they would want it recorded here."
+ },
+ "handoverDecision": {
+ "title": "Who keeps access to your record",
+ "description": "Decide what the people who looked after your record may do from now on.",
+ "proposed": "Proposed: {access}",
+ "settled": "Already changed under Shared access",
+ "empty": "Nobody else had access to your record.",
+ "later": "You can change this any time under Settings, Shared access.",
+ "save": "Confirm",
+ "saving": "Saving…",
+ "decideLater": "Later",
+ "errorFailed": "Your decision could not be saved. Try again.",
+ "loadError": "Your former guardians could not be loaded.",
+ "notificationsTitle": "Set up your own reminders",
+ "notificationsBody": "Reminders for this record used to reach the people looking after it. From now on they reach you, as soon as you add a notification channel.",
+ "notificationsAction": "Open notification settings"
}
},
"encounters": {
@@ -11434,7 +12006,7 @@
"both": "Both sides",
"none": "Not stated"
},
- "bodySiteWithSide": "{site} · {side}",
+ "bodySiteWithSide": "{site} ({side})",
"sectionTitle": "Visits",
"sectionDescription": "Doctor's visits and booked appointments in one place.",
"add": "Visit",
@@ -11449,7 +12021,7 @@
"created": "Visit saved",
"updated": "Visit updated",
"saveFailed": "The visit could not be saved.",
- "checkupNotClosed": "Visit saved. The checkup stays open — your access does not cover that.",
+ "checkupNotClosed": "Visit saved. The checkup stays open; your access does not cover that.",
"status": {
"planned": "Planned",
"done": "Attended",
@@ -11466,7 +12038,7 @@
"dateDerivesStatusHint": "A date in the future is a booked appointment.",
"status": "Status",
"practitioner": "Practice or doctor",
- "practitionerHint": "Optional — you can add the name later.",
+ "practitionerHint": "Optional: you can add the name later.",
"practitionerNone": "Not specified",
"practitionerSearch": "Search name or practice",
"practitionerNoMatch": "No entry found.",
@@ -11524,9 +12096,8 @@
},
"practitioners": {
"title": "Practices and doctors",
- "description": "Your own address book — type a practice once, pick it everywhere.",
+ "description": "Your own address book: type a practice once, pick it everywhere.",
"linkLabel": "Address book",
- "add": "Entry",
"searchPlaceholder": "Search name, practice or specialty",
"loadError": "The address book could not be loaded.",
"emptyTitle": "No entry yet",
@@ -11623,7 +12194,6 @@
"unreadableLabel": "Unreadable symptom",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Log symptom",
"sheetDescription": "One of your own symptoms, how strong and when.",
"symptom": "Symptom",
"symptomRequired": "Choose a symptom.",
@@ -11672,5 +12242,426 @@
"title": "Symptoms logged",
"filed": "logged with this illness"
}
+ },
+ "day": {
+ "scoreOfMax": "of {max}",
+ "cycleDay": "Cycle day {n}",
+ "cyclePhaseLine": {
+ "MENSTRUAL": "Cycle day {n}, menstrual phase",
+ "FOLLICULAR": "Cycle day {n}, follicular phase",
+ "OVULATORY": "Cycle day {n}, ovulatory phase",
+ "LUTEAL": "Cycle day {n}, luteal phase"
+ },
+ "previousDay": "Previous day",
+ "nextDay": "Next day",
+ "close": "Close day",
+ "hidePanel": "Hide day",
+ "showPanel": "Show {date}",
+ "openDay": "Open day",
+ "viewWholeDay": "View the whole day",
+ "captureForDay": "Add for this day",
+ "empty": "There are no entries for this day.",
+ "notShared": "Some areas are not shared with you.",
+ "inTimeline": "In the timeline",
+ "askCoach": "Ask the Coach about this day",
+ "loadFailed": "This day could not be loaded.",
+ "dayOf": "Day {n} of {m}",
+ "dayN": "Day {n}",
+ "usualRange": "Usually {lo} to {hi}",
+ "groups": {
+ "scores": "Scores",
+ "running": "Running that day",
+ "values": "Values",
+ "happened": "What happened"
+ },
+ "notable": {
+ "extremeHigh": "Highest daily value since {since}",
+ "extremeLow": "Lowest daily value since {since}",
+ "firstValue": "First reading of this kind",
+ "gap": "No reading for {days} days"
+ },
+ "previousDayShortcut": "Previous day (Alt + Left arrow)",
+ "nextDayShortcut": "Next day (Alt + Right arrow)",
+ "captureShort": "Log",
+ "inTimelineShort": "Timeline",
+ "askCoachPrompt": "What happened on {date}?",
+ "coachChips": "Days in this answer",
+ "description": "Everything your records hold for this day.",
+ "openDocument": "Open document",
+ "previousReading": "Before, {date}",
+ "sheetExpand": "Expand to full height",
+ "sheetShrink": "Shrink",
+ "valuesLegend": "Dot for this day, bar for the 30 days before",
+ "valuesAllOne": "All values",
+ "valuesAllFew": "All {count} values",
+ "valuesAllOther": "All {count} values",
+ "valuesFewer": "Fewer values",
+ "countEntriesOne": "1 entry",
+ "countEntriesFew": "{count} entries",
+ "countEntriesOther": "{count} entries",
+ "meanOfReadingsOne": "Average of 1 reading",
+ "meanOfReadingsFew": "Average of {count} readings",
+ "meanOfReadingsOther": "Average of {count} readings",
+ "sinceLastVisit": {
+ "title": "Since the last visit on {date}",
+ "countOne": "1 entry",
+ "countFew": "{count} entries",
+ "countOther": "{count} entries",
+ "allOne": "Show all",
+ "allFew": "Show all {count}",
+ "allOther": "Show all {count}",
+ "fewer": "Show fewer",
+ "note": "Compiled from your entries, without judgement. Every date opens the whole day.",
+ "report": "Doctor report from {date} to today"
+ },
+ "travel": "Away",
+ "event": {
+ "started": "{label} started",
+ "ended": "{label} ended",
+ "paused": "{label} paused",
+ "resumed": "{label} resumed",
+ "illnessBegan": "{label} began",
+ "illnessResolved": "{label} resolved"
+ },
+ "pickDate": "Choose a date",
+ "pickerTitle": "Go to a day",
+ "today": "Today",
+ "hasEntries": "has entries",
+ "dayNSince": "Day {n}, since {date}",
+ "sinceDate": "Since {date}"
+ },
+ "timeline": {
+ "title": "Timeline",
+ "subtitle": "What happened when, beside your values.",
+ "standing": "Ongoing",
+ "startUnknown": "start unknown",
+ "zoom": {
+ "all": "All",
+ "year": "12 months",
+ "quarter": "3 months",
+ "range": "Range"
+ },
+ "lanes": {
+ "life": "Life",
+ "illness": "Conditions",
+ "allergies": "Allergies",
+ "medications": "Medications",
+ "vaccinations": "Vaccinations",
+ "visits": "Doctor and procedures",
+ "labs": "Labs",
+ "documents": "Documents",
+ "cycle": "Cycle"
+ },
+ "readiness": {
+ "title": "What your timeline already carries",
+ "subtitle": "An honest inventory before you open it.",
+ "menuEntry": "Data coverage",
+ "hint": "Empty lanes stay hidden until something is there. Modules you switched off never appear. You find this overview later under Data coverage in the timeline.",
+ "later": "Later",
+ "open": "Open timeline",
+ "verdict": {
+ "carries": "It carries already.",
+ "thin": "Still thin. For now the timeline shows mostly your values."
+ },
+ "status": {
+ "carries": "carries",
+ "thin": "thin",
+ "empty": "empty"
+ },
+ "lanes": {
+ "values": "Values",
+ "mood": "Mood",
+ "environment": "Environment",
+ "life": "Life events"
+ },
+ "meter": "{carrying} of {total} lanes carry",
+ "carriesDetail": "{carrying} of {total} lanes give an overview from {since}.",
+ "loadFailed": "The inventory could not be loaded.",
+ "card": {
+ "title": "What the timeline already carries",
+ "summary": "It reaches back to {since}; {carrying} of {total} lanes already carry.",
+ "dismiss": "Hide"
+ },
+ "detail": {
+ "valuesOne": "{count} measure since {since}",
+ "valuesFew": "{count} measures since {since}",
+ "valuesOther": "{count} measures since {since}",
+ "illnessOne": "{count} episode, {chronic} of them chronic",
+ "illnessFew": "{count} episodes, {chronic} of them chronic",
+ "illnessOther": "{count} episodes, {chronic} of them chronic",
+ "allergiesOne": "{count} allergy",
+ "allergiesFew": "{count} allergies",
+ "allergiesOther": "{count} allergies",
+ "medicationsOne": "{count} medication, {missingStart} without a start date",
+ "medicationsFew": "{count} medications, {missingStart} without a start date",
+ "medicationsOther": "{count} medications, {missingStart} without a start date",
+ "medicationsDatedOne": "{count} medication with a start date",
+ "medicationsDatedFew": "{count} medications with a start date",
+ "medicationsDatedOther": "{count} medications with a start date",
+ "vaccinationsOne": "{count} vaccination",
+ "vaccinationsFew": "{count} vaccinations",
+ "vaccinationsOther": "{count} vaccinations",
+ "visitsOne": "{count} visit, {procedures} with a procedure",
+ "visitsFew": "{count} visits, {procedures} with a procedure",
+ "visitsOther": "{count} visits, {procedures} with a procedure",
+ "visitsPlannedOne": "{count} visit, planned only",
+ "visitsPlannedFew": "{count} visits, planned only",
+ "visitsPlannedOther": "{count} visits, planned only",
+ "labsOne": "{count} day with results",
+ "labsFew": "{count} days with results",
+ "labsOther": "{count} days with results",
+ "documentsOne": "{count} dated document",
+ "documentsFew": "{count} dated documents",
+ "documentsOther": "{count} dated documents",
+ "lifeOne": "{count} life event",
+ "lifeFew": "{count} life events",
+ "lifeOther": "{count} life events",
+ "cycleOne": "{count} cycle",
+ "cycleFew": "{count} cycles",
+ "cycleOther": "{count} cycles",
+ "moodOne": "{count} entry since {since}",
+ "moodFew": "{count} entries since {since}",
+ "moodOther": "{count} entries since {since}",
+ "environmentOne": "{count} day with environment data",
+ "environmentFew": "{count} days with environment data",
+ "environmentOther": "{count} days with environment data",
+ "empty": "None yet"
+ },
+ "gap": {
+ "valuesEmpty": "Connect sources",
+ "illnessEmpty": "Add an episode",
+ "medicationsWithoutStart": "Complete",
+ "visitsEmpty": "Add a visit",
+ "documentsUndated": "Review",
+ "labsEmpty": "Add a result",
+ "vaccinationsEmpty": "Add vaccinations",
+ "lifeEventsEmpty": "Add"
+ },
+ "gapLine": {
+ "medicationsWithoutStartOne": "{count} medication without a start date",
+ "medicationsWithoutStartFew": "{count} medications without a start date",
+ "medicationsWithoutStartOther": "{count} medications without a start date",
+ "documentsUndatedOne": "{count} document without a date",
+ "documentsUndatedFew": "{count} documents without a date",
+ "documentsUndatedOther": "{count} documents without a date",
+ "valuesEmpty": "No values over a longer period yet",
+ "illnessEmpty": "No conditions with a start date yet",
+ "visitsEmpty": "No completed doctor visits yet",
+ "labsEmpty": "No lab results yet",
+ "vaccinationsEmpty": "No vaccinations yet",
+ "lifeEventsEmpty": "No life events yet"
+ }
+ },
+ "legendSpan": "Period",
+ "legendEvent": "Event",
+ "legendMeanQuarter": "Quarterly mean",
+ "legendMeanMonth": "Monthly mean",
+ "legendMeanWeek": "Weekly mean",
+ "legendThin": "Fewer than 3 readings",
+ "legendGap": "Short gap without measurements",
+ "today": "Today",
+ "noDataYet": "No data yet",
+ "layers": "Layers",
+ "layersLanes": "Lanes",
+ "zoomLabel": "View range",
+ "chartLabel": "Timeline from {from} to {to}",
+ "keyboardHint": "Arrow keys move the selection, Home and End jump to either end, Enter opens the day.",
+ "loadFailed": "The timeline could not be loaded.",
+ "empty": {
+ "title": "Nothing on the timeline yet",
+ "description": "Dated entries appear here as soon as there are some."
+ },
+ "values": {
+ "menuTitle": "Values under the lanes",
+ "trigger": "Values ({count})",
+ "noneInWindow": "No values in this range",
+ "series": {
+ "BLOOD_PRESSURE_SYS": "Blood pressure sys.",
+ "BLOOD_PRESSURE_DIA": "Blood pressure dia."
+ },
+ "noValue": "no value",
+ "bloodPressure": "Blood pressure",
+ "readingsOne": "1 reading",
+ "readingsFew": "mean of {count} readings",
+ "readingsOther": "mean of {count} readings"
+ },
+ "table": {
+ "caption": "Timeline entries",
+ "lane": "Lane",
+ "entry": "Entry",
+ "start": "Start",
+ "end": "End",
+ "seriesCaption": "Value means by period",
+ "period": "Period"
+ },
+ "selection": {
+ "empty": "No entries in this period",
+ "since": "since {date}",
+ "range": "{from} to {to}",
+ "mean": "Ø {values}",
+ "week": "Week of {date}",
+ "hintClick": "Click a point in time to see it here",
+ "hintTap": "Tap a point in time to see it here"
+ },
+ "chronicle": {
+ "months": "Months",
+ "years": "Years",
+ "groupingLabel": "Grouping",
+ "empty": "No entries from {from} to {to}",
+ "emptySingle": "No entries in {period}",
+ "ended": "{label} ended",
+ "afterDaysOne": "after 1 day",
+ "afterDaysFew": "after {count} days",
+ "afterDaysOther": "after {count} days",
+ "notable": {
+ "extremeHigh": "Highest value in a long while",
+ "extremeLow": "Lowest value in a long while",
+ "firstValue": "First value of a measure",
+ "gap": "A longer gap in the values"
+ },
+ "notableMeta": "Open the day for details"
+ },
+ "standingAllergiesOne": "{count} allergy",
+ "standingAllergiesFew": "{count} allergies",
+ "standingAllergiesOther": "{count} allergies",
+ "legendMeanDay": "Daily mean",
+ "range": {
+ "from": "From",
+ "to": "To",
+ "label": "Chosen range",
+ "open": "Choose a range",
+ "clear": "Show everything",
+ "apply": "Apply"
+ },
+ "item": {
+ "labValuesOne": "1 lab value",
+ "labValuesFew": "{count} lab values",
+ "labValuesOther": "{count} lab values"
+ }
+ },
+ "lifeEvents": {
+ "title": "Life event",
+ "add": "Life event",
+ "privacy": "Title and note are stored encrypted. They go to no AI.",
+ "emptyHint": "A move, a birth or a new job gives the years an anchor.",
+ "save": "Save",
+ "cancel": "Cancel",
+ "delete": "Delete life event",
+ "saved": "Life event saved",
+ "deleted": "Life event deleted",
+ "fields": {
+ "title": "What happened?",
+ "category": "Kind",
+ "when": "When",
+ "precision": "Precision",
+ "addEnd": "End date, if it was a period",
+ "endDate": "Until",
+ "note": "Note",
+ "optionalTag": "optional",
+ "notePlaceholder": "Something you want to know later",
+ "removeEnd": "Remove end date"
+ },
+ "category": {
+ "FAMILY": "Family",
+ "HOME": "Home",
+ "WORK": "Work",
+ "LOSS": "Loss",
+ "OTHER": "Other"
+ },
+ "precision": {
+ "DAY": "Day",
+ "MONTH": "Month",
+ "YEAR": "Year"
+ },
+ "edit": "Edit life event",
+ "errors": {
+ "titleRequired": "Give the event a name.",
+ "titleTooLong": "At most 120 characters.",
+ "categoryRequired": "Choose a kind.",
+ "dateRequired": "Enter a valid date.",
+ "endBeforeStart": "The end is before the start.",
+ "noteTooLong": "At most 2000 characters."
+ },
+ "saveFailed": "Saving did not work.",
+ "deleteFailed": "Deleting did not work.",
+ "deleteConfirm": {
+ "title": "Delete this life event?",
+ "body": "Its title and note will be gone."
+ }
+ },
+ "palette": {
+ "title": "Search",
+ "description": "Find a page, a setting, a module, an action or an entry in your records.",
+ "placeholder": "Search pages, settings, your records…",
+ "results": "Results",
+ "noResults": "Nothing found for “{query}”.",
+ "moduleOn": "On",
+ "moduleOff": "Off",
+ "hintMove": "Move",
+ "hintOpen": "Open",
+ "hintClose": "Close",
+ "groups": {
+ "recent": "Recent",
+ "pages": "Pages",
+ "insights": "Insights",
+ "settings": "Settings",
+ "modules": "Modules",
+ "actions": "Actions",
+ "records": "In your records"
+ },
+ "actions": {
+ "capture": "New entry",
+ "coach": "Ask the Coach",
+ "backup": "Create a backup",
+ "today": "Open today",
+ "shortcuts": "Show keyboard shortcuts"
+ },
+ "records": {
+ "loading": "Searching your records…",
+ "empty": "Nothing in your records for “{query}”.",
+ "error": "Your records could not be searched. Try again in a moment.",
+ "truncated": "Only the most recent entries were searched.",
+ "kind": {
+ "visit": "Visit",
+ "condition": "Condition",
+ "document": "Document",
+ "vaccination": "Vaccination",
+ "lab": "Lab result",
+ "medication": "Medication",
+ "symptom": "Symptom",
+ "workout": "Workout",
+ "note": "Note"
+ },
+ "source": {
+ "mood": "Mood note",
+ "measurement": "Note on a reading",
+ "illness": "Illness diary"
+ }
+ }
+ },
+ "shortcuts": {
+ "title": "Keyboard shortcuts",
+ "description": "Keys that work anywhere in the app, except while you type in a field.",
+ "menuEntry": "Keyboard shortcuts",
+ "then": "then",
+ "search": "Search",
+ "help": "Show this list",
+ "close": "Close a dialog or panel",
+ "previousDay": "Previous day",
+ "nextDay": "Next day",
+ "toggleDay": "Open or close the day",
+ "dayHint": "Previous and next day work while a day is open.",
+ "groups": {
+ "navigation": "Navigation",
+ "actions": "Actions",
+ "day": "Day"
+ }
+ },
+ "aboutPage": {
+ "eyebrow": "About",
+ "title": "About HealthLog",
+ "lastUpdated": "Last updated: {date}",
+ "project": "Project",
+ "credits": "Credits"
}
}
diff --git a/messages/es.json b/messages/es.json
index d40131208..7ce217112 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -49,7 +49,9 @@
"dismiss": "Descartar",
"moreInfo": "Más información",
"durationHoursMinutes": "{hours} h {minutes} min",
- "durationMinutes": "{minutes} min"
+ "durationMinutes": "{minutes} min",
+ "durationClockMinutes": "{time} min",
+ "durationClockHours": "{time} h"
},
"apiErrors": {
"measurement": {
@@ -165,7 +167,7 @@
"anamnesisShiftSchedule": "Horario de trabajo",
"anamnesisColFact": "Dato del perfil",
"anamnesisColValue": "Valor registrado",
- "subtitle": "HealthLog — Informe personal de salud",
+ "subtitle": "HealthLog: informe personal de salud",
"patient": "Paciente",
"dateOfBirth": "Fecha de nacimiento",
"gender": "Sexo",
@@ -175,18 +177,18 @@
"height": "Altura",
"period": "Período del informe",
"createdOn": "Creado el",
- "vitalsTitle": "Signos vitales — Resumen",
+ "vitalsTitle": "Signos vitales: resumen",
"colParameter": "Parámetro",
"colCurrent": "Actual",
"colAvgPeriod": "Med. periodo",
"colMin": "Mín",
"colMax": "Máx",
"colN": "n",
- "bpClassificationTitle": "Tensión arterial — Clasificación ESH (2023)",
+ "bpClassificationTitle": "Tensión arterial: clasificación ESH (2023)",
"avgBp": "Tensión arterial media",
"classification": "Clasificación",
"bmiTitle": "Índice de masa corporal (IMC)",
- "bmiRow": "IMC:{bmi} kg/m² — Classification: {class} (WHO)",
+ "bmiRow": "IMC:{bmi} kg/m², Classification: {class} (WHO)",
"complianceTitle": "Cumplimiento de medicación",
"glp1Title": "Terapia GLP-1",
"glp1WeightSummary": "Peso en el periodo del informe: {start} kg → {end} kg ({delta} kg)",
@@ -204,7 +206,7 @@
"colExpected": "Esperadas",
"colComplianceRate": "Tasa de cumplimiento",
"moodTitle": "Evolución del estado de ánimo",
- "moodSummary": "Promedio:{avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+ "moodSummary": "Promedio {avg} / 5.0, {count} entradas, rango {min}–{max}",
"colMood": "Estado de ánimo",
"colCount": "Cantidad",
"colShare": "Porcentaje",
@@ -234,8 +236,8 @@
"typeGlucoseRandom": "Glucosa (aleatoria)",
"typeGlucoseBedtime": "Glucosa (antes de dormir)",
"typeGlucoseUnspecified": "Glucosa (sin indicar)",
- "glucoseClassificationTitle": "Glucemia — Rangos de referencia (ADA 2024 / DDG)",
- "glucoseRow": "{label}: med. {avg} {unit} (objetivo {rangeMin}–{rangeMax}) — {class}",
+ "glucoseClassificationTitle": "Glucemia: rangos de referencia (ADA 2024 / DDG)",
+ "glucoseRow": "{label}: med. {avg} {unit} (objetivo {rangeMin}–{rangeMax}), {class}",
"glucoseInTarget": "en objetivo",
"glucoseBelowTarget": "por debajo del objetivo",
"glucoseAboveTarget": "por encima del objetivo",
@@ -287,7 +289,7 @@
"gmi": "Indicador de gestión de la glucosa (GMI)",
"eA1c": "A1C estimada",
"cv": "Variabilidad (CV%)",
- "spotCaveat": "Son estimaciones a partir de mediciones puntuales, no un perfil de monitor continuo — léelas como una tendencia, no como un veredicto clínico."
+ "spotCaveat": "Son estimaciones a partir de mediciones puntuales, no un perfil de monitor continuo. Léelas como una tendencia, no como un veredicto clínico."
},
"illnessTitle": "Afecciones y enfermedades",
"illnessOngoing": "En curso",
@@ -376,12 +378,12 @@
"backToDashboard": "Volver al panel"
},
"offlineBanner": {
- "message": "Sin conexión — los cambios no se pueden guardar hasta que vuelvas a estar en línea.",
- "saveFailed": "No se pudo guardar — estás sin conexión. Inténtalo de nuevo cuando vuelvas a estar en línea.",
+ "message": "Sin conexión: los cambios no se pueden guardar hasta que vuelvas a estar en línea.",
+ "saveFailed": "No se pudo guardar porque estás sin conexión. Inténtalo de nuevo cuando vuelvas a estar en línea.",
"showingCached": "Mostrando tus últimos datos sincronizados."
},
"demoBanner": {
- "message": "Modo demo — los cambios no se guardan."
+ "message": "Modo demo: los cambios no se guardan."
},
"nav": {
"dashboard": "Panel",
@@ -407,6 +409,7 @@
"mood": "Estado de ánimo",
"coach": "Coach",
"skipToContent": "Saltar al contenido",
+ "breadcrumb": "Estás aquí",
"sidebar": "Barra lateral",
"mainNavigation": "Navegación principal",
"mobileNavigation": "Navegación móvil",
@@ -426,7 +429,9 @@
"workout": "Entrenamiento",
"workoutDescription": "Una sesión que tus dispositivos no registraron",
"symptom": "Síntoma",
- "symptomDescription": "Uno de tus propios síntomas, con su intensidad"
+ "symptomDescription": "Uno de tus propios síntomas, con su intensidad",
+ "lifeEventDescription": "Una mudanza, un nacimiento, un nuevo trabajo",
+ "lifeEvent": "Acontecimiento vital"
},
"cycle": "Ciclo",
"labs": "Análisis",
@@ -435,7 +440,8 @@
"illness": "Enfermedad",
"documents": "Documentos",
"vaccinations": "Vacunas",
- "mentalWellbeing": "Bienestar mental"
+ "mentalWellbeing": "Bienestar mental",
+ "timeline": "Cronología"
},
"auth": {
"login": "Iniciar sesión",
@@ -459,7 +465,6 @@
"signOut": "Cerrar sesión y aceptar la invitación"
},
"createAccount": "Crear cuenta",
- "newAccount": "Crear cuenta nueva",
"backToPasskey": "Volver a passkey",
"passkeySetupLater": "Puedes configurar passkeys más tarde en los ajustes.",
"passwordMinLength": "Mín. 12 caracteres",
@@ -512,6 +517,37 @@
"fixLan": "Servidor en una red doméstica sin TLS: pon SESSION_COOKIE_SECURE=false en su configuración y reinícialo.",
"fixHttps": "Servidor detrás de un proxy HTTPS: abre la dirección https:// y asegúrate de que el proxy reenvía X-Forwarded-Proto: https.",
"localhostNote": "Esta página está en http://localhost y el servidor solo establece su cookie de sesión para https://. La mayoría de los navegadores lo aceptan en localhost; si vuelves aquí tras iniciar sesión, pon SESSION_COOKIE_SECURE=false."
+ },
+ "claim": {
+ "title": "Asume tu historial de salud",
+ "titleNamed": "Te damos la bienvenida, {name}",
+ "description": "Hasta ahora otras personas han llevado tu historial de salud por ti. Elige cómo iniciar sesión y pasará a ser tu propia cuenta. No se copia ni se mueve nada.",
+ "loading": "Comprobando tu enlace…",
+ "invalid": "Este enlace ya no funciona",
+ "invalidHint": "Es desconocido, ha caducado, se ha retirado o ya se ha usado. Pide uno nuevo a quien te lo envió.",
+ "oidcOnly": "Este servidor solo permite el inicio de sesión único, así que aquí todavía no se puede asumir un perfil.",
+ "signedInTitle": "Cierra la sesión primero",
+ "signedInBody": "Has iniciado sesión como {username}. Este enlace crea un acceso para otra persona, así que cierra la sesión antes de usarlo.",
+ "signOut": "Cerrar sesión y continuar",
+ "guardiansTitle": "Quién conserva el acceso por ahora",
+ "guardiansHint": "Lo proponen las personas que llevaron tu historial. Tú decides justo después de iniciar sesión.",
+ "guardianUnnamed": "Una persona tutora",
+ "usernameHint": "De 3 a 30 letras, números, _ o -.",
+ "usernameReserved": "Los nombres de usuario que empiezan por managed- están reservados.",
+ "emailHint": "Obligatorio, para que puedas volver a entrar si olvidas tu contraseña.",
+ "consentNote": "Confirmas tú mismo el aviso y cualquier uso de IA. Hasta entonces, nada de tu historial se envía a un proveedor de IA.",
+ "expires": "Este enlace es válido hasta el {date}.",
+ "submit": "Asumir",
+ "submitting": "Asumiendo…",
+ "errorTaken": "Ese nombre de usuario o correo electrónico ya está en uso.",
+ "errorRateLimit": "Demasiados intentos. Espera unos minutos y vuelve a intentarlo.",
+ "errorFailed": "No ha funcionado. Vuelve a intentarlo en un momento.",
+ "errorOffline": "No se llegó al servidor. Comprueba tu conexión y vuelve a intentarlo."
+ },
+ "inviteInvalid": {
+ "title": "Invitación no válida",
+ "description": "Este enlace de invitación está incompleto o se copió mal. Pide un enlace nuevo a la persona que te invitó.",
+ "toLogin": "Ir a iniciar sesión"
}
},
"dashboard": {
@@ -543,17 +579,17 @@
"sleepShort": "Sueño",
"stepsShort": "Pasos",
"vo2MaxShort": "VO₂ máx",
- "bpInTargetWindow90": "PA-objetivo · 90 d",
- "bpInTargetWindowDynamic": "PA-objetivo · {days} d",
+ "bpInTargetWindow90": "PA-objetivo (90 d)",
+ "bpInTargetWindowDynamic": "PA-objetivo ({days} d)",
"bpInTargetCollecting": "recopilando datos",
"medications": "Medicamentos",
"quickAddMeasurement": "Registrar medición",
"quickAddMood": "Registrar estado de ánimo",
"quickAddMedicationIntake": "Registrar toma",
"quickAddWorkout": "Registrar entrenamiento",
+ "quickAddLifeEvent": "Registrar acontecimiento vital",
"customizeDashboard": "Personalizar panel",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Registrar toma de medicación",
"sheetDescription": "Registra dosis, hora y medicamento en unos pocos toques.",
"medicationLabel": "Medicamento",
"medicationPlaceholder": "Selecciona un medicamento",
@@ -666,7 +702,8 @@
"body": "Sin una copia de la clave, tus datos no se pueden recuperar tras una reinstalación. Es cuestión de un minuto.",
"action": "Abrir el paso"
},
- "quickAddSymptom": "Registrar síntoma"
+ "quickAddSymptom": "Registrar síntoma",
+ "todayCardVisible": "Mostrar la tarjeta superior"
},
"measurements": {
"title": "Mediciones",
@@ -738,7 +775,7 @@
"formReset": "Restablecer formulario",
"saveError": "Error al guardar",
"duplicateTimestamp": "Ya existe una medición de este tipo en la marca de tiempo seleccionada.",
- "serverOwnedSource": "Los valores de una fuente conectada no se pueden editar — la fecha y la nota siguen siendo editables.",
+ "serverOwnedSource": "Los valores de una fuente conectada no se pueden editar, pero la fecha y la nota siguen siendo editables.",
"deleteError": "Error al eliminar",
"deletedToast": "Medición eliminada",
"restoredToast": "Medición restaurada",
@@ -833,7 +870,8 @@
"durationHoursOnly": "{hours} h",
"durationMinutesOnly": "{minutes} min",
"durationInvalid": "Escribe una duración como 7,5, 7:30 o 7h 30m.",
- "durationTooLong": "Una entrada no puede durar más de 24 horas."
+ "durationTooLong": "Una entrada no puede durar más de 24 horas.",
+ "sourceHealthConnect": "Health Connect"
},
"metricCatalog": {
"title": "Lo que HealthLog puede registrar",
@@ -845,11 +883,11 @@
"connectCta": "Conectar una fuente",
"viewCta": "Ver",
"ecg": {
- "source": "Tiras de ECG de una derivación desde un reloj compatible — hoy, Withings ScanWatch — sincronizadas automáticamente."
+ "source": "Tiras de ECG de una derivación desde un reloj compatible (hoy, Withings ScanWatch), sincronizadas automáticamente."
},
"watchdog": {
"title": "Supervisión automática",
- "description": "HealthLog también vigila en segundo plano — notificaciones de ritmo irregular y alteraciones respiratorias de un reloj conectado, alertas de desviación de tu valor de referencia, y cambios destacables entre tus dos últimos paneles de laboratorio — y solo muestra una tarjeta cuando algo destaca. Si al menos una de esas fuentes está conectada, que no aparezca ninguna tarjeta es una buena señal, no una función ausente; si todavía no hay ninguna conectada, sencillamente no se está vigilando nada."
+ "description": "HealthLog también vigila en segundo plano (notificaciones de ritmo irregular y alteraciones respiratorias de un reloj conectado, alertas de desviación de tu valor de referencia, y cambios destacables entre tus dos últimos paneles de laboratorio) y solo muestra una tarjeta cuando algo destaca. Si al menos una de esas fuentes está conectada, que no aparezca ninguna tarjeta es una buena señal, no una función ausente; si todavía no hay ninguna conectada, sencillamente no se está vigilando nada."
}
},
"mood": {
@@ -1191,7 +1229,8 @@
"bodyLogged": "Registrado en el diario de enfermedad: {count} síntomas",
"bodyNotLogged": "No hay nada registrado en el diario de enfermedad para este día",
"openIllness": "Abrir"
- }
+ },
+ "sourceNotification": "Notificación"
},
"medications": {
"title": "Medicamentos",
@@ -1209,8 +1248,8 @@
"dialogDescription": "Estos medicamentos están pendientes ahora. Confirma para registrarlos todos como tomados.",
"confirm": "Tomar todos ({count})",
"successToast": "{count} tomas registradas",
- "partialToast": "{taken} registradas, {failed} fallidas – inténtalo de nuevo",
- "failedToast": "No se registró ninguna toma – inténtalo de nuevo"
+ "partialToast": "{taken} registradas, {failed} fallidas. Inténtalo de nuevo",
+ "failedToast": "No se registró ninguna toma. Inténtalo de nuevo"
},
"logIntake": {
"dialogTitle": "Registrar una toma",
@@ -1284,6 +1323,14 @@
"disabledToast": "Registro de tomas desactivado",
"toggleFailed": "No se pudo cambiar el registro de tomas."
},
+ "autoMarkTaken": {
+ "label": "Registrar las tomas automáticamente",
+ "helper": "HealthLog registra cada dosis programada como tomada a su hora. Puedes seguir cambiando u omitiendo una dosis a mano. No se envían recordatorios.",
+ "cardHint": "Se registra automáticamente",
+ "enabledToast": "Las tomas se registran ahora automáticamente",
+ "disabledToast": "Las tomas ya no se registran automáticamente",
+ "toggleFailed": "No se pudo cambiar el registro automático."
+ },
"withoutNotification": "Sin notificación",
"pausedSince": "En pausa desde",
"takeNow": "Tomar ahora",
@@ -1299,7 +1346,7 @@
"complianceInfoLabel": "¿Qué significa el cumplimiento?",
"lastIntake": "Última toma:",
"nextIntake": "Próxima toma:",
- "nextIntakeOverdue": "Atrasada · {time} — aún se puede tomar",
+ "nextIntakeOverdue": "Atrasada desde {time}, aún se puede tomar",
"dayStreak": "días de racha",
"dayStreakOne": "día de racha",
"cycleDueToday": "Pendiente hoy",
@@ -1393,10 +1440,10 @@
"error": {
"empty": "Escribe una descripción primero.",
"tooLong": "Mantén la descripción por debajo de 2000 caracteres.",
- "rateLimit": "Demasiadas solicitudes — espera un momento e inténtalo de nuevo.",
- "noProvider": "Sin proveedor de IA configurado — configúralo en Ajustes.",
- "budget": "Se alcanzó el presupuesto diario de IA — inténtalo de nuevo mañana.",
- "network": "No se pudo contactar con el proveedor de IA — inténtalo de nuevo.",
+ "rateLimit": "Demasiadas solicitudes. Espera un momento e inténtalo de nuevo.",
+ "noProvider": "Sin proveedor de IA configurado. Configúralo en Ajustes.",
+ "budget": "Se alcanzó el presupuesto diario de IA. Inténtalo de nuevo mañana.",
+ "network": "No se pudo contactar con el proveedor de IA. Inténtalo de nuevo.",
"consent": "Leer tu descripción con IA necesita primero tu consentimiento.",
"unavailable": "Leer descripciones con IA no está disponible aquí ahora mismo."
}
@@ -1412,7 +1459,7 @@
"everyNMonths": "Cada N meses un día concreto",
"yearly": "Una vez al año",
"rolling": "Cada N días desde la última toma (flexible)",
- "rollingExplainer": "Cuenta desde tu última toma registrada — se pausa si te saltas una dosis.",
+ "rollingExplainer": "Cuenta desde tu última toma registrada y se pausa si te saltas una dosis.",
"oneShot": "Dosis única"
},
"weekdays": {
@@ -1470,7 +1517,7 @@
"night": "Noche"
},
"max": {
- "reached": "Máximo alcanzado — quita una hora antes de añadir otra."
+ "reached": "Máximo alcanzado: quita una hora antes de añadir otra."
}
},
"courseWindow": {
@@ -1520,7 +1567,7 @@
},
"step3": {
"title": "¿Cuál es la dosis?",
- "subline": "Una pastilla, una inhalación, una gota — según la forma.",
+ "subline": "Una pastilla, una inhalación, una gota, según la forma.",
"amountLabel": "Cantidad",
"amountPlaceholder": "p. ej. 5",
"unitLabel": "Unidad",
@@ -1546,7 +1593,7 @@
},
"dosesPerUnitLabel": "Dosis por unidad",
"dosesPerUnitPlaceholder": "p. ej. 4",
- "dosesPerUnitHint": "Dosis por pluma / vial — déjalo vacío para no controlar el inventario.",
+ "dosesPerUnitHint": "Dosis por pluma / vial. Déjalo vacío para no controlar el inventario.",
"short": "Dosis",
"unitsPerDoseLabel": "Unidades por dosis",
"unitsPerDoseHint": "p. ej., 2 comprimidos de 2 mg para una dosis de 4 mg, o ½ comprimido para una dosis partida"
@@ -1558,7 +1605,7 @@
},
"step5": {
"title": "¿Con qué frecuencia lo tomas?",
- "subline": "Elige la frecuencia — los detalles vienen en el siguiente paso.",
+ "subline": "Elige la frecuencia. Los detalles vienen en el siguiente paso.",
"short": "Frecuencia"
},
"step6": {
@@ -1580,7 +1627,7 @@
},
"step7": {
"title": "¿Cuándo lo tomas?",
- "subline": "Una o varias horas del día — los recordatorios siguen esta lista.",
+ "subline": "Una o varias horas del día. Los recordatorios siguen esta lista.",
"presetsLabel": "Preajustes por hora del día",
"presets": {
"morning": "Mañana",
@@ -1596,11 +1643,11 @@
"short": "Horas"
},
"step8": {
- "title": "Listo — ¿quieres que HealthLog te lo recuerde?",
+ "title": "Listo. ¿Quieres que HealthLog te lo recuerde?",
"subline": "Revisa todo abajo antes de guardar.",
"remindersLabel": "Recordatorios activados",
- "remindersDescription": "Push, Telegram o ntfy — según tus ajustes.",
- "noChannelHint": "Los recordatorios necesitan un canal de notificaciones — ahora mismo no hay ninguno activo.",
+ "remindersDescription": "Push, Telegram o ntfy, según tus ajustes.",
+ "noChannelHint": "Los recordatorios necesitan un canal de notificaciones, pero ahora mismo no hay ninguno activo.",
"noChannelHintLink": "Configurar notificaciones",
"short": "Listo"
}
@@ -1684,7 +1731,7 @@
}
},
"errors": {
- "submitFailed": "No se pudo guardar el medicamento — inténtalo de nuevo."
+ "submitFailed": "No se pudo guardar el medicamento. Inténtalo de nuevo."
}
},
"doseStrength": {
@@ -1700,7 +1747,7 @@
"empty": "Aún no hay pasos de escalada de dosis registrados. Añade cambios de dosis con fecha para trazar el plan.",
"youAreHere": "Estás aquí",
"doseStep": "{dose} {unit}",
- "plannedFor": "Previsto · {date}"
+ "plannedFor": "Previsto para {date}"
},
"injectionSiteBodyOutlineAriaLabel": "Silueta del cuerpo",
"injectionSitePickerAriaLabel": "Selector de sitio de inyección",
@@ -1740,6 +1787,7 @@
"sourceImport": "Importación",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "vía {origin}",
+ "intakeRecordedAutomatically": "Registrada automáticamente",
"intakeHistoryEmptyTitle": "Aún no hay tomas",
"intakeHistoryEmptyDescription": "En cuanto registres una toma, aparecerá aquí con la hora y la fuente.",
"intakeHistoryEmptyAction": "Registrar toma",
@@ -1751,7 +1799,7 @@
"intakeHistoryStatusSkipped": "Omitida",
"intakeHistorySortByTaken": "Ordenar por hora de toma",
"intakeHistorySortByScheduled": "Ordenar por hora programada",
- "intakeHistoryPageInfo": "Página {page} de {total} · {count} entradas",
+ "intakeHistoryPageInfo": "Página {page} de {total}, {count} entradas",
"intakeHistoryPrev": "Anterior",
"intakeHistoryNext": "Siguiente",
"glp1": {
@@ -1792,7 +1840,7 @@
"cadence": {
"oneShotOn": "Dosis única el {date}.",
"oneShotPending": "Dosis única pendiente.",
- "asNeeded": "Según necesidad — sin plan fijo."
+ "asNeeded": "Según necesidad, sin plan fijo."
},
"intake": {
"title": "Historial de tomas",
@@ -1851,7 +1899,7 @@
"markAsTaken": "Marcar como tomada",
"markAsSkipped": "Marcar como omitida",
"adHocTag": "fuera de horario",
- "takenDetail": "prevista {planned} · tomada {taken}",
+ "takenDetail": "prevista {planned}, tomada {taken}",
"doseTaken": "Dosis: {dose}",
"status": {
"taken_on_time": "a tiempo",
@@ -1915,7 +1963,7 @@
},
"codes": {
"label": "Códigos clínicos (ATC / RxNorm)",
- "note": "Opcional. Se usan en la exportación FHIR del historial de salud. Los códigos se toman tal cual se introducen — nunca se adivinan.",
+ "note": "Opcional. Se usan en la exportación FHIR del historial de salud. Los códigos se toman tal cual se introducen; nunca se adivinan.",
"atcLabel": "ATC",
"rxNormLabel": "RxNorm",
"saved": "Códigos clínicos guardados",
@@ -1935,7 +1983,7 @@
"title": "Finalizar medicamento",
"helper": "Detiene los recordatorios y marca el medicamento como finalizado. El historial sigue siendo visible.",
"button": "Finalizar",
- "dialogTitle": "Finalizar {name} — detiene los recordatorios, el historial sigue visible",
+ "dialogTitle": "Finalizar {name}: detiene los recordatorios, el historial sigue visible",
"dialogBody": "No habrá más recordatorios. Las entradas antiguas permanecen en la línea de tiempo.",
"toast": "Medicamento finalizado",
"failed": "No se pudo finalizar el medicamento."
@@ -1973,7 +2021,7 @@
"copyFailed": "No se pudo copiar.",
"tokenCopied": "Token copiado",
"copyToken": "Copiar token",
- "mintedHint": "Copia este token ahora — solo se muestra una vez."
+ "mintedHint": "Copia este token ahora. Solo se muestra una vez."
},
"advanced": {
"dataPortability": {
@@ -2002,7 +2050,6 @@
"bestand": "Existencias",
"verlauf": "Historial",
"injektion": "Inyección",
- "api": "API",
"erweitert": "Avanzado",
"wirkung": "Efecto"
},
@@ -2014,9 +2061,9 @@
"statusTitle": "Estado",
"nextDoseLabel": "Próxima dosis",
"nextDoseNone": "No hay dosis pendiente.",
- "nextDoseAsNeeded": "Según necesidad — sin dosis programada.",
+ "nextDoseAsNeeded": "Según necesidad, sin dosis programada.",
"nextDoseNotTracked": "Sin registro de tomas, se guarda como registro.",
- "pausedHint": "En pausa — sin recordatorios.",
+ "pausedHint": "En pausa, sin recordatorios.",
"reminderLabel": "Recordatorio",
"reminderOn": "Activado",
"reminderOff": "Desactivado",
@@ -2209,9 +2256,8 @@
"tableStockDoses": "{count} dosis",
"tableStockRunway": "≈ {days} días restantes",
"cardLowStockRunway": "Reserva: ≈ {days} días",
- "takeActionFor": "Tomar – {name}",
- "skipActionFor": "Omitir – {name}",
- "reorderTitle": "Ajustar orden",
+ "takeActionFor": "Tomar: {name}",
+ "skipActionFor": "Omitir: {name}",
"reorderDescription": "Define el orden de tus medicamentos: se aplica a la vista de tarjetas y a la de tabla.",
"reorderSaved": "Orden guardado",
"reorderSaveFailed": "No se pudo guardar el orden",
@@ -2231,7 +2277,7 @@
"levelShift": "Se detectó un cambio de nivel en torno a {date}.",
"plateau": {
"title": "Meseta de peso con la dosis actual",
- "evidence": "Variación de peso de {delta} {unit} en los últimos {window} días ({readings} mediciones) — {days} días con {dose}."
+ "evidence": "Variación de peso de {delta} {unit} en los últimos {window} días ({readings} mediciones), {days} días con {dose}."
},
"retarget": {
"label": "¿No es la medición correcta?",
@@ -2356,7 +2402,7 @@
},
"charts": {
"loadingLabel": "Cargando la gráfica…",
- "loadingSlowHint": "Calculando los análisis — puede tardar un momento.",
+ "loadingSlowHint": "Calculando los análisis. Puede tardar un momento.",
"weight": "Peso",
"bloodPressure": "Tensión arterial",
"pulse": "Pulso",
@@ -2378,6 +2424,7 @@
"days7Label": "7 d",
"days7Title": "Últimos 7 días",
"days30Label": "30 d",
+ "daysNLabel": "{days} d",
"days30Title": "Últimos 30 días",
"days90Label": "90 d",
"days90Title": "Últimos 90 días",
@@ -2437,13 +2484,15 @@
"loadErrorTitle": "No se pudo cargar el gráfico",
"loadErrorReload": "Recargar",
"sparseDataCaption": "Más días completarán la tendencia.",
+ "tooltipHourlyMean": "{metric} (promedio de las horas)",
"dataTable": {
"toggle": "Puntos de datos ({count})",
"columnDate": "Fecha",
"noValue": "Sin valor",
"captionDay": "Una fila por día de {metric}: los mismos puntos que dibuja el gráfico.",
"captionWeek": "Una fila por semana de {metric}, la media de los días de esa semana: los mismos puntos que dibuja el gráfico.",
- "captionMonth": "Una fila por mes de {metric}, la media de los días de ese mes: los mismos puntos que dibuja el gráfico."
+ "captionMonth": "Una fila por mes de {metric}, la media de los días de ese mes: los mismos puntos que dibuja el gráfico.",
+ "hourlyMeanNote": "{metric} es el promedio de cada día, con cada hora contando una sola vez, para que un entrenamiento no pese más que el resto del día."
},
"personalBaseline": "Tu media",
"deltaVsBaseline": "{delta} vs. tu media",
@@ -2471,7 +2520,8 @@
"moodHeatmapEmpty": "Mapa de calor de estado de ánimo, aún no hay días registrados.",
"complianceHeatmap": "Mapa de calor de cumplimiento de medicación de {start} a {end}, {days} días activos.",
"complianceHeatmapEmpty": "Mapa de calor de cumplimiento de medicación, aún no hay días activos."
- }
+ },
+ "medicationsEmptyAction": "Añadir un medicamento"
},
"chart": {
"overlay": {
@@ -2502,8 +2552,8 @@
},
"tooltipPrior": "Periodo anterior",
"unavailable": {
- "lastMonth": "Comparación no disponible — sin datos del mes anterior",
- "lastYear": "Comparación no disponible — sin datos del año anterior"
+ "lastMonth": "Comparación no disponible: sin datos del mes anterior",
+ "lastYear": "Comparación no disponible: sin datos del año anterior"
},
"notAvailableForCompliance": "Comparación no disponible para el cumplimiento"
},
@@ -2515,7 +2565,9 @@
"dismissError": "No se pudo descartar este patrón.",
"restoreError": "No se pudo restaurar este patrón.",
"loadError": "No se pudieron cargar los patrones.",
- "empty": "Ningún patrón actual superó el umbral de evidencia."
+ "empty": "Ningún patrón actual superó el umbral de evidencia.",
+ "seasonalNote": "Desde la versión 1.42, HealthLog descuenta la estación del año y la tendencia a largo plazo antes de comparar valores. Verás menos relaciones, pero las que quedan son más sólidas. Muchos de los patrones anteriores eran solo el calendario.",
+ "seasonalNoteDismiss": "Cerrar aviso"
},
"correlation": {
"direction": {
@@ -2547,29 +2599,37 @@
"ENV_DAYLIGHT": "la luz natural",
"ENV_PRECIP": "las precipitaciones",
"ENV_PRESSURE_MEAN": "la presión atmosférica",
- "ENV_PRESSURE_DELTA": "la variación de presión del día"
+ "ENV_PRESSURE_DELTA": "la variación de presión del día",
+ "ENV_PM25": "las partículas finas (PM2,5)",
+ "ENV_OZONE_8H": "el ozono",
+ "ENV_POLLEN_MAX": "el polen"
},
"daily": {
- "high": "Cuando {behaviour} es mayor, {outcome} del día siguiente tiende a {direction} en tus datos — un patrón que conviene observar, no una causa.",
- "moderate": "Cuando {behaviour} es mayor, {outcome} del día siguiente parece tender a {direction}, según los datos disponibles — un patrón que conviene observar, no una causa.",
- "faint": "Cuando {behaviour} es mayor, apenas se insinúa —si acaso— que {outcome} del día siguiente tienda a {direction} en tus datos — demasiado leve para fiarse, nunca una causa."
+ "high": "Cuando {behaviour} es mayor, {outcome} del día siguiente tiende a {direction} en tus datos. Es un patrón que conviene observar, no una causa.",
+ "moderate": "Cuando {behaviour} es mayor, {outcome} del día siguiente parece tender a {direction}, según los datos disponibles. Es un patrón que conviene observar, no una causa.",
+ "faint": "Cuando {behaviour} es mayor, como mucho se insinúa en tus datos que {outcome} del día siguiente tienda a {direction}. Es demasiado leve para fiarse y nunca una causa."
},
"lab": {
- "default": "Unos valores de {lab} más altos coinciden con que {outcome} tienda a {direction} en los mismos periodos de tus datos — una asociación que conviene vigilar con tu médico, nunca una causa.",
- "faint": "Unos valores de {lab} más altos apenas insinúan —si acaso— que {outcome} tienda a {direction} en los mismos periodos de tus datos — demasiado leve para fiarse, una asociación que conviene comentar con tu médico, nunca una causa."
+ "default": "Unos valores de {lab} más altos coinciden con que {outcome} tienda a {direction} en los mismos periodos de tus datos, una asociación que conviene vigilar con tu médico, nunca una causa.",
+ "faint": "Unos valores de {lab} más altos apenas insinúan que {outcome} tienda a {direction} en los mismos periodos de tus datos. Es demasiado leve para fiarse y nunca una causa, pero conviene comentarlo con tu médico."
},
"hypothesis": {
"bpCompliance": {
- "support": "Una mayor adherencia a la medicación coincide con valores sistólicos más bajos — un patrón que conviene observar.",
- "surprising": "La adherencia y la sistólica se mueven juntas en tus datos — sorprendente; consulta con tu médico antes de cambiar nada.",
+ "support": "Una mayor adherencia a la medicación coincide con valores sistólicos más bajos, un patrón que conviene observar.",
+ "surprising": "La adherencia y la sistólica se mueven juntas en tus datos, algo sorprendente. Consulta con tu médico antes de cambiar nada.",
"none": "La adherencia y la sistólica no se mueven juntas de forma marcada en este periodo."
},
"moodPulse": {
- "support": "Los días de peor ánimo coinciden con un pulso en reposo más alto — un patrón que conviene observar.",
- "surprising": "Los días de mejor ánimo coinciden con un pulso en reposo más alto en tus datos — sorprendente.",
+ "support": "Los días de peor ánimo coinciden con un pulso en reposo más alto, un patrón que conviene observar.",
+ "surprising": "Los días de mejor ánimo coinciden con un pulso en reposo más alto en tus datos, algo sorprendente.",
"none": "El ánimo y el pulso en reposo no se mueven juntos de forma marcada en este periodo."
},
- "weightWeekday": "El peso de los {weekday} está {delta} kg {direction} tu media de los demás días — un patrón que conviene observar."
+ "weightWeekday": "El peso de los {weekday} está {delta} kg {direction} tu media de los demás días, un patrón que conviene observar."
+ },
+ "dailySameDay": {
+ "high": "Más {behaviour} el mismo día y el día anterior tiende a ir con {direction} {outcome} en tus datos: un patrón que conviene observar, no una causa.",
+ "moderate": "Más {behaviour} el mismo día y el día anterior parece ir con {direction} {outcome} en tus datos, según lo visto hasta ahora: un patrón que conviene observar, no una causa.",
+ "faint": "Más {behaviour} el mismo día y el día anterior muestra, como mucho, un leve indicio de {direction} {outcome} en tus datos: demasiado pequeño para fiarse, nunca una causa."
}
},
"noAnalysisYet": "Aún no hay valoración.",
@@ -2616,7 +2676,7 @@
"narrativeWeek": "Esta semana",
"narrativeMonth": "Este mes",
"narrativeUpdating": "Actualizando…",
- "heroFallbackSubtitle": "Una mirada diaria a tus tendencias — directamente de los datos que registras.",
+ "heroFallbackSubtitle": "Una mirada diaria a tus tendencias, directamente de los datos que registras.",
"heroGreetingMorning": "Buenos días",
"heroGreetingAfternoon": "Buenas tardes",
"heroGreetingEvening": "Buenas noches",
@@ -2681,7 +2741,7 @@
"referenceSleep": "Al menos {low} minutos por noche; el sueño más largo no se penaliza",
"referenceAdiposity": "Relación cintura-altura inferior a {high}",
"referenceHba1c": "HbA1c de {low} a {high} %",
- "referenceDetail": "{label} · {source} · {kind} · {low} a {high}",
+ "referenceDetail": "{label}: de {low} a {high} ({kind}, {source})",
"openBound": "abierto",
"laboratoryReferenceSource": "Intervalos de referencia del laboratorio",
"personalTargetSource": "Objetivo personal",
@@ -2716,7 +2776,8 @@
"coverage": {
"recommendedMet": "Cubre los tres ámbitos que la puntuación recomienda",
"areas": "{present} de {required} ámbitos de salud"
- }
+ },
+ "historyLink": "Evolución"
},
"dailyBriefing": {
"title": "Resumen del día",
@@ -2775,7 +2836,7 @@
"experimentCta": "Iniciar experimento de 7 días",
"experimentTooltip": "Próximamente",
"emptyTitle": "Necesitamos más datos para mostrar esta correlación",
- "sourceChip": "basado en {n} mediciones pareadas · {window}",
+ "sourceChip": "basado en {n} mediciones pareadas ({window})",
"sourceWindowLast30": "Últimos 30 días",
"confidenceHigh": "Confianza alta",
"confidenceModerate": "Confianza media",
@@ -2790,13 +2851,14 @@
}
},
"coach": {
- "refusalOutOfScope": "Solo puedo ayudarte con los datos de salud de tu registro — mediciones, medicamentos, estado de ánimo, tendencias. Preguntas fuera de ese ámbito no las respondo.",
- "refusalInjection": "Tu mensaje contiene instrucciones que pretenden sobrescribir mis indicaciones. Soy el coach de HealthLog y solo resumo tus propios datos de salud — reformula tu pregunta dentro de ese marco.",
- "errorBudget": "Has alcanzado el límite diario del coach. El cupo se reinicia a medianoche (UTC).",
+ "refusalOutOfScope": "Solo puedo ayudarte con los datos de salud de tu registro: mediciones, medicamentos, estado de ánimo, tendencias. Preguntas fuera de ese ámbito no las respondo.",
+ "refusalInjection": "Tu mensaje contiene instrucciones que pretenden sobrescribir mis indicaciones. Soy el coach de HealthLog y solo resumo tus propios datos de salud. Reformula tu pregunta dentro de ese marco.",
+ "errorBudget": "Has alcanzado el límite diario del coach. El cupo se reinicia a medianoche en tu zona horaria.",
"errorProvider": "El coach no ha podido contactar con un proveedor de análisis ahora mismo. Inténtalo en un momento.",
+ "errorDemo": "En la demo, el Coach no responde.",
"errorNoProvider": "El coach lee tus datos de salud registrados y responde preguntas sobre ellos (no es consejo médico). HealthLog necesita un proveedor de IA para ello: conecta tu propia clave o un modelo local.",
"errorNoProviderAction": "Conectar un proveedor de IA",
- "errorNetwork": "Sin conexión a Internet — inténtalo de nuevo cuando vuelvas a estar en línea.",
+ "errorNetwork": "Sin conexión a Internet. Inténtalo de nuevo cuando vuelvas a estar en línea.",
"prefsSaveError": "No se pudieron guardar tus preferencias del Coach. Inténtalo de nuevo.",
"errorCredentialExpired": "Tu conexión con el proveedor de IA ha caducado. Vuelve a conectarla en Ajustes para que el Coach siga funcionando.",
"tagline": "Coach personal de salud, apoyado en tus datos.",
@@ -2815,7 +2877,7 @@
"thinking": "Pensando…",
"unverifiedFiguresNotice": "Algunas cifras no se pudieron contrastar con tus datos y se omitieron.",
"composerPlaceholder": "Pregúntame algo …",
- "nudgeBubbleLabel": "Nuevo mensaje del coach — abrir el chat",
+ "nudgeBubbleLabel": "Nuevo mensaje del coach: abrir el chat",
"fabLabel": "Abrir el coach",
"readAloud": "Leer en voz alta",
"readAloudStop": "Detener lectura",
@@ -2833,14 +2895,13 @@
},
"rememberMessage": {
"action": "Recordar",
- "done": "Guardado en tu información personal",
- "duplicate": "Ya está en tu información personal",
- "failed": "No se pudo guardar. Inténtalo de nuevo."
+ "failed": "No se pudo guardar. Inténtalo de nuevo.",
+ "alreadyKept": "El Coach ya lo recuerda."
},
"guided": {
"offerTitle": "El Coach tiene {count} preguntas para ti",
"offerTitleOne": "El Coach tiene una pregunta para ti",
- "offerBody": "Respuestas breves afinan tu información personal — una pregunta a la vez, aquí en el chat.",
+ "offerBody": "Respuestas breves afinan tu información personal: una pregunta a la vez, aquí en el chat.",
"offerStart": "Responder ahora",
"offerLater": "Más tarde",
"offerDismiss": "No volver a preguntar",
@@ -2850,7 +2911,7 @@
"later": "Más tarde",
"dismissRemaining": "No volver a preguntar",
"answerPlaceholder": "Tu respuesta…",
- "summaryTitle": "¡Listo — gracias por tus respuestas!",
+ "summaryTitle": "¡Listo, gracias por tus respuestas!",
"summaryAnswered": "{answered} de {total} respondidas",
"summaryAdopted": "{adopted} añadidas a tu información personal",
"summaryAdoptedNone": "no se añadió nada nuevo a tu información personal",
@@ -2862,6 +2923,7 @@
"historyEmpty": "Tus conversaciones aparecen aquí. Lanza una pregunta para empezar.",
"historyDeleteAria": "Eliminar conversación",
"historyDeleted": "Conversación eliminada",
+ "historyDeleteFailed": "No se pudo eliminar la conversación.",
"rename": {
"action": "Renombrar conversación",
"input": "Título de la conversación",
@@ -2875,7 +2937,6 @@
"sourcesFresh": "Actuales",
"sourcesStale": "Desactualizados",
"sourcesFooter": "El coach solo lee los datos que has conectado.",
- "evidenceLabel": "¿En qué se basa esto?",
"metric": {
"bp": "Presión arterial",
"weight": "Peso",
@@ -2921,14 +2982,6 @@
"skin_temp": "Temperatura de la piel",
"workouts": "Entrenamientos"
},
- "window": {
- "last7days": "Últimos 7 días",
- "last30days": "Últimos 30 días",
- "last90days": "Últimos 90 días",
- "lastYear": "últimos 12 meses",
- "allTime": "todo el tiempo (12 meses en detalle)"
- },
- "windowLabel": "Periodo",
"settingsAriaLabel": "Ajustes del coach",
"settingsTitle": "Ajustes del coach",
"settingsDescription": "Configura cómo te habla el coach.",
@@ -2941,8 +2994,9 @@
"settingsVerbosityDefault": "Estándar",
"settingsVerbosityDetailed": "Detallado",
"settingsVerbosityConciseHint": "El tono conciso ya mantiene las respuestas cortas: la extensión queda fijada en «Breve» mientras esté activo.",
- "settingsSourcesPointer": "Los grupos de datos que el coach puede usar y la ventana de análisis predeterminada se gestionan directamente en el chat del coach (panel «En qué me baso»).",
- "settingsSourcesPointerLink": "Abrir el coach",
+ "settingsSourcesSummary": "El coach ve {count} de {total} áreas de datos y mira {window} atrás.",
+ "settingsSourcesSummaryAll": "El coach ve {count} de {total} áreas de datos y todo el historial.",
+ "settingsSourcesPointerLink": "Abrir ajustes del coach",
"settingsExcludeLabel": "Excluir métricas",
"settingsExcludeHint": "El coach no verá los datos de las métricas seleccionadas.",
"settingsContextLabel": "Enviar contexto",
@@ -2954,34 +3008,30 @@
"settingsSave": "Guardar",
"settingsSaved": "Guardado",
"settingsCancel": "Cancelar",
- "feedbackHelpful": "Útil",
- "feedbackUnhelpful": "Poco útil",
- "feedbackThanks": "Gracias por la indicación.",
- "feedbackError": "No se pudo guardar el comentario",
"dailyLimitTitle": "Límite diario alcanzado",
- "dailyLimitBody": "Límite diario alcanzado. El cupo se reinicia a medianoche (UTC).",
+ "dailyLimitBody": "Límite diario alcanzado. El cupo se reinicia a medianoche en tu zona horaria.",
"providerRateLimitTitle": "Proveedor con límite de tasa",
"providerRateLimitBody": "El proveedor está temporalmente saturado; reinicio en ~5 min.",
"cluster": {
"cardio": {
"label": "Cardiovascular",
- "hint": "Presión · pulso · VFC · FC en reposo y al caminar"
+ "hint": "Presión, pulso, VFC, FC en reposo y al caminar"
},
"body": {
"label": "Composición corporal",
- "hint": "Peso · grasa · músculo · IMC"
+ "hint": "Peso, grasa, músculo e IMC"
},
"activity": {
"label": "Actividad",
- "hint": "Pasos · energía · distancia · VO₂ máx"
+ "hint": "Pasos, energía, distancia y VO₂ máx"
},
"workouts": {
"label": "Entrenamientos",
- "hint": "Carreras · paseos · sesiones"
+ "hint": "Carreras, paseos y sesiones"
},
"sleep": {
"label": "Sueño",
- "hint": "Duración · fases"
+ "hint": "Duración y fases"
},
"mood": {
"label": "Estado de ánimo",
@@ -2993,56 +3043,68 @@
},
"medication": {
"label": "Medicación",
- "hint": "Adherencia · contexto GLP-1"
+ "hint": "Adherencia y contexto GLP-1"
},
"mobility": {
"label": "Movilidad y marcha",
- "hint": "Estabilidad · asimetría · marcha"
+ "hint": "Estabilidad, asimetría y marcha"
},
"environment": {
"label": "Entorno",
- "hint": "Ruido · luz del día · temp. piel"
+ "hint": "Ruido, luz del día y temperatura de la piel"
}
},
"settingsClustersLabel": "Datos que ve el Coach",
"settingsClustersHint": "Elige qué grupos de datos puede leer el Coach. Más datos significan respuestas más ricas y un prompt mayor.",
"settingsClusterNoData": "Aún no hay datos",
- "sourcesMemberCount": "{count} métricas",
+ "sourcesMemberCountOne": "1 métrica",
+ "sourcesMemberCountFew": "{count} métricas",
+ "sourcesMemberCountOther": "{count} métricas",
+ "lookback": {
+ "label": "Hasta dónde puede mirar atrás el Coach",
+ "option": {
+ "last7days": "{days} días",
+ "last30days": "{days} días",
+ "last90days": "{days} días",
+ "lastYear": "{months} meses",
+ "allTime": "Todo"
+ },
+ "detailDays": "El Coach no lee nada con más de {days} días de antigüedad, en ninguna respuesta.",
+ "detailMonths": "El Coach no lee nada con más de {months} meses de antigüedad, en ninguna respuesta.",
+ "detailAll": "Todo lo que has registrado: {dailyMonths} meses al detalle diario, las mediciones más antiguas como valores mensuales hasta {years} años atrás, los análisis de los últimos {labMonths} meses.",
+ "note": "Tu perfil, tu medicación actual, una enfermedad en curso y las próximas citas siguen visibles; los resúmenes que necesitan más historial se acortan o se omiten. Las respuestas que el Coach dio antes en conversaciones anteriores pueden seguir citando valores más antiguos."
+ },
"showConversations": "Mostrar conversaciones",
"hideConversations": "Ocultar conversaciones",
"heroGreeting": "Pregúntame lo que quieras sobre tus datos",
"heroSubline": "Pregunta por tus tendencias, medicamentos o mediciones.",
- "thinkingElapsed": "{seconds}s",
"thinkingDuration": "Pensó durante {seconds}s",
"thinkingExpandAria": "Mostrar lo que el Coach tuvo en cuenta",
"thinkingDetail": "Respondido a partir de tu resumen de salud.",
"tokensUsed": "{count} tokens",
- "tokensUsedWithModel": "{count} tokens · {model}",
"askAboutThis": "Pregúntale al coach sobre esto",
"readStrip": {
"label": "Rango habitual",
"baselineWithin": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa dentro de él.",
"baselineAbove": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa por encima.",
"baselineBelow": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa por debajo.",
- "insufficient": "Todavía estoy aprendiendo tu rango habitual — sigue registrando unos días más.",
+ "sameHoursWithin": "Hasta esta hora del día sueles estar en {low}–{high} {unit}; hoy, por ahora, {value} está dentro.",
+ "sameHoursAbove": "Hasta esta hora del día sueles estar en {low}–{high} {unit}; hoy, por ahora, {value} está por encima.",
+ "sameHoursBelow": "Hasta esta hora del día sueles estar en {low}–{high} {unit}; hoy, por ahora, {value} está por debajo.",
+ "steadyWithin": "Tus valores suelen estar en {low} {unit}; hoy {value} coincide.",
+ "steadyAbove": "Tus valores suelen estar en {low} {unit}; hoy {value} está por encima.",
+ "steadyBelow": "Tus valores suelen estar en {low} {unit}; hoy {value} está por debajo.",
+ "latestWithin": "Tu rango habitual es {low}–{high} {unit}; tu última medición, del {date}, fue {value}, dentro de él.",
+ "latestAbove": "Tu rango habitual es {low}–{high} {unit}; tu última medición, del {date}, fue {value}, por encima.",
+ "latestBelow": "Tu rango habitual es {low}–{high} {unit}; tu última medición, del {date}, fue {value}, por debajo.",
+ "steadyLatestWithin": "Tus valores suelen estar en {low} {unit}; tu última medición, del {date}, fue {value} y coincide.",
+ "steadyLatestAbove": "Tus valores suelen estar en {low} {unit}; tu última medición, del {date}, fue {value}, por encima.",
+ "steadyLatestBelow": "Tus valores suelen estar en {low} {unit}; tu última medición, del {date}, fue {value}, por debajo.",
+ "insufficient": "Todavía estoy aprendiendo tu rango habitual. Sigue registrando unos días más.",
"driver": "{note}"
},
"scope": {
- "prefix": "El Coach ya está en {metric}",
- "tap": "Toca para preguntar",
- "question": "Explícame esta métrica — ¿hay algo que deba vigilar?"
- },
- "seeded": {
- "prefix": "Hoy merece un vistazo: {signal}",
- "signal": {
- "readiness": "disposición",
- "recovery": "recuperación"
- },
- "question": {
- "readiness": "Mi disposición está rara hoy — ¿a qué se debe?",
- "recovery": "Mi recuperación ha cambiado — ¿qué hay detrás?"
- },
- "dismiss": "Descartar"
+ "prefix": "El Coach ya está en {metric}"
},
"settings": "Ajustes",
"history": {
@@ -3052,31 +3114,31 @@
"groupEarlier": "Anteriores"
},
"seed": {
- "bloodPressure": "Repasa conmigo la tendencia de mi presión arterial de los últimos 30 días — ¿hay algo que deba vigilar?",
+ "bloodPressure": "Repasa conmigo la tendencia de mi presión arterial de los últimos 30 días. ¿Hay algo que deba vigilar?",
"weight": "¿Cómo ha evolucionado mi peso últimamente y la dirección es algo sobre lo que actuar?",
"bmi": "¿Qué me dice mi tendencia de IMC y cómo debería interpretarla?",
- "pulse": "Repasa conmigo mis lecturas de pulso — ¿hay algo fuera de lo normal?",
+ "pulse": "Repasa conmigo mis lecturas de pulso. ¿Hay algo fuera de lo normal?",
"restingHr": "¿Cómo ha evolucionado mi frecuencia cardíaca en reposo y qué dice sobre mi forma física?",
"hrv": "¿Qué me dice mi variabilidad de la frecuencia cardíaca sobre la recuperación y el estrés últimamente?",
- "sleep": "Repasa conmigo mi sueño reciente — duración, regularidad y cualquier cosa que valga la pena cambiar.",
+ "sleep": "Repasa conmigo mi sueño reciente: duración, regularidad y cualquier cosa que valga la pena cambiar.",
"mood": "¿Qué patrones ves en mi estado de ánimo durante las últimas semanas?",
"medications": "¿Cómo va mi adherencia a la medicación y coincide con cómo me he sentido?",
"steps": "¿Qué tan activo he estado últimamente según mis pasos y cómo se compara con un nivel de referencia saludable?",
"activeEnergy": "¿Qué dice mi tendencia de energía activa sobre mi nivel de actividad últimamente?",
"cardioFitness": "¿Qué significa la tendencia de mi capacidad cardiorrespiratoria (VO₂ máx.) y cómo la llevo en la dirección correcta?",
- "bloodGlucose": "Repasa conmigo mis últimas lecturas de glucosa — ¿están en un rango saludable?",
+ "bloodGlucose": "Repasa conmigo mis últimas lecturas de glucosa. ¿Están en un rango saludable?",
"oxygenSaturation": "¿Qué me dice mi tendencia de oxígeno en sangre (SpO₂)?",
"respiratoryRate": "¿Qué dice mi tendencia de frecuencia respiratoria sobre mi salud?",
- "workouts": "Repasa conmigo mis entrenamientos recientes — carga, frecuencia y cómo me recupero entre ellos.",
+ "workouts": "Repasa conmigo mis entrenamientos recientes: carga, frecuencia y cómo me recupero entre ellos.",
"recoveryPage": "¿Por qué mi recuperación está donde está ahora mismo y qué la está provocando?",
- "briefing": "Repasa conmigo el resumen de hoy — ¿en qué debería centrarme y hay algo en lo que valga la pena actuar?",
+ "briefing": "Repasa conmigo el resumen de hoy. ¿En qué debería centrarme y hay algo en lo que valga la pena actuar?",
"healthScore": "¿Por qué mi puntuación de salud es {score} de 100 y qué la mejoraría más?",
- "periodWeek": "Repasa conmigo cómo fue mi última semana en mis métricas de salud — ¿qué destaca?",
- "periodMonth": "Repasa conmigo cómo fue mi último mes en mis métricas de salud — ¿qué destaca?",
+ "periodWeek": "Repasa conmigo cómo fue mi última semana en mis métricas de salud. ¿Qué destaca?",
+ "periodMonth": "Repasa conmigo cómo fue mi último mes en mis métricas de salud. ¿Qué destaca?",
"correlationBpCompliance": "¿Cómo se relaciona mi adherencia a la medicación con mi presión arterial? Explícame qué significa esta correlación para mí.",
"correlationMoodPulse": "¿Existe una relación real entre mi estado de ánimo y mi pulso? Ayúdame a interpretar esta correlación.",
"correlationWeightWeekday": "¿Mi peso sigue un patrón semanal? ¿Qué me dice esta correlación por día de la semana?",
- "recommendation": "Cuéntame más sobre esta recomendación: \"{text}\" — ¿por qué se aplica a mí y qué debería hacer al respecto?"
+ "recommendation": "Cuéntame más sobre esta recomendación: \"{text}\". ¿Por qué se aplica a mí y qué debería hacer al respecto?"
},
"assessmentPrompt": "Explícame mi evaluación de {metric}: ¿qué significa y qué debería hacer al respecto?",
"attach": {
@@ -3111,7 +3173,102 @@
"errorNotPermitted": "El Coach no está disponible dentro de un registro que alguien comparte contigo.",
"errorUnavailable": "El Coach no pudo comprobar su configuración ahora. Inténtalo de nuevo en un momento.",
"errorOperatorOff": "La IA está desactivada en este servidor, así que el Coach no puede responder.",
- "errorCoachOff": "El Coach está desactivado. Puedes volver a activarlo en Ajustes, Módulos."
+ "errorCoachOff": "El Coach está desactivado. Puedes volver a activarlo en Ajustes, Módulos.",
+ "frame": {
+ "showPanel": "Mostrar conversaciones",
+ "hidePanel": "Ocultar conversaciones",
+ "panelDescription": "Tus conversaciones con el coach, las más recientes primero.",
+ "rowActions": "Acciones para {title}",
+ "rename": "Renombrar",
+ "delete": "Eliminar",
+ "modelSection": "Modelo",
+ "provider": "Responde con",
+ "modelManaged": "El modelo de este proveedor se configura en los ajustes de IA.",
+ "noProvider": "Ningún proveedor disponible",
+ "saving": "Guardando…",
+ "saveFailed": "No se pudo guardar el cambio.",
+ "allSettings": "Todos los ajustes de IA",
+ "providerNotActive": "{provider} no pudo encargarse: no tiene un inicio de sesión ni una clave que funcionen. Configúralo en los ajustes de IA.",
+ "operatorCodex": "ChatGPT (compartido por el operador)",
+ "operatorManaged": "El operador de tu servidor gestiona esta conexión. Cuando configures uno de tus propios proveedores, responderá primero."
+ },
+ "answer": {
+ "scrollToBottom": "Ir al último mensaje",
+ "rememberDone": "Guardado",
+ "timeYesterday": "ayer {time}",
+ "timeEarlier": "{date}, {time}",
+ "info": "Detalles de la respuesta",
+ "infoModel": "Modelo: {model}"
+ },
+ "activity": {
+ "thinking": "Pensando…",
+ "fetching": "Consultando {domain}, {window}…",
+ "digestOne": "Resumiendo {count} valor…",
+ "digestFew": "Resumiendo {count} valores…",
+ "digestOther": "Resumiendo {count} valores…",
+ "digestDoneOne": "{count} valor de {areas}",
+ "digestDoneFew": "{count} valores de {areas}",
+ "digestDoneOther": "{count} valores de {areas}",
+ "areasOne": "{count} área",
+ "areasFew": "{count} áreas",
+ "areasOther": "{count} áreas",
+ "memoryOne": "Recuerda {count} cosa que le contaste",
+ "memoryFew": "Recuerda {count} cosas que le contaste",
+ "memoryOther": "Recuerda {count} cosas que le contaste",
+ "remember": "Recordando algo…",
+ "plan": "Preparando un plan…",
+ "asking": "Te pregunta…",
+ "answer": "Escribiendo la respuesta…",
+ "stop": {
+ "budget": "Presupuesto alcanzado, responde con lo que tiene",
+ "time": "Tiempo agotado, responde con lo que tiene",
+ "cap": "Muchos pasos, ahora lo resume",
+ "noProgress": "No hay datos nuevos, lo resume"
+ },
+ "thoughtProcessOne": "Razonamiento ({count} paso)",
+ "thoughtProcessFew": "Razonamiento ({count} pasos)",
+ "thoughtProcessOther": "Razonamiento ({count} pasos)",
+ "awaitingAnswer": "Se necesita tu respuesta"
+ },
+ "memory": {
+ "saved": "Recordado: {fact}",
+ "undo": "Deshacer",
+ "accept": "Sí, recuérdalo",
+ "decline": "No",
+ "confirmed": "Entendido, lo recordaré.",
+ "declined": "De acuerdo, no lo haré."
+ },
+ "plan": {
+ "accept": "Adoptar este plan",
+ "decline": "Ahora no",
+ "confirmed": "Anotado. Lo revisaremos juntos dentro de {days} días.",
+ "declined": "De acuerdo, ahora no."
+ },
+ "assumption": {
+ "line": "Supuesto: {value}",
+ "change": "Cambiar"
+ },
+ "suggestedReplies": {
+ "groupLabel": "Respuestas sugeridas"
+ },
+ "chart": {
+ "compare": {
+ "periods": "{a} frente a {b}"
+ }
+ },
+ "reasoning": {
+ "label": "Profundidad de razonamiento",
+ "hint": "Pensar más a fondo significa respuestas más precisas y un poco más de espera.",
+ "off": "Desactivado",
+ "minimal": "Mínimo",
+ "low": "Bajo",
+ "medium": "Medio",
+ "high": "Alto",
+ "capped": "(limitado por el administrador)",
+ "disabled": "El administrador ha desactivado el razonamiento profundo."
+ },
+ "memoryLink": "Memoria ({count})",
+ "newConversation": "Nueva conversación"
},
"relativeJustNow": "ahora mismo",
"relativeMinutesAgoOne": "hace {count} minuto",
@@ -3174,7 +3331,7 @@
},
"hrv": {
"title": "VFC",
- "description": "Variabilidad de la frecuencia cardíaca a lo largo del tiempo — señal de recuperación del sistema nervioso, captada por la noche por Apple Watch y otros wearables compatibles.",
+ "description": "Variabilidad de la frecuencia cardíaca a lo largo del tiempo: señal de recuperación del sistema nervioso, captada por la noche por Apple Watch y otros wearables compatibles.",
"chartTitle": "Variabilidad de la frecuencia cardíaca",
"emptyState": {
"title": "Aún no hay valores de VFC",
@@ -3183,7 +3340,7 @@
},
"restingHr": {
"title": "FC en reposo",
- "description": "El nivel basal diario del corazón — distinto del pulso puntual de la página de pulso. Apple Salud aporta un valor al día.",
+ "description": "El nivel basal diario del corazón, distinto del pulso puntual de la página de pulso. Apple Salud aporta un valor al día.",
"chartTitle": "Frecuencia cardíaca en reposo",
"emptyState": {
"title": "Aún no hay FC en reposo",
@@ -3193,7 +3350,7 @@
},
"oxygenSaturation": {
"title": "Saturación de oxígeno",
- "description": "Oxígeno en sangre (SpO₂) — rango clínico normal 95–100 %, con zonas objetivo sombreadas en el gráfico.",
+ "description": "Oxígeno en sangre (SpO₂): rango clínico normal 95–100 %, con zonas objetivo sombreadas en el gráfico.",
"chartTitle": "Saturación de oxígeno",
"emptyState": {
"title": "Aún no hay muestras de SpO₂",
@@ -3206,13 +3363,13 @@
"chartTitle": "Temperatura corporal",
"emptyState": {
"title": "Aún no hay temperatura corporal",
- "description": "Registra una lectura o deja que tu wearable sincronice una — la tendencia aparecerá aquí.",
+ "description": "Registra una lectura o deja que tu wearable sincronice una, y la tendencia aparecerá aquí.",
"cta": "Registrar temperatura corporal"
}
},
"steps": {
"title": "Pasos",
- "description": "Recuento diario de pasos — una fila por día, desde Apple Salud o un dispositivo de actividad conectado.",
+ "description": "Recuento diario de pasos: una fila por día, desde Apple Salud o un dispositivo de actividad conectado.",
"chartTitle": "Pasos por día",
"emptyState": {
"title": "Aún no hay datos de pasos",
@@ -3221,7 +3378,7 @@
},
"activeEnergy": {
"title": "Energía activa",
- "description": "Total diario de energía activa en kilocalorías — una fila por día, desde Apple Salud o la actividad de Withings.",
+ "description": "Total diario de energía activa en kilocalorías: una fila por día, desde Apple Salud o la actividad de Withings.",
"chartTitle": "Energía activa",
"emptyState": {
"title": "Aún no hay datos de energía activa",
@@ -3288,7 +3445,7 @@
"breathingDisturbancesBody": "Un wearable puede estimar con qué frecuencia se interrumpió tu respiración mientras dormías. Un índice más bajo es mejor; un nivel marcado como elevado, sobre todo durante muchas noches, conviene comentarlo con un profesional. Es una aproximación de cribado, no un diagnóstico de apnea del sueño.",
"stepsBody": "El recuento de pasos es el número total de pasos que das cada día: la medida más sencilla del movimiento cotidiano. Un hábito constante importa más que un único día destacado, y la meta redonda de 10.000 pasos es un eslogan publicitario, no una conclusión de la investigación.",
"readinessBody": "La disposición es una lectura diaria de lo listo que está tu cuerpo para afrontar el día, combinando la frecuencia cardíaca en reposo, la variabilidad de la frecuencia cardíaca y el sueño. Es una aproximación del dispositivo respecto a tu propia base, no una medida clínica: una puntuación más alta indica recuperación, una más baja es un aviso suave para bajar el ritmo.",
- "sleepScoreBody": "Tu puntuación de sueño resume toda una noche en un solo número — duración, eficiencia, regularidad y el equilibrio entre sueño ligero, profundo y REM. Fíjate en el patrón a lo largo de las semanas en lugar de perseguir una puntuación perfecta una sola noche.",
+ "sleepScoreBody": "Tu puntuación de sueño resume toda una noche en un solo número: duración, eficiencia, regularidad y el equilibrio entre sueño ligero, profundo y REM. Fíjate en el patrón a lo largo de las semanas en lugar de perseguir una puntuación perfecta una sola noche.",
"recoveryBody": "La recuperación refleja lo bien que tu cuerpo se ha repuesto, a partir de señales nocturnas como la variabilidad de la frecuencia cardíaca y la frecuencia en reposo. Una puntuación alta es luz verde para exigirte; una más baja sugiere que tu organismo aún se está reconstruyendo.",
"strainBody": "El esfuerzo capta cuánta carga cardiovascular y física le has impuesto a tu cuerpo, desde los entrenamientos hasta un día activo de pie. Cuando ya hay suficiente historial, se valora frente a tu propio esfuerzo habitual, no frente a un estándar ajeno.",
"stressBody": "El estrés aquí es una estimación de la carga fisiológica sobre tu sistema nervioso, leída sobre todo a partir de patrones de variabilidad de la frecuencia cardíaca y no de cómo te sientes conscientemente. Una puntuación más baja apunta a un estado calmado y bien regulado; una más alta es una invitación a incorporar recuperación.",
@@ -3311,7 +3468,7 @@
"mean": "Media"
},
"showAllValues": "Mostrar todas las mediciones",
- "valuesPageTitle": "{metric} — todas las mediciones",
+ "valuesPageTitle": "{metric}: todas las mediciones",
"valuesPageDescription": "Cada medición detrás de esta métrica. Edita o elimina una entrada directamente.",
"diversity": {
"trigger": "Consejo de medición",
@@ -3351,11 +3508,11 @@
"inBed": "En la cama",
"nap": "Siesta",
"napCount": "{count} siestas",
- "unavailable": "Aún no hay datos de fases — las fases de sueño aparecerán aquí en cuanto una fuente conectada las proporcione."
+ "unavailable": "Aún no hay datos de fases. Las fases de sueño aparecerán aquí en cuanto una fuente conectada las proporcione."
},
"empty": {
"title": "Aún no hay datos de sueño",
- "description": "Conecta una fuente como Apple Health o un wearable — o registra el sueño manualmente — y esta vista se llenará automáticamente.",
+ "description": "Conecta una fuente como Apple Health o un wearable (o registra el sueño manualmente) y esta vista se llenará automáticamente.",
"action": "Gestionar dispositivos"
},
"debt": {
@@ -3364,21 +3521,21 @@
"caption": "Balance pendiente de las últimas {nights} noches",
"clearValue": "Al día",
"clearCaption": "Nada pendiente en las últimas {nights} noches",
- "learning": "Aún estamos midiendo tu sueño — {counted} noches hasta ahora, faltan {remaining} para un balance fiable.",
+ "learning": "Aún estamos midiendo tu sueño: {counted} noches hasta ahora, faltan {remaining} para un balance fiable.",
"computedInfo": "Esta es la estimación propia de HealthLog: un balance móvil de la necesidad menos el sueño de las últimas noches, que el sueño de recuperación va reduciendo. Puede diferir de la cifra de deuda de sueño nativa de un wearable, que usa su propio modelo de necesidad."
},
"average": {
"title": "Media por noche",
"value": "{hours} h {minutes} min",
"caption": "Sueño medio en las últimas {nights} noches",
- "learning": "Aún estamos midiendo tu sueño — {counted} noches hasta ahora, faltan {remaining} para una media fiable."
+ "learning": "Aún estamos midiendo tu sueño: {counted} noches hasta ahora, faltan {remaining} para una media fiable."
},
"chronotype": {
"title": "Cronotipo",
- "learning": "Aún estamos aprendiendo tu ritmo — {counted} de {total} noches libres. Un cronotipo necesita varias mañanas sin despertador para tener sentido.",
+ "learning": "Aún estamos aprendiendo tu ritmo: {counted} de {total} noches libres. Un cronotipo necesita varias mañanas sin despertador para tener sentido.",
"advancedToggle": "Avanzado",
"socialJetlagValue": "{hours} h {minutes} min de jet lag social",
- "socialJetlagCaption": "La diferencia entre el horario de sueño laboral y el de los días libres — como un pequeño cambio de huso horario semanal.",
+ "socialJetlagCaption": "La diferencia entre el horario de sueño laboral y el de los días libres, como un pequeño cambio de huso horario semanal.",
"msfScValue": "Punto medio corregido {clock}",
"msfScCaption": "Ajustado por el sueño que recuperas el fin de semana (MSFsc).",
"dayTypeNote": "Se asume que los días libres son los fines de semana hasta que se añada el registro del tipo de día.",
@@ -3567,7 +3724,10 @@
"endInFuture": "Terminaría en el futuro. Pon como inicio la hora en que empezó el entrenamiento.",
"distanceInvalid": "Introduce la distancia como un número dentro de un rango realista.",
"energyInvalid": "Introduce la energía activa como un número de hasta 20.000 kcal."
- }
+ },
+ "editTitle": "Editar entrenamiento",
+ "editAction": "Editar",
+ "updated": "Entrenamiento actualizado"
}
},
"bloodGlucose": {
@@ -3741,7 +3901,7 @@
"navSkinTemperature": "Temp. piel",
"pulseWaveVelocity": {
"title": "Velocidad de onda de pulso",
- "description": "Tu velocidad de onda de pulso arterial en el tiempo, en m/s — valores más altos sugieren arterias más rígidas.",
+ "description": "Tu velocidad de onda de pulso arterial en el tiempo, en m/s. Valores más altos sugieren arterias más rígidas.",
"chartTitle": "Velocidad de onda de pulso",
"emptyState": {
"title": "Aún no hay datos",
@@ -3891,7 +4051,6 @@
"navWalkingSpeed": "Velocidad al andar",
"mood": {
"logLink": "Registrar un estado de ánimo",
- "mentalWellbeingLink": "Hacer un chequeo de bienestar mental",
"heatmapTitle": "Calendario de ánimo",
"heatmapNoEntry": "Sin registro",
"legendGreat": "Genial",
@@ -3902,7 +4061,7 @@
"distributionDaysUnit": "días",
"weekdayTitle": "Promedio por día de la semana",
"weekdayNoData": "Sin datos",
- "weekdayBestWorst": "Mejor día: {best} · Día más difícil: {worst}",
+ "weekdayBestWorst": "Mejor día: {best}, día más difícil: {worst}",
"tagsTitle": "Etiquetas y desencadenantes",
"tagAvgMood": "Ánimo promedio con esta etiqueta",
"correlationsTitle": "Ánimo y otras métricas",
@@ -3918,7 +4077,7 @@
"strengthModerate": "Moderada",
"strengthWeak": "Débil",
"strengthNone": "Ninguna",
- "source": "{n} días emparejados · r = {r}",
+ "source": "{n} días emparejados, r = {r}",
"emptyTitle": "Aún no hay suficientes datos emparejados",
"emptyDescription": "Registra el ánimo y esta métrica los mismos días para ver una correlación.",
"pendingDescription": "{n} de {needed} días emparejados. La correlación aparece cuando hay días suficientes para distinguir un patrón del azar.",
@@ -3977,7 +4136,7 @@
"crosstab": {
"title": "Etiquetas y tus métricas de salud",
"description": "Cómo difiere una métrica de salud en los días que etiquetaste cada actividad, frente a los días que no.",
- "pairLabel": "{tag} · {metric}",
+ "pairLabel": "{metric} en días con {tag}",
"metricActiveEnergy": "Energía activa",
"metricSleepDuration": "Sueño",
"metricNextDayRecovery": "Recuperación al día siguiente",
@@ -3994,7 +4153,7 @@
"factorCrosstab": {
"title": "Tus factores valorados y tus constantes",
"description": "Cómo difiere una constante en los días en que valoraste bajo cada factor frente a los días en que lo valoraste alto.",
- "pairLabel": "{factor} · {metric}",
+ "pairLabel": "{metric} en días con {factor}",
"metricSleepDuration": "Sueño",
"metricSteps": "Pasos",
"metricRestingHeartRate": "Frecuencia cardíaca en reposo",
@@ -4020,7 +4179,7 @@
"moodDown": "Un mayor {factor} suele coincidir con peor ánimo al día siguiente en tus datos.",
"outcomeUp": "Mejor ánimo suele coincidir con un {factor} más alto al día siguiente en tus datos.",
"outcomeDown": "Mejor ánimo suele coincidir con un {factor} más bajo al día siguiente en tus datos.",
- "stat": "{n} días emparejados · r = {r} · q = {q}",
+ "stat": "{n} días emparejados, r = {r}, q = {q}",
"statLabel": "Cómo se calculó esto",
"explainerLabel": "Cómo se calculó esto"
},
@@ -4064,15 +4223,15 @@
},
"derived": {
"coverage": {
- "summary": "{present} de {required} entradas · {percent}%",
+ "summary": "{present} de {required} entradas ({percent}%)",
"ratioLabel": "{present}/{required}",
"historyDays": "Basado en {count} días de historial",
"missing": "Registra estos para afinar el valor: {list}",
- "summaryLabelled": "{label} · {percent}%"
+ "summaryLabelled": "{label} ({percent}%)"
},
"scoreRing": {
"aria": "Puntuación {score} de 100, {band}",
- "ariaProvisional": "Puntuación aún no disponible — datos insuficientes",
+ "ariaProvisional": "Puntuación aún no disponible: datos insuficientes",
"provisionalCaption": "Datos aún insuficientes",
"band": {
"green": "bueno",
@@ -4086,7 +4245,7 @@
"inputsLabel": "Entradas",
"noInputs": "Aún no hay entradas que respalden este valor.",
"methodLabel": "Método",
- "window": "De tus últimos {days} días · datos de {source}",
+ "window": "De tus últimos {days} días de datos de {source}",
"asOf": "a fecha de {date}",
"standardLink": "Estándar: {name}"
},
@@ -4096,7 +4255,7 @@
"loadingLabel": "Cargando tus constantes",
"loadError": "No se pudieron cargar tus constantes en este momento.",
"infoLabel": "¿Cómo se calcula esto?",
- "building": "Creando tu rango habitual — {count} de {target} días",
+ "building": "Creando tu rango habitual: {count} de {target} días",
"typicalRange": "Habitual para ti: {low}–{high}",
"fitnessYounger": "Forma cardiovascular de alguien ~{years} años más joven",
"fitnessOlder": "Forma cardiovascular ~{years} años por encima de tu edad",
@@ -4128,9 +4287,8 @@
"anatomy": {
"outOf": "/100",
"contributorsLabel": "Factores",
- "contributorValue": "{value} · {weight}% de peso",
+ "contributorValue": "{value}, peso {weight}%",
"contributorMissing": "sin datos",
- "disclaimer": "Indicativo — no es una evaluación clínica.",
"insufficient": "Aún no hay datos suficientes para puntuarlo. Sigue registrando y aparecerá aquí."
},
"composite": {
@@ -4149,15 +4307,14 @@
"READINESS": {
"title": "Disposición",
"description": "Un indicador diario de bienestar según cómo se sitúan tus señales frente a tu propia base.",
- "method": "Una mezcla de 0–100 de desviación de la frecuencia cardíaca en reposo, desviación de la VFC (SDNN), calidad del sueño, estabilidad de la frecuencia respiratoria y estabilidad del estado de ánimo — cada una medida frente a tu base personal. Los pesos se renormalizan según los componentes que tus datos sustentan. Un indicador de bienestar, no una puntuación de recuperación ni clínica.",
+ "method": "Una mezcla de 0–100 de desviación de la frecuencia cardíaca en reposo, desviación de la VFC (SDNN), calidad del sueño, estabilidad de la frecuencia respiratoria y estabilidad del estado de ánimo, cada una medida frente a tu base personal. Los pesos se renormalizan según los componentes que tus datos sustentan. Un indicador de bienestar, no una puntuación de recuperación ni clínica.",
"component": {
"rhr": "Frecuencia cardíaca en reposo",
"hrv": "VFC (SDNN)",
"sleep": "Sueño",
"respiratory": "Frecuencia respiratoria",
"mood": "Estabilidad del ánimo"
- },
- "caveat": "Un indicador de bienestar, no una puntuación de recuperación ni clínica."
+ }
},
"VITALS_BASELINE": {
"method": "Tu rango típico personal es la mediana de tus valores diarios en el periodo, ± una dispersión robusta (desviación absoluta mediana, escalada). Sigue TUS lecturas, no una norma poblacional, así que un valor dentro de la banda es típico para ti."
@@ -4182,20 +4339,19 @@
},
"RECOVERY_SCORE": {
"method": "Un indicador nocturno de recuperación de 0 a 100 que combina tu VFC, frecuencia cardíaca en reposo y sueño frente a tu línea base. Más alto significa mejor recuperado. Se calcula una vez por noche y aquí solo se lee, nunca se recalcula al vuelo.",
- "caveat": "Un indicador de bienestar descriptivo, no una medida de recuperación clínica ni de nivel de entrenamiento.",
"description": "Un indicador nocturno de recuperación a partir de tu VFC, frecuencia en reposo y sueño."
},
"STRESS_SCORE": {
"method": "Un indicador nocturno de estrés de 0 a 100 a partir de la forma de tu VFC a lo largo del día frente a tu línea base. Más alto significa más carga fisiológica.",
- "caveat": "Un indicador derivado de la VFC, NO una medición de estrés por EDA / actividad electrodérmica: tu reloj no tiene sensor EDA. Descriptivo, no clínico.",
"description": "Un indicador nocturno de estrés a partir de la forma de tu VFC durante el día."
},
"STRAIN_SCORE": {
"method": "Un indicador nocturno de esfuerzo de 0 a 100 a partir de tu carga de actividad ponderada por frecuencia cardíaca (un modelo de impulso de entrenamiento tipo TRIMP). Más alto significa un día más exigente.",
"anchorPersonal": "Anclado a tu propia carga reciente de días de entrenamiento: se evalúa en relación con tu esfuerzo habitual, de modo que un día realmente duro para ti aparece alto.",
"anchorPopulation": "Se evalúa frente a una referencia general mientras acumulas historial de entrenamiento; cuando tengas suficiente, pasa a tu propio esfuerzo habitual.",
- "caveat": "Un indicador descriptivo de carga de actividad, no una medida de carga clínica ni de nivel de entrenamiento.",
- "description": "Un indicador nocturno de carga de actividad a partir de tu esfuerzo ponderado por frecuencia cardíaca, relativo a tu esfuerzo habitual."
+ "description": "Un indicador nocturno de carga de actividad a partir de tu esfuerzo ponderado por frecuencia cardíaca, relativo a tu esfuerzo habitual.",
+ "deviceCaption": "Esfuerzo diario de tu dispositivo: {value} de {max}",
+ "deviceSource": "Hasta que HealthLog calcule su propio valor durante la noche, la tarjeta muestra el esfuerzo diario de tu dispositivo, convertido a 0–100."
},
"WRIST_TEMPERATURE_BASELINE": {
"method": "Tu temperatura típica de muñeca durante la noche es la mediana de tus lecturas nocturnas en el periodo, ± una dispersión robusta (desviación absoluta de la mediana, escalada). Sigue TUS lecturas: la noche pasada se muestra como una desviación respecto a tu propia base, no a una norma poblacional.",
@@ -4230,7 +4386,9 @@
"recovery": "Puntuación de recuperación",
"recoveryCrossLink": "Ver los datos de recuperación y esfuerzo de tu wearable",
"stress": "Estrés",
- "strain": "Esfuerzo"
+ "strain": "Esfuerzo",
+ "historyTitle": "Evolución",
+ "historyWindow": "Últimos {days} días"
},
"coincident": {
"label": "Varias constantes fuera de la base",
@@ -4257,7 +4415,7 @@
"sameTime": {
"sectionTitle": "Hoy hasta ahora",
"sectionSubtitle": "Tus totales acumulados frente a lo que es habitual en ti a esta hora",
- "learning": "Aprendiendo tu día habitual — {count} de {target} días",
+ "learning": "Aprendiendo tu día habitual: {count} de {target} días",
"framing": "Habitual a las {time}: {typical} {unit}",
"comparisonLabel": "Tu día habitual"
}
@@ -4344,7 +4502,7 @@
"navBreathingDisturbances": "Respiración",
"rhythmEvents": {
"sectionTitle": "Notificaciones de salud del dispositivo",
- "sectionIntro": "Notificaciones que tu reloj envió por su cuenta. HealthLog solo las muestra — no las detecta, confirma ni interpreta.",
+ "sectionIntro": "Notificaciones que tu reloj envió por su cuenta. HealthLog solo las muestra; no las detecta, confirma ni interpreta.",
"loadError": "No se pudieron cargar las notificaciones de tu dispositivo.",
"disclaimer": "Estas son alertas que tu dispositivo generó con su propia detección integrada. Se muestran aquí solo para tu información. No es una evaluación médica de HealthLog, y HealthLog no diagnostica ninguna afección. Si una notificación te preocupa, habla con tu médico.",
"event": {
@@ -4413,9 +4571,9 @@
"emptyTitle": "Aún no hay datos de recuperación",
"empty": "Aún no hay datos de recuperación. Conecta una cuenta de WHOOP, Polar u Oura y estas puntuaciones se completarán cuando tu dispositivo sincronice.",
"block": {
- "ansChargeExplainer": "Cuán cargado está tu sistema nervioso autónomo para el día — una lectura de recarga obtenida de tus señales nocturnas de corazón y respiración.",
+ "ansChargeExplainer": "Cuán cargado está tu sistema nervioso autónomo para el día: una lectura de recarga obtenida de tus señales nocturnas de corazón y respiración.",
"dayStrainExplainer": "La carga cardiovascular del día en la escala de esfuerzo de tu dispositivo. Léela en relación con tu recuperación, no como una meta en sí.",
- "workoutStrainExplainer": "Cuán exigente fue un entrenamiento en la misma escala de esfuerzo — el coste cardiovascular de esa sesión.",
+ "workoutStrainExplainer": "Cuán exigente fue un entrenamiento en la misma escala de esfuerzo: el coste cardiovascular de esa sesión.",
"cardioLoadExplainer": "Una aproximación de la carga de entrenamiento que sigue la carga cardiovascular acumulada. Compárala con tus propios días recientes.",
"averageHeartRateExplainer": "Tu frecuencia cardíaca media a lo largo de todo el día o ciclo, según informa tu wearable.",
"maxHeartRateExplainer": "Tu frecuencia cardíaca máxima a lo largo de todo el día o ciclo, según informa tu wearable.",
@@ -4428,7 +4586,7 @@
},
"resilience": {
"explainer": "Cómo equilibra tu cuerpo la carga diaria con la recuperación, leído a partir de tus señales nocturnas de los últimos días.",
- "learning": "Aún estamos aprendiendo tu resiliencia — unos días más asentarán el nivel.",
+ "learning": "Aún estamos aprendiendo tu resiliencia. Unos días más asentarán el nivel.",
"band": {
"limited": "Limitada",
"adequate": "Adecuada",
@@ -4459,6 +4617,7 @@
"shiftUp": "{vital} ha subido respecto a tu normal reciente.",
"shiftDown": "{vital} ha bajado respecto a tu normal reciente.",
"rangeContext": "Ahora {value}, normalmente {low}–{high}.",
+ "rangeContextTimeOfDay": "Ahora {value}, normalmente {low}–{high} a esta hora del día.",
"shiftContext": "Cerca de {before} antes, {after} desde entonces."
},
"breathingScreening": {
@@ -4569,9 +4728,9 @@
"emptyCta": "Conectar un dispositivo"
},
"intradayPulse": {
- "title": "La forma de tu día",
- "caption": "Tu frecuencia cardíaca a lo largo del día, promediada en tramos de 10 minutos.",
- "hourlyNote": "Mostrando el promedio por hora de este día — los días recientes muestran el detalle completo de 10 minutos.",
+ "title": "Pulso a lo largo del día",
+ "caption": "Promediado en intervalos de 10 minutos.",
+ "hourlyNote": "Mostrando el promedio por hora de este día. Los días recientes muestran el detalle completo de 10 minutos.",
"empty": "Aún no hay suficientes lecturas para este día para mostrar su forma.",
"coverageOne": "Basado en 1 lectura a lo largo de {hours}h.",
"coverageMany": "Basado en {count} lecturas a lo largo de {hours}h.",
@@ -4594,6 +4753,19 @@
"settingsBody": "HealthLog funciona por completo sin IA. Conecta un proveedor abajo si quieres un resumen diario, notas breves sobre tus mediciones y el Coach.",
"action": "Configurar IA",
"dismiss": "Ahora no"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Todavía no he indicado mi altura: ¿por qué importa el IMC y qué debería saber antes de configurarla?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Todavía no he añadido medicamentos: ¿qué debería saber antes de empezar a seguir aquí la adherencia?"
+ },
+ "scoreHistory": {
+ "healthTitle": "Evolución de la puntuación de salud",
+ "healthDescription": "Tu puntuación de salud día a día, tal como se mostró cada día.",
+ "emptyDescription": "Todavía no hay ninguna puntuación registrada en este periodo.",
+ "usualRange": "Sombreado: tu rango habitual, de {lo} a {hi}.",
+ "seam": "La línea se interrumpe donde cambió lo que cuenta para la puntuación."
}
},
"thresholds": {
@@ -4623,14 +4795,15 @@
"metricBodyFat": "Grasa corporal",
"metricSleep": "Duración del sueño",
"metricSteps": "Pasos/día",
- "metricGlucoseFasting": "Glucemia — en ayunas",
- "metricGlucosePostprandial": "Glucemia — posprandial",
- "metricGlucoseRandom": "Glucemia — cualquiera",
- "metricGlucoseBedtime": "Glucemia — antes de dormir",
+ "metricGlucoseFasting": "Glucemia (en ayunas)",
+ "metricGlucosePostprandial": "Glucemia (posprandial)",
+ "metricGlucoseRandom": "Glucemia (cualquiera)",
+ "metricGlucoseBedtime": "Glucemia (antes de dormir)",
"metricBodyWater": "Agua corporal",
"metricBoneMass": "Masa ósea",
"metricOxygenSaturation": "Saturación de oxígeno (SpO₂)",
- "unsetExplanation": "Sin valor propio — se aplica el valor por defecto calculado."
+ "unsetExplanation": "Sin valor propio: se aplica el valor por defecto calculado.",
+ "cardTitle": "Rangos por métrica"
},
"onboarding": {
"errorGeneric": "Algo ha salido mal al finalizar la configuración. Inténtalo de nuevo.",
@@ -4762,18 +4935,18 @@
"sampleTitle": "Resumen diario",
"sampleTag": "Ejemplo",
"sampleAriaLabel": "Resumen diario de ejemplo",
- "sampleBody": "Peso 1,2 kg menos en dos semanas. Pulso en reposo estable en 58 lpm. Tensión dentro del rango 6 de 7 días — la única lectura alta llegó tras una cena tardía y salada.",
- "sampleCaption": "Solo un ejemplo — no son tus datos. Así se lee un resumen cuando los insights están activos.",
+ "sampleBody": "Peso 1,2 kg menos en dos semanas. Pulso en reposo estable en 58 lpm. Tensión dentro del rango 6 de 7 días; la única lectura alta llegó tras una cena tardía y salada.",
+ "sampleCaption": "Solo un ejemplo, no son tus datos. Así se lee un resumen cuando los insights están activos.",
"ladderTitle": "Tres formas de activarlo",
- "ladderLocalTitle": "Modelo local — las solicitudes de IA no salen de tu red",
- "ladderLocalBody": "Apunta HealthLog a Ollama o LM Studio en tu propia máquina. Gratis y totalmente privado — la opción tranquila por defecto.",
+ "ladderLocalTitle": "Modelo local: las solicitudes de IA no salen de tu red",
+ "ladderLocalBody": "Apunta HealthLog a Ollama o LM Studio en tu propia máquina. Gratis y totalmente privado: la opción tranquila por defecto.",
"ladderByokTitle": "Usa tu propia clave",
"ladderByokBody": "Usa tu propia clave de OpenAI o Anthropic. Funciona con tu cuenta y facturación, y el nivel de API mantiene tus datos fuera del entrenamiento por defecto.",
"ladderOauthTitle": "Inicia sesión con una suscripción",
- "ladderOauthBody": "Conecta una cuenta de ChatGPT — sin coste por token, pero los términos del nivel de consumo pueden usar el contenido para entrenamiento, así que es una elección deliberada, nunca la opción por defecto para tus datos de salud.",
- "keylessLine": "HealthLog es plenamente útil sin IA. Tus paneles, gráficos, tendencias y exportaciones funcionan — la IA solo añade la lectura en lenguaje claro por encima.",
+ "ladderOauthBody": "Conecta una cuenta de ChatGPT. Sin coste por token, pero los términos del nivel de consumo pueden usar el contenido para entrenamiento, así que es una elección deliberada, nunca la opción por defecto para tus datos de salud.",
+ "keylessLine": "HealthLog es plenamente útil sin IA. Tus paneles, gráficos, tendencias y exportaciones funcionan; la IA solo añade la lectura en lenguaje claro por encima.",
"setupCta": "Configúralo (un minuto aprox.)",
- "sharedKeyNote": "Esta instancia ya ofrece un proveedor de IA compartido, así que los insights funcionan para ti ahora mismo — sin configuración.",
+ "sharedKeyNote": "Esta instancia ya ofrece un proveedor de IA compartido, así que los insights funcionan para ti ahora mismo, sin configuración.",
"offer": {
"title": "Esta instancia ya puede escribir tu resumen en lenguaje claro.",
"body": "El operador ofrece un proveedor que funcionó por última vez en las últimas 24 horas. Con un toque activas el lenguaje claro; tus datos irán entonces a ese único proveedor y a nadie más.",
@@ -4990,7 +5163,7 @@
"profileDescription": "Altura, fecha de nacimiento y sexo desbloquean objetivos personalizados.",
"profileCta": "Abrir perfil",
"measurementTitle": "Registrar la primera medición",
- "measurementDescription": "Una medición activa la tarjeta — la tendencia se dibuja a partir de la 5.ª medición.",
+ "measurementDescription": "Una medición activa la tarjeta; la tendencia se dibuja a partir de la 5.ª medición.",
"measurementCta": "Añadir medición",
"medicationTitle": "Añadir medicamento",
"medicationDescription": "Sigue el cumplimiento y recibe recordatorios justo cuando los necesites.",
@@ -4999,7 +5172,7 @@
"dataSourceDescription": "Sincroniza peso, presión, sueño y más desde Withings, WHOOP, Oura, Polar, Nightscout, Fitbit o Apple Health.",
"dataSourceCta": "Conectar",
"notificationsTitle": "Configurar notificaciones",
- "notificationsDescription": "Elige cómo te recuerda HealthLog — Telegram, ntfy o Web Push.",
+ "notificationsDescription": "Elige cómo te recuerda HealthLog: Telegram, ntfy o Web Push.",
"notificationsCta": "Abrir notificaciones",
"insightsTitle": "Activar insights (opcional)",
"insightsDescription": "Añade un modelo local o tu propia clave de IA para desbloquear el resumen diario y Coach.",
@@ -5088,11 +5261,6 @@
"brief": "Breve",
"default": "Predeterminado",
"detailed": "Detallado",
- "defaultWindow": "Periodo predeterminado",
- "last7days": "Últimos 7 días",
- "last30days": "Últimos 30 días",
- "last90days": "Últimos 90 días",
- "allTime": "Todo el tiempo",
"excludedMetrics": "Métricas excluidas",
"dataClusters": "Grupos de datos",
"customClusters": "Usar una selección personalizada de grupos",
@@ -5152,12 +5320,10 @@
"sections": {
"account": {
"title": "Cuenta",
- "description": "Perfil, contraseña, claves de acceso y tu recorrido de incorporación.",
"subtitle": "Tu perfil, identidad y datos de acceso."
},
"integrations": {
"title": "Integraciones",
- "description": "Servicios conectados, canales de entrega y prioridad de fuentes en un solo lugar.",
"subtitle": "Conecta y gestiona tus fuentes de datos y dispositivos.",
"connectionsHeading": "Conexiones",
"channelsHeading": "Canales de entrega",
@@ -5166,16 +5332,15 @@
},
"notifications": {
"title": "Notificaciones",
- "description": "Todos los recordatorios en un solo lugar: qué se recuerda y cuándo, y dónde se entrega.",
"subtitle": "Elige qué recordatorios recibes y dónde se entregan.",
"remindersHeading": "Recordatorios",
"matrixLink": "Notificaciones por canal",
- "channelsLink": "Canales de entrega"
+ "channelsLink": "Canales de entrega",
+ "displayHeading": "En tu teléfono"
},
"layout": {
- "title": "Apariencia",
- "description": "Elige cómo se ven y se organizan el panel, los análisis y tus módulos de seguimiento.",
- "backToHub": "Volver a Apariencia",
+ "title": "Diseño",
+ "backToHub": "Volver a Diseño",
"dashboard": {
"title": "Panel",
"description": "Diseño y orden de los mosaicos."
@@ -5184,7 +5349,7 @@
"title": "Análisis",
"description": "Orden de la vista general y de las pestañas de navegación."
},
- "subtitle": "Personaliza cómo se ven tu panel, los análisis y los módulos de seguimiento.",
+ "subtitle": "Elige qué muestran tu panel, los análisis y los módulos de seguimiento, y en qué orden.",
"medications": {
"title": "Medicamentos",
"description": "Vista de lista y orden manual."
@@ -5204,6 +5369,10 @@
"mood": {
"title": "Etiquetas de ánimo",
"description": "Grupos, etiquetas y etiquetas archivadas del selector de ánimo."
+ },
+ "documents": {
+ "title": "Documentos",
+ "description": "Vista y disposición de los meses."
}
},
"dashboard": {
@@ -5211,15 +5380,13 @@
},
"thresholds": {
"title": "Objetivos",
- "description": "Por métrica: tu propio rango objetivo de valores saludables.",
"subtitle": "Define tus rangos objetivo personales por métrica."
},
"sources": {
"title": "Prioridad de fuentes",
- "description": "Elige qué fuente prevalece cuando varias registran la misma métrica el mismo día.",
"integrationsHint": "Conecta o desconecta fuentes de datos en la página de integraciones.",
"integrationsHintLink": "Abrir integraciones",
- "cardTitle": "Prioridad de fuentes por métrica",
+ "cardTitle": "Orden por métrica",
"help": "Si varias fuentes entregan el mismo valor el mismo día, la fuente superior cuenta como canónica.",
"resetDefaults": "Restablecer valores predeterminados",
"resetDefaultsTitle": "¿Restablecer el orden de fuentes?",
@@ -5232,7 +5399,7 @@
"resetSuccess": "Prioridad de fuentes restablecida a los valores predeterminados",
"perMetricToggle": "Ajustes por métrica ({count})",
"perMetricHelp": "Métricas que has personalizado respecto al orden predeterminado. Usa esta lista para devolver cualquier métrica concreta a su valor predeterminado sin tocar las demás.",
- "perMetricEmpty": "No hay ajustes por métrica — todas las métricas usan el orden global de arriba.",
+ "perMetricEmpty": "No hay ajustes por métrica. Todas las métricas usan el orden global de arriba.",
"resetMetric": "Restablecer",
"deviceTypeToggle": "Prioridad por tipo de dispositivo ({count})",
"deviceTypeHelp": "Dentro de una fuente, qué dispositivo prevalece. Cuando un Apple Watch y un iPhone registran ambos los pasos, el tipo de dispositivo mejor clasificado cuenta como la lectura canónica.",
@@ -5257,7 +5424,8 @@
"respiratoryRate": "Frecuencia respiratoria",
"recovery": "Recuperación",
"stress": "Estrés",
- "sleepDebt": "Deuda de sueño"
+ "sleepDebt": "Deuda de sueño",
+ "hrvRmssd": "VFC (RMSSD)"
},
"sourceLabels": {
"WITHINGS": "Withings",
@@ -5274,7 +5442,8 @@
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava",
- "EXTERNAL": "Dispositivo externo"
+ "EXTERNAL": "Dispositivo externo",
+ "HEALTH_CONNECT": "Health Connect"
},
"deviceLabels": {
"watch": "Reloj",
@@ -5296,26 +5465,23 @@
"moduleHintLink": "Gestionar módulos",
"detail": "Solo lectura: la app de origen es la dueña de los valores."
},
- "helpDetail": "Las demás se conservan para la traza de auditoría."
+ "helpDetail": "Las demás se conservan para la traza de auditoría. Solo aparecen las fuentes que has conectado o de las que hay datos.",
+ "noneInUse": "Aún no hay fuentes que ordenar. Conecta una en Integraciones o registra un valor."
},
"ai": {
"title": "Proveedor de IA",
- "description": "Proveedor, modelo y clave API para los análisis, además de tu contexto «sobre mí».",
"subtitle": "Elige tu proveedor de IA y usa tu propia clave."
},
"api": {
- "title": "API & Tokens",
- "description": "Tokens Bearer para tus propios scripts y apps — registra mediciones o tomas de medicación desde cualquier lugar.",
+ "title": "API y tokens",
"subtitle": "Gestiona los endpoints de la API y los tokens de acceso."
},
"advanced": {
"title": "Avanzado",
- "description": "La salida: borra todos los registros de salud para siempre o cierra la cuenta.",
"subtitle": "Restablecer datos y eliminar cuenta."
},
"export": {
"title": "Exportar e importar",
- "description": "Descarga tus datos como CSV o una copia de seguridad JSON completa, o importa desde Apple Health, JSON o CSV. El informe médico ahora está en Historia de salud.",
"otherOptionsHeading": "Otras opciones de exportación",
"downloadFailed": "No se pudo descargar la exportación. Inténtalo de nuevo.",
"actions": {
@@ -5342,7 +5508,7 @@
"fullBackup": {
"title": "Copia completa",
"description": "Todo tu historial en un archivo JSON.",
- "scopeNote": "Los metadatos de los documentos y el resumen de IA, si existe, se incluyen en texto plano; los archivos originales de los documentos y los datos de rutas GPS/muestras de los entrenamientos no están incluidos — descárgalos por separado desde el archivo de documentos o la exportación de entrenamientos.",
+ "scopeNote": "Los metadatos de los documentos y el resumen de IA, si existe, se incluyen en texto plano; los archivos originales de los documentos y los datos de rutas GPS/muestras de los entrenamientos no están incluidos. Descárgalos por separado desde el archivo de documentos o la exportación de entrenamientos.",
"encryptToggle": "Cifrar con una contraseña",
"passphrasePlaceholder": "Contraseña (mín. 12 caracteres)",
"passphraseHelp": "Deriva la clave del archivo con Argon2id y lo sella con AES-256-GCM. Guarda el archivo con la extensión .hlx.",
@@ -5373,6 +5539,9 @@
"rowsImported": "{count} filas importadas hasta ahora",
"doneSummary": "Listo: {imported} de {read} registros importados, {skipped} registros clínicos omitidos.",
"doneNothing": "No se importó nada de ese archivo: llegaron 0 de {read} registros.",
+ "writtenByHealthLogOne": "Se omitió {count} registro que HealthLog escribió en Apple Health, porque ya está en tus datos.",
+ "writtenByHealthLogOther": "Se omitieron {count} registros que HealthLog escribió en Apple Health, porque ya están en tus datos.",
+ "writtenByHealthLogFew": "Se omitieron {count} registros que HealthLog escribió en Apple Health, porque ya están en tus datos.",
"ecgResult": "ECG: {discovered} detectados, {imported} importados, {updated} actualizados, {skipped} omitidos y {failed} fallidos. Los errores de ECG no descartan los registros compatibles importados desde export.xml.",
"estimateWarning": "Los totales acumulados de {count} día(s) se estimaron a partir de la mayor fuente de la exportación. Las estadísticas nativas de Apple Health siguen siendo la referencia.",
"phase": {
@@ -5471,8 +5640,8 @@
"failed": "La importación falló. No se escribió nada.",
"columnsHeading": "Qué hace cada columna",
"jsonOnly": "solo JSON",
- "previewLine": "Filas leídas: {rows} · se importarían: {queued} · rechazadas: {refused}",
- "submittedLine": "Filas leídas: {rows} · en cola para importar: {queued}",
+ "previewLine": "{rows} filas leídas, {queued} se importarían, {refused} rechazadas",
+ "submittedLine": "{rows} filas leídas, {queued} en cola para importar",
"unmatched": "Ningún medicamento registrado coincide con: {names}",
"ambiguous": "Coincide con más de un medicamento: {names}",
"mirrored": "Copiado de otra aplicación, así que no se puede añadir ninguna dosis: {names}",
@@ -5508,18 +5677,59 @@
"tooManyRows": "Ese archivo tiene más filas de las que admite una importación. Divídelo e importa las partes."
},
"detail": "Un archivo abarca todos los medicamentos que contiene."
+ },
+ "healthConnect": {
+ "title": "Exportación de Health Connect",
+ "description": "Importa el archivo ZIP que crea la app Health Connect en Android al exportar tus datos.",
+ "choose": "Elegir archivo ZIP",
+ "uploading": "Subiendo…",
+ "uploadFailed": "La subida ha fallado. Inténtalo de nuevo.",
+ "tooLarge": "El archivo supera el tamaño que acepta este servidor.",
+ "doneSummary": "{count} valores importados.",
+ "failed": "La importación ha fallado: {reason}",
+ "phase": {
+ "queued": "Esperando para empezar",
+ "unpacking": "Descomprimiendo el archivo",
+ "parsing": "Leyendo los registros",
+ "upserting": "Guardando los valores",
+ "done": "Terminado",
+ "failed": "Fallido"
+ },
+ "steps": {
+ "export": "En tu teléfono Android, abre Health Connect, ve a Gestionar datos, luego a Exportar e importar, e inicia una exportación.",
+ "cloud": "Health Connect solo guarda la exportación en una app de almacenamiento en la nube como Google Drive, Dropbox o Nextcloud, nunca en el propio teléfono. Elige una allí.",
+ "upload": "Descarga el archivo ZIP de esa nube a este dispositivo y súbelo aquí."
+ },
+ "dropLabel": "Suelta aquí el ZIP de Health Connect",
+ "dropHint": "o haz clic para elegirlo. Hasta 1 GB.",
+ "fileInputLabel": "Archivo ZIP de la exportación de Health Connect",
+ "privacyNote": "El archivo se borra del servidor en cuanto termina la importación. Los registros de apps que ya conectas directamente, como Withings o Fitbit, se omiten para que nada llegue dos veces. Subir de nuevo el mismo archivo no cambia nada.",
+ "rateLimited": "Demasiadas subidas en poco tiempo. Espera un minuto y vuelve a intentarlo.",
+ "busy": "Ya hay una importación en curso para tu cuenta. Espera a que termine.",
+ "rowsSoFar": "{count} valores guardados hasta ahora",
+ "doneNothing": "La importación terminó, pero no había nada nuevo que añadir.",
+ "workouts": "Entrenamientos",
+ "leftOutConnected": "Se omitieron {count} registros de apps que ya conectas directamente.",
+ "nutrientsModuleOff": "Los totales de agua y nutrientes no se importaron porque el módulo Nutrientes está desactivado.",
+ "cycleModuleOff": "Los datos del periodo no se importaron porque el seguimiento del ciclo está desactivado.",
+ "failure": {
+ "unsupportedVersion": "Esta exportación de Health Connect es demasiado antigua para leerla. Actualiza Health Connect en el teléfono y vuelve a exportar.",
+ "notHealthConnect": "Este archivo no es una exportación de Health Connect. Elige el archivo ZIP que guardó Health Connect.",
+ "unsafeSchema": "Este archivo no tiene la estructura de una exportación de Health Connect y no se leyó.",
+ "tooLarge": "La exportación es mayor de lo que acepta este servidor.",
+ "stagingMissing": "El archivo subido ya no estaba en el servidor. Súbelo de nuevo.",
+ "interrupted": "Un reinicio del servidor interrumpió la importación. Sube el archivo de nuevo."
+ }
}
},
"subtitle": "Exporta tus datos o importa desde una copia de seguridad."
},
"about": {
"title": "Acerca de esta app",
- "description": "Versión, licencia, enlaces.",
"subtitle": "Versión, licencia y enlaces del proyecto."
},
"sharing": {
"title": "Compartir",
- "description": "Crea un enlace de solo lectura para compartir tu historial de salud con un profesional médico.",
"subtitle": "Comparte acceso de solo lectura a tus datos."
},
"insights": {
@@ -5554,12 +5764,10 @@
},
"gesundheitsakte": {
"title": "Historial médico",
- "description": "Exporta tu historial médico completo como PDF, FHIR R4 o un paquete ZIP combinado.",
"subtitle": "Exporta un historial de salud completo, listo para el médico."
},
"coach": {
"title": "Coach",
- "description": "Activa el Coach, tono y alcance, recuerdos guardados.",
"subtitle": "Ajusta el Coach, sus avisos proactivos y su memoria."
},
"security": {
@@ -5568,8 +5776,7 @@
},
"privacy": {
"title": "Datos y privacidad",
- "subtitle": "Tus datos, su retención, el cifrado y la seguridad de la cuenta en un solo lugar.",
- "description": "Exportación, eliminación, retención, estado de cifrado, sesiones y actividad de seguridad."
+ "subtitle": "Tus datos, su retención, el cifrado y la seguridad de la cuenta en un solo lugar."
},
"mcp": {
"title": "Conector MCP",
@@ -5577,8 +5784,7 @@
},
"anamnesis": {
"title": "Antecedentes médicos",
- "subtitle": "Gestiona tus alergias y tus antecedentes familiares.",
- "description": "Registros estructurados de alergias y antecedentes familiares."
+ "subtitle": "Tus antecedentes, tu estilo de vida, tus datos de emergencia y lo que la IA puede leer."
},
"environment": {
"title": "Ubicación y clima",
@@ -5612,7 +5818,8 @@
"add": "Añadir {label}",
"remove": "Eliminar periodo de ubicación",
"removeConfirmTitle": "¿Quitar este viaje?",
- "removeConfirmBody": "Las mediciones de ese periodo permanecen; solo desaparece la entrada del viaje."
+ "removeConfirmBody": "Las mediciones de ese periodo permanecen; solo desaparece la entrada del viaje.",
+ "range": "Del {start} al {end}"
},
"backfill": {
"title": "Rellenar días pasados",
@@ -5622,7 +5829,21 @@
"noResults": "No se encontraron lugares coincidentes.",
"fetchFailedOne": "La última actualización meteorológica en segundo plano falló ({when}).",
"fetchFailedFew": "Las últimas {count} actualizaciones meteorológicas en segundo plano fallaron, la más reciente {when}.",
- "fetchFailedOther": "Las últimas {count} actualizaciones meteorológicas en segundo plano fallaron, la más reciente {when}."
+ "fetchFailedOther": "Las últimas {count} actualizaciones meteorológicas en segundo plano fallaron, la más reciente {when}.",
+ "airQuality": {
+ "title": "Calidad del aire y polen",
+ "description": "Calidad del aire, polen y UV diarios para tu ubicación, del modelo CAMS a través de Open-Meteo.",
+ "toggle": "Incluir calidad del aire y polen",
+ "operatorDisabled": "La calidad del aire está desactivada en este servidor.",
+ "coverage": "{count} días con datos de calidad del aire",
+ "pollenEuropeOnly": "Los datos de polen solo cubren Europa.",
+ "historyProgress": "Completando la calidad del aire: {done} de {total} días",
+ "historyComplete": "La calidad del aire está completada para todos los días anteriores con registros que cubre la fuente ({total} días). Los días más antiguos quedan vacíos.",
+ "attribution": "Fuentes de datos y licencias",
+ "saveError": "No se pudo guardar el ajuste.",
+ "latest": "Día más reciente ({date})",
+ "modelNote": "Valores exteriores modelados para tu zona, no tu exposición personal."
+ }
},
"score": {
"title": "Puntuación de salud",
@@ -5749,7 +5970,6 @@
},
"changePassword": "Cambiar contraseña",
"changePasswordDescription": "Sustituye tu contraseña actual por una nueva.",
- "passwordReset": "Restablecer contraseña",
"currentPassword": "Contraseña actual",
"newPassword": "Nueva contraseña",
"confirmNewPassword": "Confirmar nueva contraseña",
@@ -5794,9 +6014,9 @@
"botToken": "Token del bot",
"chatId": "ID del chat",
"enableNotifications": "Activar notificaciones",
- "telegramStep1": "1. Crea un bot con @BotFather en Telegram y copia el token.",
- "telegramStep2": "2. Envía /start a tu bot para activar el chat.",
- "telegramStep3": "3. Obtén tu ID del chat con @userinfobot o la API del bot.",
+ "telegramStep1": "Crea un bot con @BotFather en Telegram y copia el token.",
+ "telegramStep2": "Envía /start a tu bot para activar el chat.",
+ "telegramStep3": "Obtén tu ID del chat con @userinfobot o la API del bot.",
"ntfy": "ntfy",
"ntfyDescription": "Notificaciones a través de ntfy (autoalojado o ntfy.sh).",
"ntfyEnable": "Activar ntfy",
@@ -5805,7 +6025,7 @@
"ntfyAuthToken": "Token de autenticación (opcional)",
"ntfyAuthTokenHint": "Solo necesario para temas privados con control de acceso.",
"webhook": "Webhook",
- "webhookDescription": "Envía notificaciones por POST a cualquier URL: Gotify, Discord, Slack, Matrix o Home Assistant.",
+ "webhookDescription": "Envía notificaciones por POST a una URL: Discord, Slack, Gotify, Home Assistant, n8n o un relé que acepte JSON.",
"webhookEnable": "Activar webhook",
"webhookUrl": "URL del webhook",
"webhookUrlHint": "La URL completa para el POST. Una dirección de una red privada necesita la aprobación del administrador del servidor.",
@@ -5813,6 +6033,7 @@
"webhookHeaderValue": "Valor de cabecera (opcional)",
"webhookFormat": "Formato del mensaje",
"webhookFormatGeneric": "JSON genérico",
+ "webhookFormatGenericHint": "Pega una URL de webhook de Discord o Slack tal cual. Home Assistant y n8n leen los campos title, message, eventType y priority.",
"webhookFormatGotify": "Gotify",
"webhookFormatGotifyHint": "Usa la URL que termina en /message y pon el token de la aplicación en la cabecera X-Gotify-Key.",
"email": "Correo electrónico",
@@ -5829,7 +6050,6 @@
"webPushSubscribed": "¡Notificaciones push activadas!",
"webPushUnsubscribed": "Notificaciones push desactivadas.",
"webPushSubscribeFailed": "Falló la activación",
- "kiInsightsDescription": "Opcional: guarda tu clave de OpenAI para análisis diarios y automáticos en Insights.",
"codexConnected": "¡ChatGPT conectado correctamente! Insights ya está activo.",
"codexDisconnected": "Conexión con ChatGPT eliminada.",
"codexConnectionFailed": "Falló la conexión con ChatGPT. Inténtalo de nuevo.",
@@ -5839,7 +6059,7 @@
"rawDataWarning": "Historial detallado activado: cada análisis envía además promedios diarios de los últimos 90 días, promedios semanales del año anterior y promedios mensuales hasta 5 años atrás.",
"regenerateInsights": "Regenerar informes",
"regenerateSuccess": "Informes regenerados correctamente",
- "regenerateRateLimit": "Espera un momento — has alcanzado el límite por hora de análisis.",
+ "regenerateRateLimit": "Espera un momento: has alcanzado el límite por hora de análisis.",
"lastGeneratedAt": "Generado por última vez",
"ai": {
"chatgptConnectedBadge": "ChatGPT conectado",
@@ -5849,11 +6069,11 @@
"deviceCodeHeading": "Termina de conectar en chatgpt.com",
"deviceCodeStep1": "Abre este enlace en cualquier dispositivo:",
"deviceCodeStep2": "Introduce este código de un solo uso:",
- "deviceCodeStep3": "Aprueba la conexión — esta página se actualiza automáticamente.",
+ "deviceCodeStep3": "Aprueba la conexión. Esta página se actualiza automáticamente.",
"deviceCodeCopy": "Copiar",
"deviceCodeWaiting": "Esperando aprobación…",
"deviceCodeCancel": "Cancelar",
- "oauthNotConfigured": "El OAuth de ChatGPT no está configurado en esta instancia — usa tu propia clave de API a continuación.",
+ "oauthNotConfigured": "El OAuth de ChatGPT no está configurado en esta instancia. Usa tu propia clave de API a continuación.",
"modelLabel": "Modelo",
"modelOptionDefault": "— Predeterminado —",
"modelOptionCustom": "Personalizado…",
@@ -5877,7 +6097,7 @@
"admin-openai": "OpenAI de administrador"
},
"title": "Cadena de respaldo",
- "description": "Si el proveedor principal falla, HealthLog recorre la cadena en orden. Arrastra las filas para reordenarlas; usa el interruptor para desactivar un proveedor sin eliminarlo.",
+ "description": "Si el proveedor activo falla, HealthLog prueba el siguiente activado en este orden.",
"moveUp": "Subir",
"moveDown": "Bajar",
"removeFromChain": "Quitar de la cadena",
@@ -5890,11 +6110,8 @@
"resetConfirmTitle": "¿Restablecer la cadena a los valores predeterminados?",
"resetConfirmBody": "La cadena vuelve a Codex → OpenAI → Anthropic → Local → OpenAI de administrador. Tus credenciales guardadas no se modifican."
},
- "providerCardTitle": "Proveedor de IA",
"activeProviderHeading": "Proveedor activo",
- "activeProviderBody": "Elige el proveedor que quieres usar primero. El formulario de abajo configura únicamente el proveedor seleccionado; la cadena de respaldo del final decide qué ocurre si falla.",
"activeProviderLabel": "Proveedor principal",
- "providerConfigTitle": "Configuración del proveedor",
"providerSelect": {
"codex": "Cuenta de ChatGPT (Codex)",
"openai": "OpenAI (tu clave de API)",
@@ -5940,11 +6157,11 @@
"notConfigured": "El operador no ha configurado una clave de OpenAI compartida en esta instancia."
},
"testProvider": "Probar el proveedor activo",
- "testSuccess": "OK — {provider} ({model})",
+ "testSuccess": "OK: {provider} ({model})",
"testFailedShort": "Prueba fallida: {message}",
- "testUnexpectedResponse": "Falló la conexión con el proveedor de IA — respuesta inesperada del servidor.",
- "testReasonCredentials": "El proveedor rechazó las credenciales — vuelve a autenticarte en los ajustes de IA.",
- "testReasonRateLimited": "El proveedor limitó la solicitud — inténtalo de nuevo en breve.",
+ "testUnexpectedResponse": "Falló la conexión con el proveedor de IA: respuesta inesperada del servidor.",
+ "testReasonCredentials": "El proveedor rechazó las credenciales. Vuelve a autenticarte en los ajustes de IA.",
+ "testReasonRateLimited": "El proveedor limitó la solicitud. Inténtalo de nuevo en breve.",
"testReasonServerError": "El proveedor de IA devolvió un error del servidor.",
"testReasonBadRequest": "El proveedor rechazó la solicitud (HTTP {status}): el endpoint respondió, así que el problema está en el formato de la solicitud o en el nombre del modelo, no en la conexión.",
"testReasonUnreachable": "No se pudo contactar con el proveedor de IA.",
@@ -5958,7 +6175,6 @@
"categoryGoal": "Objetivos",
"categoryConstraint": "Restricciones",
"categoryContext": "Contexto",
- "forget": "Olvidar",
"forgetAria": "Olvidar este dato",
"forgetTitle": "¿Olvidar este dato?",
"forgetBody": "El Coach dejará de usarlo a partir del próximo mensaje. Un dato olvidado no se puede recuperar.",
@@ -5974,7 +6190,8 @@
"forgotError": "No se pudo olvidar ese dato. Inténtalo de nuevo.",
"clearedToast": "Se olvidaron {count} datos guardados.",
"clearError": "No se pudieron borrar los datos guardados. Inténtalo de nuevo.",
- "detail": "Los usa para que sus respuestas sigan siendo relevantes. Elimina lo que prefieras que olvide."
+ "detail": "Los usa para que sus respuestas sigan siendo relevantes. Elimina lo que prefieras que olvide.",
+ "categoryMedication": "Medicamentos"
},
"coachReminders": {
"title": "Tus recordatorios",
@@ -5988,7 +6205,7 @@
"markDone": "Hecho",
"dismiss": "Descartar",
"deleteAria": "Eliminar este recordatorio",
- "note": "Los recordatorios se guardan cifrados. Elimina uno cuando quieras — desaparece de lo que el Coach puede ver.",
+ "note": "Los recordatorios se guardan cifrados. Elimina uno cuando quieras y desaparece de lo que el Coach puede ver.",
"status": {
"proposed": "Sugerido",
"active": "Guardado",
@@ -6040,14 +6257,14 @@
},
"centralCodex": {
"title": "Usar el acceso de IA compartido del servidor",
- "subLabel": "Enruta las solicitudes de IA a través de la conexión que el operador configuró para este servidor, junto a tus propios proveedores como alternativa.",
+ "subLabel": "Las solicitudes de IA pasan por la conexión compartida del servidor, con tus propios proveedores como respaldo.",
"honesty": "El acceso compartido es una única cuenta de IA con sesión iniciada que el operador conectó para todos en este servidor, no tu propia clave. Las solicitudes que hagas se usan de forma predeterminada para entrenar los modelos del proveedor y ningún acuerdo de tratamiento de datos las cubre, los límites por hora y semanales de la cuenta se comparten con todos los demás que se han unido, y tus propios proveedores se prueban primero. Actívalo solo si aceptas este compromiso.",
"confirm": "Entendido: usar la conexión",
"cancel": "Cancelar"
},
"consent": {
"title": "Consentimiento de IA",
- "activeSince": "Otorgado el {date}. Tus datos de salud pueden enviarse al proveedor configurado para las funciones de IA.",
+ "activeSince": "Otorgado el {date}: tus datos de salud pueden enviarse al proveedor configurado para las funciones de IA.",
"withdrawn": "Retirado. Salvo que otro consentimiento lo cubra, el Coach e Insights ya no usan el acceso de IA de este servidor y ningún modelo en línea lee tus documentos. Con tu propia clave o un modelo local, el Coach e Insights siguen funcionando.",
"withdraw": "Retirar el consentimiento",
"confirmBody": "Salvo que otro consentimiento tuyo los siga cubriendo, se detienen de inmediato: el Coach e Insights cuando usan el acceso de IA de este servidor, y la lectura de documentos con un modelo en línea. Con tu propia clave o un modelo local, el Coach e Insights siguen funcionando. Salvo que otro consentimiento siga cubriendo Insights, se eliminan las notas que un modelo escribió para ti: notas de estado, el resumen diario, los resúmenes de periodo escritos por un modelo, las líneas de llegada y las notas de entrenamiento. Tus conversaciones con el Coach, lo que el Coach recordaba, tus planes y los resúmenes de tus documentos se conservan, y puedes leerlos o eliminarlos tú mismo.",
@@ -6058,7 +6275,7 @@
"error": "No se pudo guardar la retirada. Inténtalo de nuevo.",
"documentReading": {
"title": "Consentimiento para leer documentos",
- "activeSince": "Dado el {date}. Los documentos, los informes de laboratorio escaneados y las descripciones de medicamentos pueden enviarse a tu servicio de IA para leerlos.",
+ "activeSince": "Dado el {date}: documentos, análisis escaneados y textos de medicamentos pueden ir a tu servicio de IA para leerlos.",
"confirmBody": "Los documentos, los informes de laboratorio escaneados y las descripciones de medicamentos dejan de enviarse para leerlos. No se elimina nada de lo que ya tienes."
}
},
@@ -6080,7 +6297,21 @@
"title": "Planes del Coach",
"description": "Los planes que acordaste con el Coach, para leerlos o eliminarlos.",
"open": "Abrir planes"
- }
+ },
+ "reasoning": {
+ "label": "Razonamiento",
+ "hint": "Los modelos de razonamiento como Gemma o Qwen piensan antes de responder. Si lo desactivas, todo el presupuesto de respuesta se queda para la respuesta.",
+ "options": {
+ "default": "Predeterminado",
+ "none": "Desactivado",
+ "low": "Bajo",
+ "medium": "Medio",
+ "high": "Alto"
+ },
+ "coachNote": "En el Coach se aplica la profundidad de pensamiento de los ajustes del Coach."
+ },
+ "providerCardDescription": "El proveedor al que HealthLog pregunta primero, con su clave y su modelo.",
+ "runtimeTitle": "Comprobar y actualizar"
},
"dashboard": {
"units": {
@@ -6104,8 +6335,8 @@
"withingsClientId": "ID de cliente",
"withingsClientSecret": "Secreto de cliente",
"withingsCredentialsSaved": "Credenciales guardadas",
- "withingsCredentialsSavedPlaceholder": "Guardado — introduce uno nuevo para reemplazarlo",
- "withingsCredentialsSavedPlaceholderSecret": "Guardado — introduce uno nuevo para reemplazarlo",
+ "withingsCredentialsSavedPlaceholder": "Guardado. Introduce uno nuevo para reemplazarlo",
+ "withingsCredentialsSavedPlaceholderSecret": "Guardado. Introduce uno nuevo para reemplazarlo",
"withingsSaveCredentials": "Guardar",
"configured": "Configurado",
"withingsSync": "Sincronizar ahora",
@@ -6120,7 +6351,7 @@
},
"withingsFullSyncResult": "{count} mediciones sincronizadas por completo",
"withingsSyncFailed": "Falló la sincronización",
- "withingsOauthConnected": "Withings conectado — la primera sincronización comienza en segundo plano.",
+ "withingsOauthConnected": "Withings conectado. La primera sincronización comienza en segundo plano.",
"withingsOauthFailed": "Error al conectar con Withings",
"withingsOauthError": {
"csrf1": "La comprobación de seguridad falló porque faltaba la cookie del navegador. Inicia la conexión de nuevo en el mismo navegador.",
@@ -6145,8 +6376,8 @@
"whoopClientId": "Client ID",
"whoopClientSecret": "Client Secret",
"whoopCredentialsSaved": "Credenciales guardadas",
- "whoopCredentialsSavedPlaceholder": "Guardado — introduce nuevas para reemplazar",
- "whoopCredentialsSavedPlaceholderSecret": "Guardado — introduce nuevas para reemplazar",
+ "whoopCredentialsSavedPlaceholder": "Guardado. Introduce nuevas para reemplazar",
+ "whoopCredentialsSavedPlaceholderSecret": "Guardado. Introduce nuevas para reemplazar",
"whoopSaveCredentials": "Guardar",
"whoopSync": "Sincronizar ahora",
"whoopFullSync": "Sincronizar todos los datos",
@@ -6175,7 +6406,7 @@
},
"fitbit": "Fitbit",
"fitbitTag": "Fitbit y Pixel",
- "fitbitDescription": "Conecta Fitbit para sincronizar tus datos de Fitbit y Pixel Watch — peso, frecuencia cardíaca y más.",
+ "fitbitDescription": "Conecta Fitbit para sincronizar tus datos de Fitbit y Pixel Watch: peso, frecuencia cardíaca y más.",
"fitbitExperimentalBadge": "Experimental",
"fitbitCredentials": "Credenciales de API",
"fitbitClientId": "Client ID",
@@ -6207,8 +6438,8 @@
"googleHealthClientId": "ID de cliente",
"googleHealthClientSecret": "Secreto de cliente",
"googleHealthCredentialsSaved": "Credenciales guardadas",
- "googleHealthCredentialsSavedPlaceholder": "Guardado — introduce nuevas para reemplazar",
- "googleHealthCredentialsSavedPlaceholderSecret": "Guardado — introduce nuevas para reemplazar",
+ "googleHealthCredentialsSavedPlaceholder": "Guardado. Introduce nuevas para reemplazar",
+ "googleHealthCredentialsSavedPlaceholderSecret": "Guardado. Introduce nuevas para reemplazar",
"googleHealthSaveCredentials": "Guardar",
"googleHealthSync": "Sincronizar ahora",
"googleHealthSyncCheckStatus": "Actualizar estado",
@@ -6279,10 +6510,10 @@
},
"integrationPill": {
"connected": "Conectado",
- "errorReconnect": "Error — reconectar",
+ "errorReconnect": "Error: reconectar",
"warningServerError": "Sincronización con errores",
"failureCount": "{count}/{threshold}",
- "parkedReconnect": "Pausado — reconectar",
+ "parkedReconnect": "Pausado: reconectar",
"notConnected": "No conectado",
"justNow": "ahora mismo",
"minutesAgo": "hace {count} min",
@@ -6297,7 +6528,7 @@
},
"integrationFreshness": {
"title": "Datos recibidos",
- "summary": "{count} tipos de datos · más reciente {relative}",
+ "summary": "{count} tipos de datos, más reciente {relative}",
"quiet": "{count} en silencio",
"workouts": "Entrenamientos"
},
@@ -6311,31 +6542,33 @@
"fitbitViewData": "Ver tu sueño",
"googleHealthViewData": "Ver tus pasos",
"apiTokens": "Tokens de API",
- "apiTokensDescription": "Revisa y revoca tokens de API para el registro externo de tomas de medicación.",
+ "apiTokensDescription": "Revisa y revoca los tokens de mediciones, entrenamientos, documentos y tomas de medicación.",
"measurementsToken": {
"title": "Tokens de mediciones",
"description": "Crea un token para que una báscula, un reloj o tu domótica añadan mediciones.",
"detail": "El token se muestra una sola vez.",
"scopeNote": "Solo puede añadir mediciones nuevas a tu propio historial: no puede leer tus datos, ni modificar o borrar nada, ni crear otro token. Las mediciones se marcan como procedentes de un dispositivo externo, así las distingues más tarde, y puedes corregirlas igualmente.",
- "createFailed": "No se pudo crear el token. Inténtalo de nuevo."
+ "createFailed": "No se pudo crear el token. Inténtalo de nuevo.",
+ "namePlaceholder": "Nombre, p. ej. Home Assistant"
},
"workoutsToken": {
"title": "Tokens de entrenamientos",
"description": "Crea un token para que un puente de tu reloj o un script de sincronización añadan tus entrenamientos.",
"detail": "El token se muestra una sola vez.",
"scopeNote": "Solo puede añadir entrenamientos nuevos a tu propio historial. No puede leer tus datos, ni modificar o borrar nada, ni crear otro token. Los entrenamientos que añade se marcan como procedentes de un dispositivo externo, y si tu teléfono registró la misma sesión, ves la copia del teléfono.",
- "createFailed": "No se pudo crear el token. Inténtalo de nuevo."
+ "createFailed": "No se pudo crear el token. Inténtalo de nuevo.",
+ "namePlaceholder": "Nombre, p. ej. Garmin"
},
"documentsToken": {
"title": "Tokens de documentos",
"description": "Crea un token para que Paperless-ngx, Papra o el script de importación añadan documentos a tu archivo.",
"detail": "El token se muestra una sola vez.",
"scopeNote": "Solo puede añadir documentos nuevos a tu propio archivo. No puede listar, abrir, descargar, modificar ni borrar nada, ni crear otro token. Lo único que recibe es si un archivo se guardó, no cómo lo archivaste.",
- "createFailed": "No se pudo crear el token. Inténtalo de nuevo."
+ "createFailed": "No se pudo crear el token. Inténtalo de nuevo.",
+ "namePlaceholder": "Nombre, p. ej. Paperless-ngx"
},
- "tokenMintMovedDescription": "Los tokens se emiten donde se aplican: arriba para las mediciones y los documentos, o en el endpoint de API de un medicamento para ese medicamento concreto.",
- "tokenNamePlaceholder": "Nombre del token (p. ej., Atajo de iPhone)",
- "tokenCreated": "Token creado — ¡cópialo ahora! No se mostrará de nuevo.",
+ "tokenMintMovedDescription": "Los tokens se emiten donde se aplican: arriba para las mediciones, los entrenamientos y los documentos, o en el endpoint de API de un medicamento para ese medicamento concreto.",
+ "tokenCreated": "Token creado. ¡Cópialo ahora! No se mostrará de nuevo.",
"tokenCopy": "Copiar token",
"tokenCopied": "Token copiado al portapapeles",
"tokenCopyFailed": "No se pudo copiar el token",
@@ -6374,7 +6607,7 @@
"deleteAccountCta": "Eliminar cuenta",
"deleteAccountConfirmTitle": "¿Eliminar la cuenta de forma permanente?",
"deleteAccountConfirmDescription": "Tu cuenta, todos los datos de salud, las claves de acceso, las sesiones y las entradas de auditoría se eliminarán de forma permanente. Se cerrará tu sesión inmediatamente después.",
- "deleteAccountSuccess": "Cuenta eliminada — cerrando tu sesión.",
+ "deleteAccountSuccess": "Cuenta eliminada. Cerrando tu sesión.",
"deleteAccountFailed": "No se pudo eliminar la cuenta",
"deleteAccountFinal": "Eliminar permanentemente",
"saved": "Guardado",
@@ -6397,7 +6630,7 @@
"testing": "Probando…",
"ok": "Conectado (latencia {latency} ms)",
"errors": {
- "credentials_rejected": "Credenciales rechazadas — comprueba tu token",
+ "credentials_rejected": "Credenciales rechazadas: comprueba tu token",
"rate_limited": "Limitado por el servidor remoto",
"timeout": "La solicitud agotó el tiempo de espera",
"upstream_error": "El servidor remoto devolvió un error",
@@ -6408,7 +6641,7 @@
"private_origin_not_approved": "Destino en una red privada. El operador debe añadir este origen a NOTIFICATION_PRIVATE_ORIGINS",
"private_origin_not_grantable": "Esta dirección es de enlace local, de metadatos o no especificada; ninguna autorización del operador puede abrirla. Usa la dirección en la que escucha el relay",
"url_invalid": "La URL no es válida",
- "redirected": "El endpoint redirige — comprueba la URL",
+ "redirected": "El endpoint redirige: comprueba la URL",
"endpoint_not_found": "No se encontró el endpoint en la URL",
"credentials_unreadable": "Las credenciales no se pueden descifrar",
"upstream_invalid_json": "El servidor remoto envió un JSON no válido",
@@ -6419,7 +6652,7 @@
},
"notificationStatus": {
"title": "Fiabilidad de los canales",
- "description": "Estado de cada canal — «Desactivado automáticamente» significa detenido tras errores permanentes.",
+ "description": "Estado de cada canal; «Desactivado automáticamente» significa detenido tras errores permanentes.",
"emptyDescription": "Configura un canal en Ajustes › Integraciones › Canales de entrega.",
"stateActive": "Activo",
"stateAutoDisabled": "Desactivado automáticamente",
@@ -6477,7 +6710,7 @@
"createTitle": "Nuevo enlace para compartir",
"createDescription": "El enlace es de solo lectura y limitado en el tiempo.",
"label": "Etiqueta",
- "labelPlaceholder": "p. ej. Dra. García — revisión anual",
+ "labelPlaceholder": "p. ej. Dra. García, revisión anual",
"range": "Días de historial",
"rangeHint": "Hasta cuándo se remonta la ventana compartida.",
"expiry": "Caduca en (días)",
@@ -6497,7 +6730,7 @@
"statusRevoked": "Revocado",
"statusExpired": "Caducado",
"inactiveTitle": "Revocados y caducados ({count})",
- "shownOnce": "Se muestra una sola vez. Guarde ahora el código QR y la frase de acceso — no se pueden recuperar después.",
+ "shownOnce": "Se muestra una sola vez. Guarde ahora el código QR y la frase de acceso: no se pueden recuperar después.",
"qrAlt": "Código QR con el enlace para compartir y la frase de acceso",
"linkLabel": "Enlace",
"passphraseLabel": "Frase de acceso",
@@ -6526,7 +6759,8 @@
"statusNeedsReselect": "Elegir contenido de nuevo",
"reselectReason": "Este enlace mostraba un alcance que nadie eligió, por eso se cerró. Crea uno nuevo y elige su contenido.",
"reselectAction": "Crear un reemplazo",
- "createDetail": "El token se muestra una sola vez al crearlo — guárdalo entonces; no se puede recuperar."
+ "createDetail": "El token se muestra una sola vez al crearlo. Guárdalo entonces; no se puede recuperar.",
+ "activeDescription": "Los enlaces que has compartido, cuándo caducan y cuántas veces se abrieron."
},
"genderOther": "Otro",
"nightscout": "Nightscout",
@@ -6558,7 +6792,7 @@
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
"polarCredentialsSaved": "Credenciales guardadas",
- "polarCredentialsSavedPlaceholder": "Guardado — introduce nuevas para reemplazar",
+ "polarCredentialsSavedPlaceholder": "Guardado. Introduce nuevas para reemplazar",
"polarSaveCredentials": "Guardar",
"polarUnavailable": "Polar no está configurado en este servidor. Añade arriba tus propias credenciales OAuth de Polar, o pide a tu administrador que configure la app compartida.",
"polarConnect": "Conectar Polar",
@@ -6587,7 +6821,7 @@
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
"ouraCredentialsSaved": "Credenciales guardadas",
- "ouraCredentialsSavedPlaceholder": "Guardado — introduce nuevas para reemplazar",
+ "ouraCredentialsSavedPlaceholder": "Guardado. Introduce nuevas para reemplazar",
"ouraSaveCredentials": "Guardar",
"ouraUnavailable": "Oura no está configurado en este servidor. Añade arriba tus propias credenciales OAuth de Oura, o pide a tu administrador que configure la app compartida.",
"ouraConnect": "Conectar Oura",
@@ -6607,6 +6841,7 @@
"nocreds": "Oura no está configurado en este servidor. Pide a tu administrador que añada las credenciales OAuth de Oura.",
"token": "El intercambio de token con Oura falló. Inicia la conexión de nuevo.",
"rate_limited": "Demasiados intentos de conexión. Espera un momento e inténtalo de nuevo.",
+ "issuer": "La respuesta de inicio de sesión no procedía del servidor de autorización de Oura, así que la conexión se detuvo por tu seguridad. Vuelve a iniciar la conexión. Si sigue ocurriendo, avisa a tu administrador.",
"generic": "Error desconocido al conectar. Inicia la conexión de nuevo."
},
"integrationSetupGuide": "Guía de configuración",
@@ -6618,10 +6853,17 @@
"savedOn": "Coach activado.",
"savedOff": "Coach oculto.",
"saveError": "No se pudo guardar el cambio. Inténtalo de nuevo."
+ },
+ "memory": {
+ "edit": "Editar",
+ "source": {
+ "user": "de ti",
+ "coach": "del Coach"
+ }
}
},
"integrationRedirectGuide": {
- "intro": "Esto se conecta mediante tu propia app de desarrollador de {provider} — unos 5 minutos de configuración:",
+ "intro": "Esto se conecta mediante tu propia app de desarrollador de {provider}, con unos 5 minutos de configuración:",
"step1": "Crea una app en el portal de desarrolladores de {provider}.",
"step2": "Configura su URL de redirección/callback en:",
"step3": "Pega arriba el ID de cliente y el secreto de cliente que te da."
@@ -6745,12 +6987,11 @@
},
"stored": {
"title": "Qué se almacena",
- "measurements": "Mediciones (peso, presión arterial, pulso, glucosa, sueño, composición corporal)",
- "medications": "Medicamentos e historial de tomas",
- "moodLabs": "Entradas de estado de ánimo y resultados de laboratorio",
- "coach": "Conversaciones con el asistente",
+ "measurements": "Mediciones: peso, presión arterial, pulso y cualquier otro valor que registres tú o un dispositivo",
"integrations": "Tokens de integración (cifrados)",
- "security": "Actividad de la cuenta y de seguridad (sesiones, registro de inicio de sesión)"
+ "security": "Actividad de la cuenta y de seguridad (sesiones, registro de inicio de sesión)",
+ "profile": "Tu perfil, tus antecedentes médicos y los datos del historial de salud",
+ "modules": "Registros de cada módulo: {list}"
},
"retention": {
"title": "Retención",
@@ -6781,8 +7022,6 @@
"mcp": {
"enableTitle": "Endpoint MCP remoto",
"enableDescription": "Permite que un asistente compatible con MCP (Claude.ai, ChatGPT) lea tus datos de salud mediante un token limitado.",
- "enabled": "Activado",
- "disabled": "Desactivado",
"enableToggleLabel": "Activar el endpoint MCP",
"endpointLabel": "URL del conector",
"connectDescription": "Pega esta URL en la configuración del conector personalizado de tu asistente y autoriza con el token de abajo o inicia sesión cuando se te solicite.",
@@ -6801,7 +7040,8 @@
"connectionRevokeDescription": "Esto finaliza el acceso del asistente de inmediato e impide su renovación. Será necesario autorizarlo de nuevo para volver a conectarlo.",
"connectionRevokeFailed": "No se pudo desconectar el asistente.",
"tokensDetail": "El token se muestra una sola vez.",
- "connectionsDetail": "Revocar una conexión finaliza su acceso de inmediato, incluida su capacidad de renovación."
+ "connectionsDetail": "Revocar una conexión finaliza su acceso de inmediato, incluida su capacidad de renovación.",
+ "tokenNamePlaceholder": "Nombre, p. ej. Claude Desktop"
},
"strava": "Strava",
"stravaDescription": "Conecta tu cuenta de Strava para sincronizar tus actividades con tu historial de entrenamientos.",
@@ -6810,7 +7050,7 @@
"stravaClientId": "ID de cliente",
"stravaClientSecret": "Secreto de cliente",
"stravaCredentialsSaved": "Credenciales guardadas",
- "stravaCredentialsSavedPlaceholder": "Guardadas — introduce nuevas para reemplazar",
+ "stravaCredentialsSavedPlaceholder": "Guardadas. Introduce nuevas para reemplazar",
"stravaSaveCredentials": "Guardar",
"stravaUnavailable": "Strava no está configurado en este servidor. Añade arriba tus propias credenciales de API de Strava o pide a tu administrador que configure la app compartida.",
"stravaConnect": "Conectar Strava",
@@ -6848,26 +7088,33 @@
"title": "Progreso de sincronización",
"receivedLabel": "Registros recibidos",
"oldestLabel": "Lectura más antigua alcanzada",
- "flowing": "Han llegado datos en la última hora — la app del iPhone sigue enviando.",
- "waiting": "Esperando a que la app del iPhone envíe más datos. La transferencia continúa cuando la app está abierta o cuando iOS le concede tiempo en segundo plano — una pausa durante la primera sincronización grande es normal, no un error."
+ "flowing": "Han llegado datos en la última hora. La app del iPhone sigue enviando.",
+ "waiting": "Esperando a que la app del iPhone envíe más datos. La transferencia continúa cuando la app está abierta o cuando iOS le concede tiempo en segundo plano. Una pausa durante la primera sincronización grande es normal, no un error."
},
"delivery": {
"title": "Entrega",
"lastTriggerLabel": "La última sincronización llegó",
"lastBackgroundLabel": "Última sincronización en segundo plano",
- "backgroundNever": "Nunca — hasta ahora todas las sincronizaciones llegaron con la app abierta",
+ "backgroundNever": "Nunca: hasta ahora todas las sincronizaciones llegaron con la app abierta",
"noSyncYet": "Aún no ha llegado ninguna sincronización, así que no hay nada que informar aquí.",
"trigger": {
"foreground": "con la app abierta",
"background": "en segundo plano",
"push": "desde un aviso push",
- "unknown": "la app no informó del desencadenante"
+ "unknown": "la app no informó del desencadenante",
+ "manual": "desde «Sincronizar todo»"
}
+ },
+ "freshness": {
+ "title": "Por tipo de dato",
+ "lastReceived": "Última recepción {relative}",
+ "lastNewValue": "Último valor nuevo {relative}",
+ "never": "Aún no recibido"
}
},
"garminInfo": {
"title": "Garmin",
- "body": "Garmin no tiene conexión directa aquí. Conéctalo a través de Apple Health (iOS) o Google Health Connect (Android). Las métricas propias de Garmin — Body Battery, Training Load, estado de HRV, VO₂ máx.— no las comparte Garmin con ninguna de las dos plataformas.",
+ "body": "Garmin no tiene conexión directa aquí. Conéctalo a través de Apple Health (iOS) o Google Health Connect (Android). Las métricas propias de Garmin (Body Battery, Training Load, estado de HRV, VO₂ máx.) no las comparte Garmin con ninguna de las dos plataformas.",
"docsLink": "Leer la guía"
},
"glucoseReference": {
@@ -6923,6 +7170,12 @@
"description": "Esto comparte o exporta tus datos de salud. Confirma que eres tú para continuar.",
"signInAgain": "Vuelve a iniciar sesión y inténtalo de nuevo. Tu cuenta no tiene contraseña ni llave de acceso que confirmar aquí.",
"failed": "No ha funcionado. Revisa lo que has introducido e inténtalo de nuevo."
+ },
+ "passwordTitle": "Contraseña",
+ "tokenNameLabel": "Nombre del token",
+ "displayCard": {
+ "title": "Visualización",
+ "description": "Idioma, unidades y formatos; se guardan en cuanto los cambias."
}
},
"admin": {
@@ -6955,14 +7208,14 @@
"aiServerKey": {
"title": "Clave de IA del servidor",
"description": "Una clave del operador para las cuentas que no tienen una propia.",
- "byokHint": "La clave propia del usuario (BYOK, inicio de sesión de ChatGPT o proveedor local) siempre tiene prioridad — la clave del servidor es el último paso de la cadena de proveedores.",
+ "byokHint": "La clave propia del usuario (BYOK, inicio de sesión de ChatGPT o proveedor local) siempre tiene prioridad; la clave del servidor es el último paso de la cadena de proveedores.",
"consentHint": "Las solicitudes con la clave del servidor requieren el consentimiento de IA activo del usuario; sin un recibo de consentimiento no se envía nada.",
"baseUrlHint": "La URL base determina la API: se reconoce una URL de Anthropic como https://api.anthropic.com/v1, cualquier otro host se llama a través de la API compatible con OpenAI.",
"statusConfigured": "Configurada",
"statusNotConfigured": "Sin configurar",
"keyLabel": "Clave de API",
"keyPlaceholder": "sk-...",
- "keyPlaceholderSet": "Guardada ({preview}) — introduce una clave nueva para reemplazarla",
+ "keyPlaceholderSet": "Guardada ({preview}). Introduce una clave nueva para reemplazarla",
"modelLabel": "Modelo",
"baseUrlLabel": "URL base",
"save": "Guardar",
@@ -6996,7 +7249,10 @@
"typeCodex": "Inicio de sesión de ChatGPT (personal)",
"typeLocal": "Modelo local (personal)",
"typeGateway": "Pasarela compatible con OpenAI (personal)",
- "spendToday": "Hoy ({date}): {total} tokens, {operator} de ellos en la clave del operador."
+ "spendToday": "Hoy, en el día natural de cada cuenta: {total} tokens, {operator} de ellos en la clave del operador (cuentas con consumo: {accounts}).",
+ "statusPaused": "En pausa",
+ "pausedHint": "En pausa tras fallos repetidos para {count} usuarios hasta {at}. Después se vuelve a intentar automáticamente.",
+ "modelNotServed": "El endpoint ya no ofrece el modelo {model}. Elige en los ajustes de IA un modelo que ofrezca."
},
"aiQuality": {
"title": "Calidad de los análisis",
@@ -7040,9 +7296,9 @@
"defaultLanguage": "Idioma predeterminado",
"defaultLanguageDescription": "Se usa para los nuevos usuarios y como reserva.",
"defaultUserTimezone": "Zona horaria predeterminada para cuentas nuevas",
- "defaultUserTimezoneDescription": "Se usa para cuentas nuevas que no tienen una zona detectada por el navegador. No afecta a los usuarios existentes.",
+ "defaultUserTimezoneDescription": "Se usa en cuentas nuevas sin zona detectada por el navegador; las cuentas existentes conservan la suya.",
"defaultUserTimezoneFallback": "Europe/Berlin (predeterminada)",
- "servicesGlobal": "Servicios (global)",
+ "servicesGlobal": "Canales y API",
"servicesGlobalDescription": "Los servicios desactivados no están disponibles para ningún usuario.",
"telegramGlobal": "Permitir notificaciones de Telegram.",
"ntfyGlobal": "Permitir notificaciones de ntfy.",
@@ -7050,19 +7306,21 @@
"integrationWebPush": "Web Push",
"offlineGeoLabel": "Datos geográficos",
"offlineGeoEnabled": "Sin conexión (GeoLite2)",
- "offlineGeoFallback": "En línea ({host}) — monta las bases GeoLite2 y define GEOLITE2_DIR",
+ "offlineGeoFallback": "En línea ({host}): monta las bases GeoLite2 y define GEOLITE2_DIR",
+ "offlineGeoOff": "Desactivado",
+ "offlineGeoChosen": "En línea ({host})",
"apiGlobal": "Permitir endpoints de API y tokens de API.",
"umamiTitle": "Umami",
- "umamiDescription": "Analítica web con Umami",
+ "umamiDescription": "Analítica web con Umami.",
"glitchtipTitle": "GlitchTip",
- "glitchtipDescription": "Seguimiento de errores con GlitchTip",
+ "glitchtipDescription": "Seguimiento de errores con GlitchTip.",
"webPushVapidTitle": "Web Push VAPID",
- "umamiEnabled": "Activar el seguimiento de Umami.",
+ "umamiEnabled": "Activar el seguimiento de Umami",
"umamiScriptUrl": "URL del script de Umami",
"umamiScriptUrlPlaceholder": "https://analytics.example.com/script.js",
"umamiWebsiteId": "ID del sitio web de Umami",
"umamiWebsiteIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "glitchtipEnabled": "Activar el seguimiento de errores de GlitchTip.",
+ "glitchtipEnabled": "Activar el seguimiento de errores de GlitchTip",
"glitchtipDsn": "GlitchTip DSN",
"glitchtipDsnPlaceholder": "https://@glitchtip.example.com/",
"glitchtipEnvironment": "Entorno de GlitchTip",
@@ -7113,18 +7371,23 @@
"documentAi": {
"title": "Lectura de documentos",
"description": "Lectura con IA de documentos guardados, escaneo de informes de laboratorio y extracción de medicamentos a partir de texto. La mayor parte de los datos que salen del servidor."
+ },
+ "reasoning": {
+ "enabled": "Permitir razonamiento profundo",
+ "max": "Profundidad máxima de razonamiento",
+ "description": "Los modelos piensan antes de responder: el Coach, el resumen diario y los resúmenes por periodo. Más precisos, usan más tokens."
}
},
"modules": {
"title": "Disponibilidad de módulos",
"description": "Desactiva un módulo para todo el servidor.",
"saveError": "No se pudo actualizar la disponibilidad de módulos.",
- "detail": "Un módulo desactivado aquí queda apagado para todas las cuentas, sin importar la preferencia personal. Los dominios principales (peso, presión arterial, pulso, medicación) están siempre activos y no se pueden desactivar.",
- "coachFollowsSwitch": "Sigue el interruptor del Coach en Asistente."
+ "detail": "El peso, la presión arterial y el pulso son datos básicos y siempre están activos.",
+ "coachFollowsSwitch": "Sigue el interruptor del Coach en la sección Coach."
},
"invites": {
- "title": "Invitaciones",
- "description": "Enlaces de invitación para el registro: funcionan incluso con el registro cerrado.",
+ "title": "Enlaces de invitación",
+ "description": "Cada enlace permite un registro aunque el registro esté cerrado.",
"create": "Crear invitación",
"createTitle": "Crear invitación",
"createDescription": "El enlace permite registrarse incluso con el registro cerrado.",
@@ -7170,8 +7433,8 @@
"revokedToast": "Invitación revocada.",
"revokeError": "No se pudo revocar la invitación."
},
- "userManagement": "Gestión de usuarios",
- "userManagementDescription": "Gestiona las cuentas: roles, restablecimientos de contraseña y cierres de sesión forzados.",
+ "userManagement": "Cuentas",
+ "userManagementDescription": "Roles, restablecimientos de contraseña y cierres de sesión forzados de cada cuenta.",
"userName": "Nombre",
"userEmail": "Correo electrónico",
"userRole": "Rol",
@@ -7205,7 +7468,6 @@
"tokenNeverUsed": "Nunca",
"tokensEmptyTitle": "Aún no hay tokens de API",
"tokensEmptyDescription": "Los tokens se emiten cuando un cliente nativo inicia sesión o desde Ajustes → Cuenta.",
- "loginOverview": "Resumen de inicios de sesión",
"loginOverviewDescription": "Eventos de inicio de sesión y autenticación de todas las cuentas, filtrables y exportables como CSV.",
"allAuthEvents": "Todos los eventos de autenticación",
"failedOnly": "Solo fallidos",
@@ -7238,6 +7500,7 @@
"finalDelete": "Eliminar permanentemente",
"deletedResult": "Eliminadas {rows} filas en {tables} tablas. Se conservaron las cuentas de usuario y las credenciales de inicio de sesión.",
"authRegister": "Registro",
+ "authClaim": "Perfil asumido",
"authLogin": "Iniciar sesión",
"authLoginPasskey": "Inicio de sesión con passkey",
"authLoginPassword": "Inicio de sesión con contraseña",
@@ -7312,7 +7575,7 @@
},
"services": {
"title": "Servicios",
- "subtitle": "Activa o desactiva los canales de notificación y las integraciones en todo el sistema."
+ "subtitle": "Activa o desactiva los canales de notificación y la API en todo el servidor."
},
"integrations": {
"title": "Integraciones",
@@ -7398,7 +7661,8 @@
"refresh": "Actualizar",
"eventDetails": "Detalles del evento",
"closeDetails": "Cerrar detalles",
- "processDetail": "El búfer es volátil y está limitado a 500 entradas."
+ "processDetail": "El búfer es volátil y está limitado a 500 entradas.",
+ "cardTitle": "Eventos recientes"
},
"backups": {
"title": "Copias de seguridad",
@@ -7425,6 +7689,12 @@
"scheduleFailedDescription": "{when}: {reason}",
"scheduleFailedNoReason": "sin motivo registrado",
"scheduleRemedy": "Ejecuta una copia ahora y comprueba que aparece una fila nueva. Si vuelve a fallar, el registro del worker indica la cuenta en la que se detuvo.",
+ "activity": {
+ "running": "En curso desde {when}.",
+ "interruptedTitle": "Una ejecución se detuvo a medias y no volvió",
+ "interruptedAccount": "{username}: empezó {when}, nunca terminó",
+ "interruptedRemedy": "Lo más probable es que la aplicación se reiniciara o se quedara sin memoria al escribir esta copia. La próxima ejecución deja estas cuentas para el final, así las demás reciben su copia primero. Si se repite, dale más memoria al contenedor."
+ },
"download": "Descargar",
"downloadAria": "Descargar copia de seguridad de {username}",
"downloadFailed": "La descarga falló",
@@ -7444,7 +7714,7 @@
"restorePromptLabel": "Escribe RESTORE para continuar",
"previewTitle": "Contenido de la copia",
"previewLoading": "Leyendo el contenido…",
- "previewUnavailable": "No se pudo leer el contenido — restaurar sigue siendo posible.",
+ "previewUnavailable": "No se pudo leer el contenido, pero restaurar sigue siendo posible.",
"previewStillReading": "Esta copia se hizo con una versión anterior, así que su contenido se está leyendo completo, y con un historial grande eso lleva un rato. Vuelve a abrir este diálogo en unos minutos para verlo. Restaurar no depende de ello.",
"previewEmpty": "El archivo no contiene registros.",
"previewOther": "Otros registros",
@@ -7555,7 +7825,12 @@
"restoreFailureKeyMissing": "La copia necesita la clave de cifrado {keys}, con la que este servidor no puede abrirla. Vuelve a añadir la clave a ENCRYPTION_KEYS, reinicia e inicia de nuevo la restauración.",
"restoreFailureForeignReference": "El archivo apunta a registros que pertenecen a otra cuenta. No se escribió nada.",
"previewKeyMissing": "Esta copia no se puede restaurar en este servidor: necesita la clave de cifrado {keys}. Vuelve a añadir la clave a ENCRYPTION_KEYS y reinicia primero.",
- "restoreSkippedSymptomEpisodeReference": "Entrada de síntoma vinculada a una enfermedad que el archivo no contiene (se conserva sin el vínculo)"
+ "restoreSkippedSymptomEpisodeReference": "Entrada de síntoma vinculada a una enfermedad que el archivo no contiene (se conserva sin el vínculo)",
+ "restoreSkippedCycleCiphertext": "Nota o etiqueta del ciclo cifrada con una clave que este servidor no tiene (el día o el síntoma se conserva sin ella)",
+ "restoreSkippedMoodLabelCiphertext": "Nombre de etiqueta o grupo de ánimo cifrado con una clave que este servidor no tiene (se conserva sin el nombre)",
+ "restoreSkippedVisitCiphertext": "Texto de visita o profesional cifrado con una clave que este servidor no tiene (se conserva sin él)",
+ "restoreSkippedVaccinationCiphertext": "Nota de vacunación cifrada con una clave que este servidor no tiene (la dosis se conserva sin ella)",
+ "restoreSkippedEnvironmentLocationCiphertext": "Ubicación de un periodo o día ambiental cifrada con una clave que este servidor no tiene (se conserva cifrada, esos días no se actualizan)"
},
"danger-zone": {
"title": "Zona de peligro",
@@ -7572,7 +7847,6 @@
"encryption": {
"title": "Cifrado",
"subtitle": "Cobertura de cifrado y estado de rotación de claves para los datos en reposo.",
- "loading": "Cargando el estado de cifrado...",
"loadError": "No se pudo cargar el estado de cifrado.",
"coverageTitle": "Cobertura",
"coverageDescription": "Cuántas filas cifradas hay bajo cada id de clave.",
@@ -7617,12 +7891,45 @@
"backupsNone": "Ninguna copia ha registrado todavía las claves que necesita.",
"backupsStillNeed": "Todas las filas están rotadas, pero las copias aún necesitan {keys}",
"backupsUnrecordedBadge": "Todas las filas están rotadas, pero copias antiguas pueden necesitar aún una clave retirada",
- "backupsDetail": "La rotación vuelve a cifrar cada fila, pero una copia conserva la clave con la que se escribió su contenido, y restaurarla necesita esa clave. Mantén una clave retirada en ENCRYPTION_KEYS hasta que no aparezca abajo."
+ "backupsDetail": "La rotación vuelve a cifrar cada fila, pero una copia conserva la clave con la que se escribió su contenido, y restaurarla necesita esa clave. Mantén una clave retirada en ENCRYPTION_KEYS hasta que no aparezca abajo.",
+ "columnsShow": "Mostrar la tabla ({count} con filas)",
+ "columnsHide": "Ocultar la tabla",
+ "columnsShowAll": "Mostrar las {count} columnas",
+ "columnsShowWithRows": "Solo columnas con filas",
+ "columnsNoneWithRows": "Ninguna columna cifrada contiene filas todavía."
+ },
+ "maintenance": {
+ "title": "Mantenimiento de la tabla de mediciones",
+ "description": "Libera el espacio que ocupaban los restos antiguos de sincronización, cuando la limpieza ya ha terminado.",
+ "body": "Ejecuta un vacuum de la tabla de mediciones y después reconstruye sus índices, uno a uno. Leer y guardar siguen funcionando mientras se ejecuta. Elige una hora tranquila y deja espacio libre en disco para una copia más del índice más grande.",
+ "tableSize": "Tabla con índices",
+ "indexSize": "Índices",
+ "loading": "Comprobando el estado del mantenimiento…",
+ "loadError": "No se pudo leer el estado del mantenimiento.",
+ "unavailable": "No hay ningún proceso en segundo plano conectado, así que el mantenimiento no puede ejecutarse aquí.",
+ "purgePending": "La limpieza de restos antiguos de sincronización sigue en marcha. El mantenimiento podrá empezar cuando termine.",
+ "state": {
+ "idle": "Aún no se ha ejecutado.",
+ "queued": "Esperando al proceso en segundo plano.",
+ "running": "En marcha desde las {since}. En una instancia grande puede tardar un rato.",
+ "completed": "Terminado el {at}.",
+ "refusedPurge": "No empezó porque la limpieza seguía en marcha. Vuelve a iniciarlo cuando la limpieza haya terminado.",
+ "alreadyRunning": "No empezó porque ya había otra ejecución trabajando.",
+ "failed": "Detenido el {at}. El registro de la aplicación indica el paso y el runbook explica qué revisar."
+ },
+ "start": "Iniciar mantenimiento",
+ "confirmTitle": "¿Iniciar el mantenimiento ahora?",
+ "confirmBody": "Se ejecuta en segundo plano, paso a paso, y puede tardar un rato. Si hoy aún no has hecho una copia de seguridad, hazla antes.",
+ "confirm": "Iniciar",
+ "cancel": "Cancelar",
+ "started": "Mantenimiento iniciado.",
+ "alreadyQueued": "Ya hay un mantenimiento en cola o en marcha.",
+ "startFailed": "No se pudo iniciar el mantenimiento."
}
},
"carrier": "Operador",
"mfaRequired": "Exigir un segundo factor",
- "mfaRequiredDescription": "Cada cuenta sin un segundo factor activo se dirige a la configuración tras iniciar sesión. Esto pide a los usuarios web que lo configuren, pero no bloquea el acceso directo por API o nativo (Bearer) de las cuentas que aún no lo tienen.",
+ "mfaRequiredDescription": "Las cuentas sin segundo factor van a configurarlo tras iniciar sesión en la web; el acceso por API o nativo (Bearer) sigue abierto.",
"mfaEnforcedOn": "MFA activado",
"mfaEnforcedOff": "MFA desactivado",
"mfaEnforcedOnHint": "Este usuario debe usar un segundo factor. Haz clic para dejar de exigirlo.",
@@ -7720,7 +8027,9 @@
"restartNote": "Reinicia HealthLog después del cambio.",
"lastResort": "Último recurso, solo si estás seguro de que la clave es correcta y ninguna de las tres opciones sirve: define {setting} y reinicia. HealthLog sirve igualmente y solo registra el hallazgo. Esto desactiva una comprobación de seguridad: con una clave incorrecta falla cada valor cifrado y los datos nuevos se escriben con esa clave.",
"codes": "Para el administrador: las peticiones a la API responden 503 con el código de error {code}, y /api/health indica el motivo {reason}."
- }
+ },
+ "loginOverviewCardTitle": "Eventos de inicio de sesión",
+ "userColumn": "Usuario"
},
"achievements": {
"title": "Logros",
@@ -7736,7 +8045,9 @@
"noneUnlockedYet": "Aún no has desbloqueado ningún logro.",
"pointsValue": "{points} puntos",
"metricCount": "{count}",
- "metricDays": "{count} días",
+ "metricDaysOne": "{count} día",
+ "metricDaysFew": "{count} días",
+ "metricDaysOther": "{count} días",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -7759,14 +8070,6 @@
"title": "Leyenda de las tomas",
"description": "Registra 300 tomas de medicación."
},
- "overIntake1": {
- "title": "Dosis doble",
- "description": "Toma la medicación al menos una vez más de lo previsto."
- },
- "skippedIntake1": {
- "title": "Un paso atrás",
- "description": "Omite al menos una toma planificada."
- },
"passkeyCreated1": {
"title": "Clave de acceso creada",
"description": "Crea tu primera clave de acceso."
@@ -7792,19 +8095,19 @@
"description": "Toma todos los medicamentos a tiempo durante 1 día."
},
"compliance801": {
- "title": "Adherencia 80% · 1d",
+ "title": "Adherencia del 80% durante 1 día",
"description": "Alcanza al menos un 80% de adherencia de 30 días durante 1 día."
},
"bmiGreen1": {
- "title": "IMC en verde · 1d",
+ "title": "IMC en verde durante 1 día",
"description": "Mantén tu IMC en el rango normal durante 1 día."
},
"bpGreen1": {
- "title": "Presión en verde · 1d",
+ "title": "Presión en verde durante 1 día",
"description": "Mantén tu presión arterial en el rango normal durante 1 día."
},
"pulseGreen1": {
- "title": "Pulso en verde · 1d",
+ "title": "Pulso en verde durante 1 día",
"description": "Mantén tu pulso en reposo en el rango normal durante 1 día."
},
"onTimePerfect7": {
@@ -7812,19 +8115,19 @@
"description": "Toma todos los medicamentos a tiempo durante 7 días seguidos."
},
"compliance807": {
- "title": "Adherencia 80% · 7d",
+ "title": "Adherencia del 80% durante 7 días",
"description": "Alcanza al menos un 80% de adherencia de 30 días durante 7 días seguidos."
},
"bmiGreen7": {
- "title": "IMC en verde · 7d",
+ "title": "IMC en verde durante 7 días",
"description": "Mantén tu IMC en el rango normal durante 7 días seguidos."
},
"bpGreen7": {
- "title": "Presión en verde · 7d",
+ "title": "Presión en verde durante 7 días",
"description": "Mantén tu presión arterial en el rango normal durante 7 días seguidos."
},
"pulseGreen7": {
- "title": "Pulso en verde · 7d",
+ "title": "Pulso en verde durante 7 días",
"description": "Mantén tu pulso en reposo en el rango normal durante 7 días seguidos."
},
"onTimePerfect30": {
@@ -7832,19 +8135,19 @@
"description": "Toma todos los medicamentos a tiempo durante 30 días seguidos."
},
"compliance8030": {
- "title": "Adherencia 80% · 30d",
+ "title": "Adherencia del 80% durante 30 días",
"description": "Alcanza al menos un 80% de adherencia de 30 días durante 30 días seguidos."
},
"bmiGreen30": {
- "title": "IMC en verde · 30d",
+ "title": "IMC en verde durante 30 días",
"description": "Mantén tu IMC en el rango normal durante 30 días seguidos."
},
"bpGreen30": {
- "title": "Presión en verde · 30d",
+ "title": "Presión en verde durante 30 días",
"description": "Mantén tu presión arterial en el rango normal durante 30 días seguidos."
},
"pulseGreen30": {
- "title": "Pulso en verde · 30d",
+ "title": "Pulso en verde durante 30 días",
"description": "Mantén tu pulso en reposo en el rango normal durante 30 días seguidos."
},
"onTimePerfect180": {
@@ -7852,19 +8155,19 @@
"description": "Toma todos los medicamentos a tiempo durante 180 días seguidos."
},
"compliance80180": {
- "title": "Adherencia 80% · 180d",
+ "title": "Adherencia del 80% durante 180 días",
"description": "Alcanza al menos un 80% de adherencia de 30 días durante 180 días seguidos."
},
"bmiGreen180": {
- "title": "IMC en verde · 180d",
+ "title": "IMC en verde durante 180 días",
"description": "Mantén tu IMC en el rango normal durante 180 días seguidos."
},
"bpGreen180": {
- "title": "Presión en verde · 180d",
+ "title": "Presión en verde durante 180 días",
"description": "Mantén tu presión arterial en el rango normal durante 180 días seguidos."
},
"pulseGreen180": {
- "title": "Pulso en verde · 180d",
+ "title": "Pulso en verde durante 180 días",
"description": "Mantén tu pulso en reposo en el rango normal durante 180 días seguidos."
},
"onTimePerfect360": {
@@ -7872,19 +8175,19 @@
"description": "Toma todos los medicamentos a tiempo durante 360 días seguidos."
},
"compliance80360": {
- "title": "Adherencia 80% · 360d",
+ "title": "Adherencia del 80% durante 360 días",
"description": "Alcanza al menos un 80% de adherencia de 30 días durante 360 días seguidos."
},
"bmiGreen360": {
- "title": "IMC en verde · 360d",
+ "title": "IMC en verde durante 360 días",
"description": "Mantén tu IMC en el rango normal durante 360 días seguidos."
},
"bpGreen360": {
- "title": "Presión en verde · 360d",
+ "title": "Presión en verde durante 360 días",
"description": "Mantén tu presión arterial en el rango normal durante 360 días seguidos."
},
"pulseGreen360": {
- "title": "Pulso en verde · 360d",
+ "title": "Pulso en verde durante 360 días",
"description": "Mantén tu pulso en reposo en el rango normal durante 360 días seguidos."
},
"moodFirst": {
@@ -7968,15 +8271,15 @@
"description": "Cambiaste el idioma de la app al menos una vez."
},
"missFree7": {
- "title": "Sin dosis perdidas · 7d",
+ "title": "Sin dosis perdidas en 7 días",
"description": "7 días consecutivos sin una dosis perdida."
},
"missFree30": {
- "title": "Sin dosis perdidas · 30d",
+ "title": "Sin dosis perdidas en 30 días",
"description": "30 días consecutivos sin una dosis perdida."
},
"missFree90": {
- "title": "Sin dosis perdidas · 90d",
+ "title": "Sin dosis perdidas en 90 días",
"description": "90 días consecutivos sin una dosis perdida."
},
"measurementWeeks4": {
@@ -7985,10 +8288,10 @@
},
"selfContextComplete": {
"title": "Autoinforme al día",
- "description": "Los cuatro campos del autoinforme completados — el Coach conoce tu contexto."
+ "description": "Los cuatro campos del autoinforme completados: el Coach conoce tu contexto."
},
"sleepLog7": {
- "title": "Registro de sueño · 7d",
+ "title": "Registro de sueño durante 7 días",
"description": "Datos de sueño registrados durante 7 días consecutivos."
}
},
@@ -8007,7 +8310,7 @@
},
"hiddenCard": {
"title": "Logro oculto",
- "description": "Sigue registrando — quizá te topes con él.",
+ "description": "Sigue registrando y quizá te topes con él.",
"ariaLabel": "Logro oculto bloqueado"
},
"hiddenUnlockToast": {
@@ -8016,7 +8319,7 @@
"dashboardCard": {
"title": "Desbloqueos recientes",
"viewAll": "Ver todo",
- "empty": "Aún no hay logros — sigue registrando para desbloquear el primero."
+ "empty": "Aún no hay logros. Sigue registrando para desbloquear el primero."
}
},
"notifications": {
@@ -8078,9 +8381,6 @@
"enabledToast": "Avisos proactivos del Coach activados.",
"disabledToast": "Avisos proactivos del Coach desactivados.",
"saveError": "No se pudo guardar. Inténtalo de nuevo más tarde.",
- "suggestionsLabel": "Sugerencias de ejemplo",
- "suggestionsDesc": "Muestra de vez en cuando una pregunta de ejemplo en la pantalla del Coach.",
- "suggestionsAria": "Activar o desactivar las sugerencias de ejemplo del Coach",
"aiComposedLabel": "Deja que el Coach redacte el aviso",
"aiComposedDesc": "Cuando hay un proveedor disponible, el aviso proactivo lo redacta el modelo en lugar de la plantilla fija. Desactivado por defecto.",
"aiComposedAria": "Activar o desactivar el aviso proactivo redactado por IA"
@@ -8151,7 +8451,46 @@
"loginRequired": "Inicia sesión para gestionar las notificaciones.",
"someChannelsGloballyDisabled": "Uno o más canales están desactivados en los ajustes y no recibirán notificaciones.",
"reminderSettingsLink": "Ajustes de recordatorios",
- "channelsLink": "Canales"
+ "channelsLink": "Canales",
+ "handover": {
+ "claimedTitle": "{name} ya tiene su propia cuenta",
+ "claimedEnd": "{name} ha asumido su historial de salud. Tu acceso ha terminado.",
+ "claimedRead": "{name} ha asumido su historial de salud. Puedes seguir viéndolo, pero ya no cambiarlo.",
+ "claimedManage": "{name} ha asumido su historial de salud. Puedes seguir gestionándolo hasta que decida otra cosa.",
+ "changedTitle": "{name} ha cambiado tu acceso",
+ "changedEnd": "Tu acceso al historial de salud de {name} ha terminado.",
+ "changedRead": "Ahora puedes ver el historial de salud de {name}, pero no cambiarlo.",
+ "changedManage": "Puedes gestionar el historial de salud de {name}.",
+ "unnamed": {
+ "claimedTitle": "La persona a la que cuidabas ya tiene su propia cuenta",
+ "claimedEnd": "La persona a la que cuidabas ha asumido su historial de salud. Tu acceso ha terminado.",
+ "claimedRead": "La persona a la que cuidabas ha asumido su historial de salud. Puedes seguir viéndolo, pero ya no modificarlo.",
+ "claimedManage": "La persona a la que cuidabas ha asumido su historial de salud. Puedes seguir gestionándolo hasta que decida otra cosa.",
+ "changedTitle": "Tu acceso a un historial de salud ha cambiado",
+ "changedEnd": "Tu acceso al historial de salud de la persona a la que cuidabas ha terminado.",
+ "changedRead": "Ahora puedes ver el historial de salud de la persona a la que cuidabas, pero no modificarlo.",
+ "changedManage": "Puedes gestionar el historial de salud de la persona a la que cuidabas."
+ }
+ },
+ "links": {
+ "open": "Abrir en HealthLog"
+ },
+ "quickActions": {
+ "moodGood": "Bien",
+ "moodOkay": "Normal",
+ "moodBad": "Mal"
+ },
+ "content": {
+ "title": "Valores de salud en las notificaciones",
+ "description": "Si una notificación puede mostrar valores como tu puntuación de la mañana.",
+ "detail": "Desactivado, una notificación solo dice de qué trata, así que la pantalla de bloqueo no muestra valores de salud. Activado, añade los valores, por ejemplo la puntuación de la mañana en el resumen. Al tocar una notificación se abre el lugar adecuado de la app en ambos casos.",
+ "toggleAria": "Mostrar valores de salud en las notificaciones",
+ "statusDetailed": "Con valores",
+ "statusGeneric": "Sin valores",
+ "detailedToast": "Las notificaciones ahora incluyen valores de salud.",
+ "genericToast": "Las notificaciones ya no incluyen valores de salud.",
+ "saveError": "No se pudo guardar. Inténtalo de nuevo más tarde."
+ }
},
"i18n": {
"maintainershipBanner": {
@@ -8182,7 +8521,7 @@
}
},
"edit": {
- "title": "Editar rango objetivo — {metric}",
+ "title": "Editar rango objetivo: {metric}",
"description": "Ajusta el rango objetivo usado en gráficas, insights y el informe médico. Valores en {unit}.",
"derivedHint": "Esta tarjeta se calcula a partir de otras métricas. Edita en su lugar los objetivos subyacentes (p. ej. tensión arterial o peso).",
"minLabel": "Mín",
@@ -8206,15 +8545,19 @@
"currentValue": "Actual: {value} {unit}",
"targetRangeValue": "Rango objetivo: {min}–{max} {unit}",
"sourceLabel": "Fuente: {source}",
+ "sourceNames": {
+ "moodEntries": "Entradas de ánimo",
+ "sevenDays": "7 días"
+ },
"inTarget": "En rango objetivo",
"aboveTarget": "{delta} {unit} por encima del rango",
"belowTarget": "{delta} {unit} por debajo del rango",
"diastolic": "Diastólica",
- "glucoseFasting": "Glucemia — en ayunas",
- "glucosePostprandial": "Glucemia — posprandial",
- "glucoseRandom": "Glucemia — cualquiera",
- "glucoseBedtime": "Glucemia — antes de dormir",
- "glucoseUnspecified": "Glucemia — sin indicar",
+ "glucoseFasting": "Glucemia (en ayunas)",
+ "glucosePostprandial": "Glucemia (posprandial)",
+ "glucoseRandom": "Glucemia (cualquiera)",
+ "glucoseBedtime": "Glucemia (antes de dormir)",
+ "glucoseUnspecified": "Glucemia (sin indicar)",
"status": {
"underweight": "Peso insuficiente",
"normal": "Normal",
@@ -8300,7 +8643,7 @@
"askMedicationName": "Indica el medicamento, p. ej. tomado Ramipril.",
"medicationNotFoundExact": "Medicamento no encontrado. Envía el nombre exactamente como aparece en HealthLog.",
"helpHeader": "Comandos disponibles:",
- "helpBody": "/help — mostrar esta ayuda\n/add — registrar una toma\n/start — iniciar el bot\n\nComandos de texto:\ntaken <nombre> — confirmar una toma\n\nBotones en los recordatorios:\n• Tomado — confirmar la toma\n• 🕐 1h / 🕐 3h — posponer el recordatorio\n• ⏭ Omitir — omitir la toma\n• ✓ Confirmar — confirmar una toma omitida",
+ "helpBody": "/help: mostrar esta ayuda\n/add: registrar una toma\n/start: iniciar el bot\n\nComandos de texto:\ntaken <nombre>: confirmar una toma\n\nBotones en los recordatorios:\n• Tomado: confirmar la toma\n• 🕐 1h / 🕐 3h: posponer el recordatorio\n• ⏭ Omitir: omitir la toma\n• ✓ Confirmar: confirmar una toma omitida",
"measureValuePrompt": "Responde con el valor para registrarlo (opcional).",
"buttonMeasureValue": "Valor…",
"measureValueSaved": "Lectura guardada.",
@@ -8325,9 +8668,9 @@
},
"lowStockReminders": {
"title": "Quedan pocas existencias: {medName}",
- "bodyToday": "{medName}: quedan {units} unidades — menos de un día de suministro.",
- "bodyReorder": "{medName}: quedan {units} unidades. Las existencias se agotan hacia el {runsOutOn} — vuelve a pedir antes del {reorderBy}.",
- "bodyLastDose": "{medName}: quedan {units} unidades — alrededor de tu última dosis. Las existencias se agotan hacia el {runsOutOn}.",
+ "bodyToday": "{medName}: quedan {units} unidades, menos de un día de suministro.",
+ "bodyReorder": "{medName}: quedan {units} unidades. Las existencias se agotan hacia el {runsOutOn}. Vuelve a pedir antes del {reorderBy}.",
+ "bodyLastDose": "{medName}: quedan {units} unidades, alrededor de tu última dosis. Las existencias se agotan hacia el {runsOutOn}.",
"bodyExpired": "{medName}: no quedan existencias utilizables. {units} unidades están marcadas como caducadas.",
"bodyAsNeeded": "{medName}: solo quedan {units} unidades. Vuelve a pedir antes de necesitar la próxima dosis."
},
@@ -8358,7 +8701,7 @@
},
"clinicianView": {
"title": "Registro de salud compartido",
- "period": "Período del informe: {start} – {end}",
+ "period": "Período del informe: {start}–{end}",
"expires": "Este enlace caduca el {date}.",
"provenance": "Este es un resumen de solo lectura compartido por la persona a la que pertenece, exportado de sus datos de HealthLog autorregistrados. Los valores son autoinformados y pueden incluir lecturas sincronizadas desde dispositivos; trátelos como informados por el paciente, no como un examen clínico.",
"labs": "Valores de laboratorio",
@@ -8382,7 +8725,7 @@
"strain": "Puntuación de esfuerzo",
"score": "Puntuación de bienestar"
},
- "footer": "Compartido desde HealthLog — un registro de salud autoalojado.",
+ "footer": "Compartido desde HealthLog, un registro de salud autoalojado.",
"unlock": {
"title": "Introduzca la frase de acceso",
"description": "Este registro de salud compartido está protegido. Introduzca la frase de acceso que recibió para verlo.",
@@ -8417,7 +8760,11 @@
},
"course": "Tratamiento {n}",
"courseRange": "del {start} al {end}",
- "courseSince": "desde el {start}"
+ "courseSince": "desde el {start}",
+ "unavailable": {
+ "title": "Enlace caducado o no válido",
+ "description": "Este enlace para compartir ha caducado, se ha revocado o está incompleto. Pide un enlace nuevo a la persona que lo compartió."
+ }
},
"cycle": {
"symptomCategory": {
@@ -8470,7 +8817,7 @@
},
"crosstab": {
"description": "Cómo se compara cada medición entre tus fases lútea y folicular, con tus propios datos.",
- "pairLabel": "{metric} · lútea vs. folicular",
+ "pairLabel": "{metric}: fase lútea vs. folicular",
"detail": "Lútea {lutealAvg} {unit} ({lutealDays} días) vs. folicular {follicularAvg} {unit} ({follicularDays} días).",
"metricRestingHeartRate": "Frecuencia cardíaca en reposo",
"metricHrv": "Variabilidad de la frecuencia cardíaca",
@@ -8593,8 +8940,8 @@
"ariaUnknown": "Aún no hay ciclo activo",
"caption": "de tu ciclo",
"firstPeriodCta": "Registrar mi primer periodo",
- "periodOverdue": "Tu período llega más tarde de lo habitual — el conteo de días se pausa hasta que registres el próximo período.",
- "periodOverdueDays": "Tu período llega {days} días más tarde de lo habitual — el conteo de días se pausa hasta que registres el próximo período.",
+ "periodOverdue": "Tu período llega más tarde de lo habitual. El conteo de días se pausa hasta que registres el próximo período.",
+ "periodOverdueDays": "Tu período llega {days} días más tarde de lo habitual. El conteo de días se pausa hasta que registres el próximo período.",
"logPeriodCta": "Registrar período"
},
"calendar": {
@@ -8619,7 +8966,7 @@
"predictions": {
"title": "Qué viene después",
"nextPeriod": "Próximo periodo",
- "nextPeriodWindow": "{low} – {high}",
+ "nextPeriodWindow": "{low}–{high}",
"fertileWindow": "Ventana fértil",
"ovulation": "Ovulación estimada",
"confidenceHigh": "Alta",
@@ -8628,7 +8975,7 @@
"stillLearning": "Aún aprendiendo",
"stillLearningBody": "Registra al menos tres ciclos completos y tus predicciones mejorarán. Hasta entonces es una estimación aproximada.",
"rawMode": "Predicción desactivada",
- "rawModeBody": "Has desactivado las predicciones — solo se muestran tus datos registrados, sin pronóstico.",
+ "rawModeBody": "Has desactivado las predicciones: solo se muestran tus datos registrados, sin pronóstico.",
"none": "Aún no hay predicción. Registra un periodo para empezar.",
"ovulationConfirmed": "Ovulación (confirmada)"
},
@@ -8690,7 +9037,7 @@
"temperatureSection": "Temperatura y ovulación",
"intimacySection": "Intimidad y anticoncepción",
"testsSection": "Pruebas",
- "phaseDayHeader": "Día {day} · {phase}",
+ "phaseDayHeader": "Día {day}, {phase}",
"bbtHint": "Mídela a primera hora de la mañana, antes de levantarte.",
"titleForDate": "Registro del {date}",
"descriptionToday": "Hoy. Flujo, síntomas y notas.",
@@ -8818,14 +9165,14 @@
"postpone": {
"menuItem": "Aplazar…",
"title": "Aplazar vencimiento",
- "description": "Solo mueve el vencimiento actual — el ritmo regular no cambia.",
+ "description": "Solo mueve el vencimiento actual; el ritmo regular no cambia.",
"chip": "+{days} días",
"dateLabel": "Nueva fecha",
"confirm": "Aplazar"
},
"skip": {
"heading": "Omitir este vencimiento",
- "explainer": "Se registra honestamente como omitido — no como hecho. El intervalo se reinicia desde hoy.",
+ "explainer": "Se registra honestamente como omitido, no como hecho. El intervalo se reinicia desde hoy.",
"action": "Omitir",
"confirmTitle": "¿Omitir este vencimiento?"
},
@@ -8867,10 +9214,10 @@
"VISCERAL_FAT": "Grasa visceral",
"BODY_MASS_INDEX": "Índice de masa corporal (IMC)",
"WAIST_CIRCUMFERENCE": "Perímetro de cintura",
- "PHQ9_SCORE": "PHQ-9 · ánimo",
- "GAD7_SCORE": "GAD-7 · preocupación",
- "WHO5_SCORE": "WHO-5 · bienestar",
- "SCI_SCORE": "SCI · sueño"
+ "PHQ9_SCORE": "PHQ-9 (ánimo)",
+ "GAD7_SCORE": "GAD-7 (preocupación)",
+ "WHO5_SCORE": "WHO-5 (bienestar)",
+ "SCI_SCORE": "SCI (sueño)"
},
"typeGroups": {
"vitals": "Constantes vitales",
@@ -9028,7 +9375,7 @@
"emptyDescription": "Añade un valor para seguir este biomarcador con el tiempo.",
"title": "Biomarcador",
"genericDescription": "Un biomarcador de laboratorio que sigues. Añade lecturas para ver su evolución frente al rango de referencia.",
- "valuesTitle": "{marker} — todas las lecturas",
+ "valuesTitle": "{marker}: todas las lecturas",
"valuesDescription": "Cada lectura de este biomarcador. Edita o elimina una entrada directamente.",
"backToMarker": "Volver a {marker}",
"latestResult": "Último resultado"
@@ -9199,12 +9546,12 @@
"dateLabel": "Fecha",
"refLowLabel": "Referencia inferior",
"refHighLabel": "Referencia superior",
- "newBiomarker": "Nuevo biomarcador — se creará",
+ "newBiomarker": "Nuevo biomarcador: se creará",
"linksExisting": "Se vincula a {name} existente",
"duplicateWarning": "Ya existe un valor coincidente",
"lowConfidence": "Verifica este valor",
"lowConfidenceBadge": "Confianza baja",
- "valueUnreadable": "Valor ilegible — introdúcelo manualmente",
+ "valueUnreadable": "Valor ilegible: introdúcelo manualmente",
"saveSelected": "Guardar {count} seleccionados",
"discardAll": "Descartar todo",
"savedToast": "{count} resultados de laboratorio guardados",
@@ -9320,6 +9667,10 @@
"vaccinations": {
"label": "Vacunas",
"description": "La cartilla de vacunación estructurada: dosis, serie, próximo refuerzo."
+ },
+ "timeline": {
+ "label": "Cronología",
+ "description": "Años de un vistazo: enfermedades, medicamentos y visitas junto a tus valores, además de tus propios acontecimientos vitales."
}
},
"moduleOff": {
@@ -9351,8 +9702,8 @@
},
"form": {
"date": "Fecha",
- "catalogLabel": "Vacuna (catálogo)",
- "identityHint": "Elige del catálogo o escríbela abajo: basta con una.",
+ "catalogLabel": "Vacuna",
+ "identityHint": "Elige una vacuna de la lista o escríbela abajo. Basta con una.",
"catalogNone": "Elegir una vacuna",
"catalogSearch": "Buscar vacunas",
"catalogNoMatch": "No se encontró ninguna entrada del catálogo",
@@ -9472,10 +9823,31 @@
"overdue": "Refuerzo pendiente desde el {date}",
"dueSoon": "Refuerzo previsto el {date}",
"current": "Próximo refuerzo el {date}"
+ },
+ "custom": {
+ "title": "Vacunas propias",
+ "add": "Añadir vacuna propia",
+ "name": "Nombre",
+ "components": "Protege contra",
+ "seriesDoses": "Dosis de la serie primaria",
+ "boosterMonths": "Refuerzo cada (meses)",
+ "saved": "Vacuna guardada",
+ "deleted": "Vacuna eliminada",
+ "deleteConfirm": "¿Eliminar esta vacuna? Las dosis ya registradas conservan su nombre.",
+ "saveFailed": "No se pudo guardar la vacuna.",
+ "nameTaken": "Ya tienes una vacuna con este nombre.",
+ "editTitle": "Editar vacuna propia",
+ "description": "Una vacuna que el catálogo no recoge, contada como cualquier entrada del catálogo.",
+ "componentsHint": "Elige todas las enfermedades de las que protege.",
+ "boosterHint": "Déjalo vacío si no hay refuerzo.",
+ "protectsAgainst": "Protege contra {list}.",
+ "deleteTitle": "¿Quitar la vacuna propia?",
+ "sectionDescription": "Vacunas que añadiste tú porque el catálogo no las recoge.",
+ "catalogGroup": "Catálogo"
}
},
"illness": {
- "title": "Diario de enfermedad",
+ "title": "Enfermedad",
"subtitle": "Un registro retrospectivo de tus episodios de enfermedad y dolencias.",
"settings": {
"viewDescription": "Elige si los episodios se muestran como tarjetas o como lista compacta.",
@@ -9614,7 +9986,7 @@
"loadError": "No se pudo cargar este episodio.",
"backToList": "Volver al diario",
"detail": {
- "note": "Note"
+ "note": "Nota"
},
"edit": {
"title": "Editar episodio",
@@ -9663,7 +10035,7 @@
"dismiss": "Ahora no",
"applied": "Añadido",
"dismissed": "Descartado",
- "failed": "No se pudo añadir — inténtalo de nuevo",
+ "failed": "No se pudo añadir. Inténtalo de nuevo",
"checkup": {
"title": "¿Añadir un recordatorio de revisión?"
},
@@ -9726,9 +10098,6 @@
"showResult": "Abriendo una tabla anterior",
"reuse": "Usando la tabla de antes",
"headerActive": "{label}…",
- "headerDoneOne": "Consultó 1 fuente",
- "headerDoneFew": "Consultó {count} fuentes",
- "headerDoneOther": "Consultó {count} fuentes",
"listLabel": "Lo que consultó el Coach",
"toggleShow": "Mostrar lo que consultó el Coach",
"toggleHide": "Ocultar lo que consultó el Coach",
@@ -9754,7 +10123,9 @@
"illness": "Enfermedad y recuperación",
"cycle": "Ciclo",
"correlations": "Relaciones entre métricas",
- "snapshot": "Resumen de datos"
+ "snapshot": "Resumen de datos",
+ "environment": "Tiempo y aire",
+ "day": "Un día"
},
"window": {
"last7days": "últimos 7 días",
@@ -9878,7 +10249,8 @@
"relatedMetric": "Ver también: {metric}",
"continue": "Seguir buscando",
"settingLabel": "Sugerir preguntas de seguimiento",
- "settingHint": "Muestra hasta tres sugerencias bajo la última respuesta del Coach."
+ "settingHint": "Muestra hasta tres sugerencias bajo la última respuesta del Coach.",
+ "changeAssumption": "En su lugar: {value}"
},
"clarify": {
"cardLabel": "El Coach tiene una pregunta",
@@ -9891,6 +10263,12 @@
"last90days": "Últimos 90 días",
"lastYear": "Últimos 12 meses",
"allTime": "Todo el periodo"
+ },
+ "comparison": {
+ "baseline90d": "Comparar con mis 90 días habituales"
+ },
+ "anchor": {
+ "illness": "Desde la enfermedad del {date}"
}
},
"reuse": {
@@ -9936,10 +10314,10 @@
"saved": "Guardado",
"error": "Algo salió mal. Inténtalo de nuevo.",
"instrument": {
- "phq9": "PHQ-9 · ánimo",
- "gad7": "GAD-7 · preocupación",
- "who5": "WHO-5 · bienestar",
- "sci": "SCI · sueño"
+ "phq9": "PHQ-9 (ánimo)",
+ "gad7": "GAD-7 (preocupación)",
+ "who5": "WHO-5 (bienestar)",
+ "sci": "SCI (sueño)"
},
"instrumentSub": {
"phq9": "Cribado de depresión, 9 preguntas",
@@ -10380,12 +10758,34 @@
"daylight": "Luz diurna",
"precip": "Precipitación",
"pressureMean": "Presión atmosférica",
- "pressureDelta": "Variación de presión"
+ "pressureDelta": "Variación de presión",
+ "pm25": "Partículas finas (PM2,5)",
+ "pm10": "Partículas (PM10)",
+ "no2": "Dióxido de nitrógeno",
+ "ozone8h": "Ozono (máximo de 8 horas)",
+ "europeanAqi": "Índice europeo de calidad del aire",
+ "uvIndexMax": "Índice UV (máximo)",
+ "dustMax": "Polvo del Sáhara",
+ "pollenMax": "Polen (valor más alto)"
+ },
+ "chip": {
+ "day": "Tiempo del {date}",
+ "pollenHigh": "Mucho polen: {kinds}",
+ "hotNight": "Noche cálida",
+ "veryPoorAir": "Aire muy malo"
+ },
+ "pollen": {
+ "alder": "aliso",
+ "birch": "abedul",
+ "grass": "gramíneas",
+ "mugwort": "artemisa",
+ "olive": "olivo",
+ "ragweed": "ambrosía"
}
},
"documents": {
"title": "Documentos",
- "subtitle": "Cada carta, informe y escaneo en un solo lugar con búsqueda — cifrado en reposo.",
+ "subtitle": "Cada carta, informe y escaneo en un solo lugar con búsqueda, cifrado en reposo.",
"pageUpload": "Subir",
"ownerOnlyUpload": "Solo el titular del historial puede añadir documentos.",
"kind": {
@@ -10405,11 +10805,13 @@
"searchPlaceholder": "Buscar por título o nombre de archivo…",
"typeGroup": "Tipo de documento",
"conditionGroup": "Afección",
+ "procedureGroup": "Procedimientos y cirugías",
"yearGroup": "Año",
"clear": "Borrar filtros",
"typeAll": "Todos los tipos",
"typeCount": "{count} tipos",
"conditionAll": "Todas las afecciones",
+ "procedureAll": "Todos los procedimientos",
"yearAll": "Todos los años"
},
"card": {
@@ -10425,15 +10827,15 @@
},
"error": {
"fileTooLarge": "Este archivo supera el límite de {maxSize}.",
- "quotaExceeded": "El almacenamiento está lleno — {used} de {quota} usados.",
+ "quotaExceeded": "El almacenamiento está lleno: {used} de {quota} usados.",
"unsupportedType": "Este tipo de archivo no es compatible.",
"purged": "Este documento se eliminó definitivamente y no se puede restaurar.",
"duplicateExists": "Ya hay guardado un documento idéntico.",
- "rateLimited": "Demasiadas subidas — inténtalo de nuevo en unos minutos.",
+ "rateLimited": "Demasiadas subidas. Inténtalo de nuevo en unos minutos.",
"generic": "La subida falló. Inténtalo de nuevo."
},
"toast": {
- "duplicate": "Ya estaba guardado — se resalta el documento existente.",
+ "duplicate": "Ya estaba guardado. Se resalta el documento existente.",
"deleted": "Documento eliminado",
"restoreFailed": "No se pudo restaurar el documento."
},
@@ -10456,7 +10858,7 @@
},
"empty": {
"title": "Tu archivo de documentos está vacío",
- "description": "Guarda cartas médicas, resultados de laboratorio, imágenes, recetas y certificados de vacunación — cifrados y con búsqueda por tipo, afección y año.",
+ "description": "Guarda cartas médicas, resultados de laboratorio, imágenes, recetas y certificados de vacunación. Se guardan cifrados y puedes buscarlos por tipo, afección y año.",
"action": "Subir un primer documento",
"noMatchesTitle": "No hay documentos coincidentes",
"noMatchesDescription": "Nada coincide con la búsqueda y los filtros actuales."
@@ -10487,6 +10889,7 @@
"saveError": "No se pudo guardar el cambio.",
"deleteError": "No se pudo eliminar el documento.",
"download": "Descargar",
+ "openInNewTab": "Abrir en una pestaña nueva",
"delete": "Eliminar",
"titleLabel": "Título",
"titlePlaceholder": "Añadir un título",
@@ -10496,9 +10899,9 @@
"linkCondition": "Vincular",
"noConditions": "Sin vincular a una afección",
"previewUnavailable": "Sin vista previa integrada para este tipo de archivo",
- "uploadedMeta": "Añadido el {date} · {size}",
+ "uploadedMeta": "Añadido el {date}, {size}",
"importedFrom": "Importado de {system}",
- "importedFromWithId": "Importado de {system} · ID {id}",
+ "importedFromWithId": "Importado de {system}, ID {id}",
"sourceOther": "otro sistema",
"share": "Compartir",
"labValuesLink": "Ver {count} valores de laboratorio",
@@ -10598,7 +11001,7 @@
"CONDITION": "Afección",
"MEDICATION_STATEMENT": "Medicamento"
},
- "statedRange": "rango indicado {range}",
+ "statedRange": "Rango indicado {range}",
"needsCheck": "Comprueba este valor contra el documento y luego guárdalo.",
"verify": "Comprobar",
"verifySave": "Guardar valor",
@@ -10714,6 +11117,21 @@
"finishedHint": "Busca de nuevo o cambia un filtro para elegir más documentos.",
"maxReached": "Puedes importar como máximo {max} documentos a la vez.",
"noMatchHint": "Prueba con parte del nombre o quita la etiqueta y las fechas."
+ },
+ "customize": "Personalizar vista",
+ "layout": {
+ "viewDescription": "Tarjetas con miniatura o una lista compacta.",
+ "arrangementHeading": "Disposición",
+ "arrangementDescription": "Cada mes en su propio bloque o todo seguido a lo ancho.",
+ "stacked": "Meses apilados",
+ "flow": "Continuo",
+ "saveFailed": "No se pudo guardar la vista."
+ },
+ "deleteConfirm": {
+ "titleOne": "¿Eliminar este documento?",
+ "titleMany": "¿Eliminar {count} documentos?",
+ "body": "Puedes deshacerlo justo después. A los 30 días se elimina definitivamente.",
+ "action": "Eliminar"
}
},
"selfDescription": {
@@ -10800,7 +11218,7 @@
"genericDescription": "Una medición que registras. Añade valores para seguirla con el tiempo.",
"emptyTitle": "Aún sin valores",
"emptyDescription": "Añade tu primer valor para empezar a representar esta métrica.",
- "valuesTitle": "{metric} — todos los valores",
+ "valuesTitle": "{metric}: todos los valores",
"valuesDescription": "Todos los valores registrados de esta métrica.",
"backToMetric": "Volver a {metric}"
}
@@ -10853,14 +11271,14 @@
}
},
"hydration": {
- "referenceMeta": "Ingesta de referencia {value} {unit}/día · EFSA"
+ "referenceMeta": "Ingesta de referencia {value} {unit}/día (EFSA)"
},
"caffeine": {
- "ceilingMeta": "Límite de seguridad {value} {unit}/día · EFSA"
+ "ceilingMeta": "Límite de seguridad {value} {unit}/día (EFSA)"
},
"micronutrients": {
"title": "Vitaminas y minerales",
- "rowMeta": "{days} de {window} días · ref. {value} {unit} (EFSA)",
+ "rowMeta": "{days} de {window} días, ref. {value} {unit} (EFSA)",
"rowMetaNoRef": "{days} de {window} días",
"footer": "{tracked} de {total} micronutrientes registrados tienen datos en este periodo.",
"emptyTitle": "Aún no hay datos de micronutrientes sincronizados",
@@ -10870,12 +11288,18 @@
},
"daily": {
"line": {
- "score": "Tu puntuación de salud hoy es {score}.",
- "allClear": "Hoy nada necesita tu atención."
+ "allClear": "Hoy nada necesita tu atención.",
+ "scoreReady": "Tu puntuación de salud de hoy está lista."
},
"push": {
"title": "Tu resumen matinal",
- "bodyProvisional": "{line} · El sueño de anoche aún no está disponible."
+ "bodyProvisional": "{line}\nEl sueño de anoche aún no está disponible.",
+ "titleScore": "Puntuación de la mañana {score}",
+ "band": {
+ "green": "Hoy todo parece estable.",
+ "yellow": "Hoy vale la pena revisar algunas cosas.",
+ "red": "Hoy algunas áreas necesitan atención."
+ }
},
"item": {
"doseWindow": {
@@ -10957,7 +11381,31 @@
"sleepPending": "El sueño de anoche aún no ha llegado; la lectura de hoy es provisional.",
"worthALook": "Merece un vistazo",
"allClear": "Hoy nada necesita tu atención.",
- "ringLink": "Abrir detalles de {metric}"
+ "ringLink": "Abrir detalles de {metric}",
+ "heading": "Hoy",
+ "steadyWeeksOne": "Estable desde hace {count} semana",
+ "steadyWeeksFew": "Estable desde hace {count} semanas",
+ "steadyWeeksOther": "Estable desde hace {count} semanas",
+ "valueWithUnit": "{value} {unit}",
+ "rangeWithUnit": "{low} a {high} {unit}",
+ "rangeAtThisTime": "{range} para esta hora del día",
+ "vital": {
+ "RESTING_HEART_RATE": "Frecuencia cardíaca en reposo",
+ "HEART_RATE_VARIABILITY": "Variabilidad de la frecuencia cardíaca",
+ "RESPIRATORY_RATE": "Frecuencia respiratoria",
+ "OXYGEN_SATURATION": "Saturación de oxígeno",
+ "BODY_TEMPERATURE": "Temperatura corporal",
+ "SKIN_TEMPERATURE": "Temperatura de la piel",
+ "BLOOD_PRESSURE_SYS": "Presión arterial sistólica",
+ "BLOOD_PRESSURE_DIA": "Presión arterial diastólica",
+ "BLOOD_GLUCOSE": "Glucosa en sangre",
+ "PULSE": "Pulso",
+ "WEIGHT": "Peso",
+ "generic": "Un signo vital"
+ },
+ "steadyAtLeastWeeksOne": "Estable desde hace al menos {count} semana",
+ "steadyAtLeastWeeksFew": "Estable desde hace al menos {count} semanas",
+ "steadyAtLeastWeeksOther": "Estable desde hace al menos {count} semanas"
},
"milestone": {
"metric": {
@@ -10993,6 +11441,68 @@
"body": "Un mes entero con tu {metric} dentro de tu rango habitual. Maravillosamente estable."
}
}
+ },
+ "lead": {
+ "vitalOutside": {
+ "above": "{metric} está en {value}, por encima de tu rango habitual de {range}.",
+ "below": "{metric} está en {value}, por debajo de tu rango habitual de {range}."
+ },
+ "vitalsOutside": {
+ "aboveOne": "{metric} está en {value}, por encima de tu rango habitual de {range}, y {count} signo vital más también está fuera de su rango.",
+ "aboveFew": "{metric} está en {value}, por encima de tu rango habitual de {range}, y {count} signos vitales más también están fuera de su rango.",
+ "aboveOther": "{metric} está en {value}, por encima de tu rango habitual de {range}, y {count} signos vitales más también están fuera de su rango.",
+ "belowOne": "{metric} está en {value}, por debajo de tu rango habitual de {range}, y {count} signo vital más también está fuera de su rango.",
+ "belowFew": "{metric} está en {value}, por debajo de tu rango habitual de {range}, y {count} signos vitales más también están fuera de su rango.",
+ "belowOther": "{metric} está en {value}, por debajo de tu rango habitual de {range}, y {count} signos vitales más también están fuera de su rango."
+ },
+ "sleep": {
+ "more": "Anoche dormiste {duration}, {diff} más de lo habitual.",
+ "less": "Anoche dormiste {duration}, {diff} menos de lo habitual.",
+ "usual": "Anoche dormiste {duration}, cerca de lo habitual.",
+ "plain": "Anoche dormiste {duration}."
+ },
+ "vitalsInRangeOne": "Tu último signo vital está dentro de su rango habitual.",
+ "vitalsInRangeFew": "Tus {count} últimos signos vitales están dentro de su rango habitual.",
+ "vitalsInRangeOther": "Tus {count} últimos signos vitales están dentro de su rango habitual.",
+ "vitalInRange": "{metric} está en {value}, dentro de tu rango habitual de {range}."
+ },
+ "todayFact": {
+ "restMode": {
+ "label": "Modo descanso",
+ "value": "Día {day}"
+ },
+ "medications": {
+ "label": "Medicamentos",
+ "value": "{taken} de {scheduled} tomados"
+ },
+ "appointment": {
+ "label": "Cita",
+ "today": "Hoy {time}, {what}",
+ "tomorrow": "Mañana {time}, {what}"
+ },
+ "sleep": {
+ "label": "Anoche",
+ "plain": "{duration}",
+ "usual": "{duration}, cerca de lo habitual",
+ "more": "{duration}, {diff} más de lo habitual",
+ "less": "{duration}, {diff} menos de lo habitual"
+ },
+ "vitals": {
+ "label": "Signos vitales",
+ "allInOne": "{count} revisado, en tu rango",
+ "allInFew": "{count} revisados, todos en tu rango",
+ "allInOther": "{count} revisados, todos en tu rango",
+ "oneOut": {
+ "above": "{metric} por encima de tu rango",
+ "below": "{metric} por debajo de tu rango"
+ },
+ "someOut": "{count} de {total} fuera de tu rango"
+ },
+ "cycle": {
+ "label": "Ciclo",
+ "value": "{phase}, día {day}",
+ "dayOnly": "Día {day}"
+ }
}
},
"reportSelection": {
@@ -11177,7 +11687,7 @@
"managed": {
"title": "Perfiles de los que te ocupas",
"description": "Historiales de salud sin inicio de sesión propio.",
- "explainer": "Un perfil administrado es un historial de salud para alguien que no inicia sesión — una criatura, o una persona a la que cuidas. No tiene inicio de sesión ni dirección de correo, y tú pasas a ser su primera persona responsable. Más adelante puedes añadir otras, y cualquiera de vosotras puede entrar en el historial para mantenerlo al día.",
+ "explainer": "Un perfil administrado es un historial de salud para alguien que no inicia sesión, como una criatura o una persona a la que cuidas. No tiene inicio de sesión ni dirección de correo, y tú pasas a ser su primera persona responsable. Más adelante puedes añadir otras, y cualquiera de vosotras puede entrar en el historial para mantenerlo al día.",
"nameLabel": "Nombre",
"namePlaceholder": "El nombre con el que reconocerás este historial",
"nameHint": "Hasta 80 caracteres. Se muestra allá donde aparezca este historial.",
@@ -11199,7 +11709,7 @@
"creating": "Creando…",
"created": "{name} ya está listo. Puedes entrar en ese historial desde el menú de la cuenta.",
"errorStepUp": "Confirma tu segundo factor y vuelve a intentarlo.",
- "errorOffline": "Esto no llegó al servidor. No ha cambiado nada — revisa tu conexión y vuelve a intentarlo.",
+ "errorOffline": "Esto no llegó al servidor. No ha cambiado nada. Revisa tu conexión y vuelve a intentarlo.",
"errorInvalid": "El servidor rechazó esos datos. Revisa el nombre, la fecha y la zona horaria.",
"errorFailed": "No se pudo crear el perfil.",
"edit": "Editar",
@@ -11210,7 +11720,7 @@
"editErrorFailed": "No se han podido guardar los cambios.",
"delete": "Eliminar perfil",
"deleteTitle": "¿Eliminar {name}?",
- "deleteBody": "Todo lo registrado para {name} desaparece con el perfil — mediciones, medicamentos, documentos y notas. Esto no se puede deshacer.",
+ "deleteBody": "Todo lo registrado para {name} desaparece con el perfil: mediciones, medicamentos, documentos y notas. Esto no se puede deshacer.",
"deleteGuardiansOne": "{count} persona se ocupa hoy de este historial y lo perderá.",
"deleteGuardiansFew": "{count} personas se ocupan hoy de este historial y lo perderán.",
"deleteGuardiansOther": "{count} personas se ocupan hoy de este historial y lo perderán.",
@@ -11239,7 +11749,47 @@
"guardianErrorFailed": "No se pudo enviar la invitación.",
"errorMfaRequired": "Esto necesita un segundo factor y tu cuenta aún no tiene ninguno. Configura uno en Ajustes → Seguridad y vuelve a intentarlo.",
"rosterLoadError": "No se pudo cargar quién se ocupa de este historial.",
- "errorRateLimit": "Demasiados perfiles creados en la última hora. Espera un poco y vuelve a intentarlo."
+ "errorRateLimit": "Demasiados perfiles creados en la última hora. Espera un poco y vuelve a intentarlo.",
+ "handover": {
+ "action": "Entregar",
+ "title": "Entregar a {name}",
+ "description": "Da a {name} un acceso propio. No se copia ni se mueve nada: el historial pasa a ser su cuenta.",
+ "guardiansLabel": "Acceso después de la entrega",
+ "guardiansHint": "Tu propuesta. {name} la ve y decide después de iniciar sesión.",
+ "access": {
+ "end": "Sin acceso",
+ "read": "Solo ver",
+ "manage": "Gestionar"
+ },
+ "expiryLabel": "Enlace válido durante",
+ "expiry1": "1 día",
+ "expiry7": "7 días",
+ "expiry14": "14 días",
+ "minorHint": "{name} tiene menos de 16 años. Vale la pena conservar el acceso de lectura y hablar el paso juntos.",
+ "irreversible": "En cuanto {name} use el enlace, la entrega no se puede deshacer. El historial será suyo y decidirá quién conserva el acceso.",
+ "afterwards": "{name} confirma personalmente el aviso y cualquier consentimiento de IA. Los recordatorios de este historial dejarán de llegarte.",
+ "replaceNote": "Crear un enlace nuevo retira el que está abierto.",
+ "create": "Crear enlace",
+ "creating": "Creando…",
+ "linkOnce": "Este enlace solo se muestra ahora. Dáselo a {name} en persona o por un canal de confianza, o deja que escanee el código.",
+ "qrAlt": "Código QR del enlace de entrega para {name}",
+ "urlLabel": "Enlace de entrega",
+ "copy": "Copiar",
+ "copied": "Enlace copiado",
+ "copyError": "No se pudo copiar el enlace. Selecciónalo y cópialo a mano.",
+ "linkExpires": "Válido hasta el {date}.",
+ "done": "Listo",
+ "pending": "Hay un enlace de entrega abierto hasta el {date}.",
+ "replace": "Nuevo enlace",
+ "revoke": "Retirar enlace",
+ "revoked": "Enlace retirado",
+ "revokeFailed": "No se pudo retirar el enlace. Vuelve a intentarlo.",
+ "errorFailed": "No se pudo crear el enlace de entrega.",
+ "errorRosterChanged": "Mientras tanto ha cambiado quién se ocupa del historial. Cierra esto y vuelve a abrirlo.",
+ "oidcOnly": "No se puede entregar un perfil mientras este servidor solo permita el inicio de sesión único.",
+ "loadError": "No se pudo cargar el estado de la entrega.",
+ "entryHint": "Cuando {name} quiera llevar este historial por su cuenta."
+ }
},
"toast": {
"savedTo": "Guardado en el historial de {name}"
@@ -11358,7 +11908,13 @@
"symptomEventDelete": "{name} eliminó una entrada de síntoma",
"symptomDefinitionCreate": "{name} añadió un síntoma",
"symptomDefinitionUpdate": "{name} cambió un síntoma",
- "symptomDefinitionDelete": "{name} quitó un síntoma"
+ "symptomDefinitionDelete": "{name} quitó un síntoma",
+ "customVaccineCreate": "{name} añadió una vacuna propia",
+ "customVaccineUpdate": "{name} editó una vacuna propia",
+ "customVaccineDelete": "{name} quitó una vacuna propia",
+ "lifeEventCreate": "{name} añadió un acontecimiento vital",
+ "lifeEventUpdate": "{name} cambió un acontecimiento vital",
+ "lifeEventDelete": "{name} eliminó un acontecimiento vital"
},
"section": {
"measurements": {
@@ -11408,12 +11964,28 @@
"notFound": "Ese acceso ya no existe.",
"revokeFailed": "No se pudo terminar el acceso.",
"renounceFailed": "No se pudo devolver el acceso.",
- "profileNotFound": "Ese perfil ya no está — puede que se haya eliminado, o que tu acceso haya terminado.",
+ "profileNotFound": "Ese perfil ya no está. Puede que se haya eliminado, o que tu acceso haya terminado.",
"deleteProfileFailed": "No se pudo eliminar el perfil.",
"removeGuardianFailed": "No se pudo terminar ese acceso."
},
"anamnesis": {
"relativeNote": "Una entrada de antecedentes familiares describe a los familiares de esta persona. Añádela solo si querría que quedara registrada aquí."
+ },
+ "handoverDecision": {
+ "title": "Quién conserva el acceso a tu historial",
+ "description": "Decide qué pueden hacer a partir de ahora las personas que llevaron tu historial.",
+ "proposed": "Propuesto: {access}",
+ "settled": "Ya cambiado en Acceso compartido",
+ "empty": "Nadie más tenía acceso a tu historial.",
+ "later": "Puedes cambiarlo cuando quieras en Ajustes, Acceso compartido.",
+ "save": "Confirmar",
+ "saving": "Guardando…",
+ "decideLater": "Más tarde",
+ "errorFailed": "No se pudo guardar tu decisión. Vuelve a intentarlo.",
+ "loadError": "No se pudo cargar quién llevaba tu historial.",
+ "notificationsTitle": "Configura tus propios recordatorios",
+ "notificationsBody": "Hasta ahora, los recordatorios de este historial llegaban a las personas que lo llevaban. A partir de ahora te llegan a ti, en cuanto añadas un canal de notificaciones.",
+ "notificationsAction": "Abrir notificaciones"
}
},
"encounters": {
@@ -11434,7 +12006,7 @@
"both": "Ambos lados",
"none": "Sin indicar"
},
- "bodySiteWithSide": "{site} · {side}",
+ "bodySiteWithSide": "{site} ({side})",
"sectionTitle": "Citas",
"sectionDescription": "Visitas médicas y citas reservadas en un solo sitio.",
"add": "Cita",
@@ -11526,7 +12098,6 @@
"title": "Consultas y médicos",
"description": "Tu propia agenda: escribe una consulta una vez y elígela en todas partes.",
"linkLabel": "Agenda",
- "add": "Entrada",
"searchPlaceholder": "Buscar nombre, consulta o especialidad",
"loadError": "No se pudo cargar la agenda.",
"emptyTitle": "Todavía no hay entradas",
@@ -11623,7 +12194,6 @@
"unreadableLabel": "Síntoma ilegible",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Registrar síntoma",
"sheetDescription": "Uno de tus propios síntomas, su intensidad y cuándo.",
"symptom": "Síntoma",
"symptomRequired": "Elige un síntoma.",
@@ -11672,5 +12242,426 @@
"title": "Síntomas registrados",
"filed": "registrado con esta enfermedad"
}
+ },
+ "day": {
+ "scoreOfMax": "de {max}",
+ "cycleDay": "Día {n} del ciclo",
+ "cyclePhaseLine": {
+ "MENSTRUAL": "Día {n} del ciclo, fase menstrual",
+ "FOLLICULAR": "Día {n} del ciclo, fase folicular",
+ "OVULATORY": "Día {n} del ciclo, fase ovulatoria",
+ "LUTEAL": "Día {n} del ciclo, fase lútea"
+ },
+ "previousDay": "Día anterior",
+ "nextDay": "Día siguiente",
+ "close": "Cerrar día",
+ "hidePanel": "Ocultar día",
+ "showPanel": "Mostrar {date}",
+ "openDay": "Abrir día",
+ "viewWholeDay": "Ver el día completo",
+ "captureForDay": "Registrar para este día",
+ "empty": "No hay entradas para este día.",
+ "notShared": "Algunas áreas no están compartidas contigo.",
+ "inTimeline": "En la cronología",
+ "askCoach": "Preguntar al Coach por este día",
+ "loadFailed": "No se pudo cargar este día.",
+ "dayOf": "Día {n} de {m}",
+ "dayN": "Día {n}",
+ "usualRange": "Habitual de {lo} a {hi}",
+ "groups": {
+ "scores": "Puntuaciones",
+ "running": "En curso ese día",
+ "values": "Valores",
+ "happened": "Qué pasó"
+ },
+ "notable": {
+ "extremeHigh": "Valor diario más alto desde {since}",
+ "extremeLow": "Valor diario más bajo desde {since}",
+ "firstValue": "Primera medición de este tipo",
+ "gap": "{days} días sin medición"
+ },
+ "previousDayShortcut": "Día anterior (Alt + flecha izquierda)",
+ "nextDayShortcut": "Día siguiente (Alt + flecha derecha)",
+ "captureShort": "Registrar",
+ "inTimelineShort": "Cronología",
+ "askCoachPrompt": "¿Qué pasó el {date}?",
+ "coachChips": "Días en esta respuesta",
+ "description": "Todo lo que tus registros contienen para este día.",
+ "openDocument": "Abrir documento",
+ "previousReading": "Antes, {date}",
+ "sheetExpand": "Ampliar a toda la altura",
+ "sheetShrink": "Reducir",
+ "valuesLegend": "Punto para este día, barra para los 30 días anteriores",
+ "valuesAllOne": "Todos los valores",
+ "valuesAllFew": "Los {count} valores",
+ "valuesAllOther": "Los {count} valores",
+ "valuesFewer": "Menos valores",
+ "countEntriesOne": "1 entrada",
+ "countEntriesFew": "{count} entradas",
+ "countEntriesOther": "{count} entradas",
+ "meanOfReadingsOne": "Media de 1 medición",
+ "meanOfReadingsFew": "Media de {count} mediciones",
+ "meanOfReadingsOther": "Media de {count} mediciones",
+ "sinceLastVisit": {
+ "title": "Desde la última visita el {date}",
+ "countOne": "1 entrada",
+ "countFew": "{count} entradas",
+ "countOther": "{count} entradas",
+ "allOne": "Mostrar todo",
+ "allFew": "Mostrar las {count}",
+ "allOther": "Mostrar las {count}",
+ "fewer": "Mostrar menos",
+ "note": "Reunido a partir de tus entradas, sin valoración. Cada fecha abre el día completo.",
+ "report": "Informe médico del {date} hasta hoy"
+ },
+ "travel": "De viaje",
+ "event": {
+ "started": "Inicio: {label}",
+ "ended": "Fin: {label}",
+ "paused": "Pausa: {label}",
+ "resumed": "Reanudación: {label}",
+ "illnessBegan": "Inicio: {label}",
+ "illnessResolved": "Recuperación: {label}"
+ },
+ "pickDate": "Elegir una fecha",
+ "pickerTitle": "Ir a un día",
+ "today": "Hoy",
+ "hasEntries": "con entradas",
+ "dayNSince": "Día {n}, desde el {date}",
+ "sinceDate": "Desde el {date}"
+ },
+ "timeline": {
+ "title": "Cronología",
+ "subtitle": "Qué pasó y cuándo, junto a tus valores.",
+ "standing": "Permanente",
+ "startUnknown": "inicio desconocido",
+ "zoom": {
+ "all": "Todo",
+ "year": "12 meses",
+ "quarter": "3 meses",
+ "range": "Periodo"
+ },
+ "lanes": {
+ "life": "Vida",
+ "illness": "Enfermedades",
+ "allergies": "Alergias",
+ "medications": "Medicamentos",
+ "vaccinations": "Vacunas",
+ "visits": "Médico e intervenciones",
+ "labs": "Laboratorio",
+ "documents": "Documentos",
+ "cycle": "Ciclo"
+ },
+ "readiness": {
+ "title": "Lo que tu cronología ya muestra",
+ "subtitle": "Un inventario honesto antes de abrirla.",
+ "menuEntry": "Cobertura de datos",
+ "hint": "Las pistas vacías permanecen ocultas hasta que haya algo. Los módulos desactivados nunca aparecen. Encontrarás este resumen más adelante en Cobertura de datos, dentro de la cronología.",
+ "later": "Más tarde",
+ "open": "Abrir cronología",
+ "verdict": {
+ "carries": "Ya tiene contenido.",
+ "thin": "Aún escasa. Por ahora la cronología muestra sobre todo tus valores."
+ },
+ "status": {
+ "carries": "completa",
+ "thin": "escasa",
+ "empty": "vacía"
+ },
+ "lanes": {
+ "values": "Valores",
+ "mood": "Estado de ánimo",
+ "environment": "Entorno",
+ "life": "Acontecimientos vitales"
+ },
+ "meter": "{carrying} de {total} pistas tienen contenido",
+ "carriesDetail": "{carrying} de {total} pistas dan una visión general desde {since}.",
+ "loadFailed": "No se pudo cargar el inventario.",
+ "card": {
+ "title": "Lo que la cronología ya muestra",
+ "summary": "Llega hasta {since}; {carrying} de {total} pistas ya tienen contenido.",
+ "dismiss": "Ocultar"
+ },
+ "detail": {
+ "valuesOne": "{count} medida desde {since}",
+ "valuesFew": "{count} medidas desde {since}",
+ "valuesOther": "{count} medidas desde {since}",
+ "illnessOne": "{count} episodio; crónicos: {chronic}",
+ "illnessFew": "{count} episodios; crónicos: {chronic}",
+ "illnessOther": "{count} episodios; crónicos: {chronic}",
+ "allergiesOne": "{count} alergia",
+ "allergiesFew": "{count} alergias",
+ "allergiesOther": "{count} alergias",
+ "medicationsOne": "{count} medicamento; sin fecha de inicio: {missingStart}",
+ "medicationsFew": "{count} medicamentos; sin fecha de inicio: {missingStart}",
+ "medicationsOther": "{count} medicamentos; sin fecha de inicio: {missingStart}",
+ "medicationsDatedOne": "{count} medicamento con fecha de inicio",
+ "medicationsDatedFew": "{count} medicamentos con fecha de inicio",
+ "medicationsDatedOther": "{count} medicamentos con fecha de inicio",
+ "vaccinationsOne": "{count} vacuna",
+ "vaccinationsFew": "{count} vacunas",
+ "vaccinationsOther": "{count} vacunas",
+ "visitsOne": "{count} visita, {procedures} con intervención",
+ "visitsFew": "{count} visitas, {procedures} con intervención",
+ "visitsOther": "{count} visitas, {procedures} con intervención",
+ "visitsPlannedOne": "{count} visita, solo planificada",
+ "visitsPlannedFew": "{count} visitas, solo planificadas",
+ "visitsPlannedOther": "{count} visitas, solo planificadas",
+ "labsOne": "{count} día con resultados",
+ "labsFew": "{count} días con resultados",
+ "labsOther": "{count} días con resultados",
+ "documentsOne": "{count} documento con fecha",
+ "documentsFew": "{count} documentos con fecha",
+ "documentsOther": "{count} documentos con fecha",
+ "lifeOne": "{count} acontecimiento vital",
+ "lifeFew": "{count} acontecimientos vitales",
+ "lifeOther": "{count} acontecimientos vitales",
+ "cycleOne": "{count} ciclo",
+ "cycleFew": "{count} ciclos",
+ "cycleOther": "{count} ciclos",
+ "moodOne": "{count} entrada desde {since}",
+ "moodFew": "{count} entradas desde {since}",
+ "moodOther": "{count} entradas desde {since}",
+ "environmentOne": "{count} día con datos ambientales",
+ "environmentFew": "{count} días con datos ambientales",
+ "environmentOther": "{count} días con datos ambientales",
+ "empty": "Aún ninguno"
+ },
+ "gap": {
+ "valuesEmpty": "Conectar fuentes",
+ "illnessEmpty": "Añadir episodio",
+ "medicationsWithoutStart": "Completar",
+ "visitsEmpty": "Añadir visita",
+ "documentsUndated": "Revisar",
+ "labsEmpty": "Añadir resultado",
+ "vaccinationsEmpty": "Añadir vacunas",
+ "lifeEventsEmpty": "Añadir"
+ },
+ "gapLine": {
+ "medicationsWithoutStartOne": "{count} medicamento sin fecha de inicio",
+ "medicationsWithoutStartFew": "{count} medicamentos sin fecha de inicio",
+ "medicationsWithoutStartOther": "{count} medicamentos sin fecha de inicio",
+ "documentsUndatedOne": "{count} documento sin fecha",
+ "documentsUndatedFew": "{count} documentos sin fecha",
+ "documentsUndatedOther": "{count} documentos sin fecha",
+ "valuesEmpty": "Aún no hay valores durante un periodo largo",
+ "illnessEmpty": "Aún no hay enfermedades con fecha de inicio",
+ "visitsEmpty": "Aún no hay visitas médicas realizadas",
+ "labsEmpty": "Aún no hay resultados de laboratorio",
+ "vaccinationsEmpty": "Aún no hay vacunas",
+ "lifeEventsEmpty": "Aún no hay acontecimientos vitales"
+ }
+ },
+ "legendSpan": "Periodo",
+ "legendEvent": "Evento",
+ "legendMeanQuarter": "Media trimestral",
+ "legendMeanMonth": "Media mensual",
+ "legendMeanWeek": "Media semanal",
+ "legendThin": "Menos de 3 mediciones",
+ "legendGap": "Hueco corto sin mediciones",
+ "today": "Hoy",
+ "noDataYet": "Aún sin datos",
+ "layers": "Capas",
+ "layersLanes": "Pistas",
+ "zoomLabel": "Rango de la vista",
+ "chartLabel": "Cronología de {from} a {to}",
+ "keyboardHint": "Las flechas mueven la selección, Inicio y Fin saltan a los extremos, Intro abre el día.",
+ "loadFailed": "No se pudo cargar la cronología.",
+ "empty": {
+ "title": "Aún no hay nada en la cronología",
+ "description": "Las entradas con fecha aparecerán aquí en cuanto haya alguna."
+ },
+ "values": {
+ "menuTitle": "Valores bajo los carriles",
+ "trigger": "Valores ({count})",
+ "noneInWindow": "Sin valores en este periodo",
+ "series": {
+ "BLOOD_PRESSURE_SYS": "Presión sistólica",
+ "BLOOD_PRESSURE_DIA": "Presión diastólica"
+ },
+ "noValue": "sin valor",
+ "bloodPressure": "Presión arterial",
+ "readingsOne": "1 medición",
+ "readingsFew": "media de {count} mediciones",
+ "readingsOther": "media de {count} mediciones"
+ },
+ "table": {
+ "caption": "Entradas de la cronología",
+ "lane": "Pista",
+ "entry": "Entrada",
+ "start": "Inicio",
+ "end": "Fin",
+ "seriesCaption": "Medias por periodo",
+ "period": "Periodo"
+ },
+ "selection": {
+ "empty": "No hay entradas en este periodo",
+ "since": "desde {date}",
+ "range": "{from} a {to}",
+ "mean": "Ø {values}",
+ "week": "Semana del {date}",
+ "hintClick": "Haz clic en un momento para verlo aquí",
+ "hintTap": "Toca un momento para verlo aquí"
+ },
+ "chronicle": {
+ "months": "Meses",
+ "years": "Años",
+ "groupingLabel": "Agrupación",
+ "empty": "Sin entradas de {from} a {to}",
+ "emptySingle": "Sin entradas en {period}",
+ "ended": "Fin: {label}",
+ "afterDaysOne": "después de 1 día",
+ "afterDaysFew": "después de {count} días",
+ "afterDaysOther": "después de {count} días",
+ "notable": {
+ "extremeHigh": "El valor más alto en mucho tiempo",
+ "extremeLow": "El valor más bajo en mucho tiempo",
+ "firstValue": "Primer valor de una medida",
+ "gap": "Un hueco largo en los valores"
+ },
+ "notableMeta": "Abre el día para ver detalles"
+ },
+ "standingAllergiesOne": "{count} alergia",
+ "standingAllergiesFew": "{count} alergias",
+ "standingAllergiesOther": "{count} alergias",
+ "legendMeanDay": "Media diaria",
+ "range": {
+ "from": "Desde",
+ "to": "Hasta",
+ "label": "Periodo elegido",
+ "open": "Elegir un periodo",
+ "clear": "Mostrar todo",
+ "apply": "Aplicar"
+ },
+ "item": {
+ "labValuesOne": "1 valor de laboratorio",
+ "labValuesFew": "{count} valores de laboratorio",
+ "labValuesOther": "{count} valores de laboratorio"
+ }
+ },
+ "lifeEvents": {
+ "title": "Acontecimiento vital",
+ "add": "Acontecimiento vital",
+ "privacy": "El título y la nota se guardan cifrados. No se envían a ninguna IA.",
+ "emptyHint": "Una mudanza, un nacimiento o un nuevo trabajo dan un ancla a los años.",
+ "save": "Guardar",
+ "cancel": "Cancelar",
+ "delete": "Eliminar acontecimiento vital",
+ "saved": "Acontecimiento vital guardado",
+ "deleted": "Acontecimiento vital eliminado",
+ "fields": {
+ "title": "¿Qué pasó?",
+ "category": "Tipo",
+ "when": "Cuándo",
+ "precision": "Precisión",
+ "addEnd": "Fecha de fin, si fue un periodo",
+ "endDate": "Hasta",
+ "note": "Nota",
+ "optionalTag": "opcional",
+ "notePlaceholder": "Algo que quieras saber más adelante",
+ "removeEnd": "Quitar fecha de fin"
+ },
+ "category": {
+ "FAMILY": "Familia",
+ "HOME": "Vivienda",
+ "WORK": "Trabajo",
+ "LOSS": "Pérdida",
+ "OTHER": "Otro"
+ },
+ "precision": {
+ "DAY": "Día",
+ "MONTH": "Mes",
+ "YEAR": "Año"
+ },
+ "edit": "Editar acontecimiento vital",
+ "errors": {
+ "titleRequired": "Ponle un nombre al acontecimiento.",
+ "titleTooLong": "Máximo 120 caracteres.",
+ "categoryRequired": "Elige un tipo.",
+ "dateRequired": "Introduce una fecha válida.",
+ "endBeforeStart": "El final es anterior al inicio.",
+ "noteTooLong": "Máximo 2000 caracteres."
+ },
+ "saveFailed": "No se pudo guardar.",
+ "deleteFailed": "No se pudo eliminar.",
+ "deleteConfirm": {
+ "title": "¿Eliminar este acontecimiento vital?",
+ "body": "Su título y su nota desaparecerán."
+ }
+ },
+ "palette": {
+ "title": "Buscar",
+ "description": "Encuentra una página, un ajuste, un módulo, una acción o una entrada de tus registros.",
+ "placeholder": "Buscar páginas, ajustes, tus registros…",
+ "results": "Resultados",
+ "noResults": "No hay resultados para «{query}».",
+ "moduleOn": "Activado",
+ "moduleOff": "Desactivado",
+ "hintMove": "Mover",
+ "hintOpen": "Abrir",
+ "hintClose": "Cerrar",
+ "groups": {
+ "recent": "Recientes",
+ "pages": "Páginas",
+ "insights": "Insights",
+ "settings": "Ajustes",
+ "modules": "Módulos",
+ "actions": "Acciones",
+ "records": "En tus registros"
+ },
+ "actions": {
+ "capture": "Nueva entrada",
+ "coach": "Preguntar al Coach",
+ "backup": "Crear una copia de seguridad",
+ "today": "Abrir hoy",
+ "shortcuts": "Mostrar atajos de teclado"
+ },
+ "records": {
+ "loading": "Buscando en tus registros…",
+ "empty": "Nada en tus registros para «{query}».",
+ "error": "No se pudo buscar en tus registros. Inténtalo de nuevo en un momento.",
+ "truncated": "Solo se buscaron las entradas más recientes.",
+ "kind": {
+ "visit": "Consulta",
+ "condition": "Enfermedad",
+ "document": "Documento",
+ "vaccination": "Vacuna",
+ "lab": "Resultado de laboratorio",
+ "medication": "Medicamento",
+ "symptom": "Síntoma",
+ "workout": "Entrenamiento",
+ "note": "Nota"
+ },
+ "source": {
+ "mood": "Nota de ánimo",
+ "measurement": "Nota de una medición",
+ "illness": "Diario de enfermedad"
+ }
+ }
+ },
+ "shortcuts": {
+ "title": "Atajos de teclado",
+ "description": "Teclas que funcionan en toda la aplicación, salvo mientras escribes en un campo.",
+ "menuEntry": "Atajos de teclado",
+ "then": "luego",
+ "search": "Buscar",
+ "help": "Mostrar esta lista",
+ "close": "Cerrar un diálogo o panel",
+ "previousDay": "Día anterior",
+ "nextDay": "Día siguiente",
+ "toggleDay": "Abrir o cerrar el día",
+ "dayHint": "Día anterior y siguiente funcionan mientras hay un día abierto.",
+ "groups": {
+ "navigation": "Navegación",
+ "actions": "Acciones",
+ "day": "Día"
+ }
+ },
+ "aboutPage": {
+ "eyebrow": "Información",
+ "title": "Acerca de HealthLog",
+ "lastUpdated": "Última actualización: {date}",
+ "project": "Proyecto",
+ "credits": "Créditos"
}
}
diff --git a/messages/fr.json b/messages/fr.json
index a5d0afa08..5181f03ba 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -49,7 +49,9 @@
"dismiss": "Ignorer",
"moreInfo": "Plus d'informations",
"durationHoursMinutes": "{hours} h {minutes} min",
- "durationMinutes": "{minutes} min"
+ "durationMinutes": "{minutes} min",
+ "durationClockMinutes": "{time} min",
+ "durationClockHours": "{time} h"
},
"apiErrors": {
"measurement": {
@@ -165,7 +167,7 @@
"anamnesisShiftSchedule": "Organisation du travail",
"anamnesisColFact": "Élément du profil",
"anamnesisColValue": "Valeur enregistrée",
- "subtitle": "HealthLog — Rapport personnel de santé",
+ "subtitle": "HealthLog : rapport personnel de santé",
"patient": "Patient",
"dateOfBirth": "Date de naissance",
"gender": "Sexe",
@@ -175,18 +177,18 @@
"height": "Taille",
"period": "Période du rapport",
"createdOn": "Créé le",
- "vitalsTitle": "Signes vitaux — Résumé",
+ "vitalsTitle": "Signes vitaux : résumé",
"colParameter": "Paramètre",
"colCurrent": "Actuel",
"colAvgPeriod": "Moy. période",
"colMin": "Min",
"colMax": "Max",
"colN": "n",
- "bpClassificationTitle": "Tension artérielle — Classification ESH (2023)",
+ "bpClassificationTitle": "Tension artérielle : classification ESH (2023)",
"avgBp": "Tension artérielle moyenne",
"classification": "Classification",
"bmiTitle": "Indice de masse corporelle (IMC)",
- "bmiRow": "IMC:{bmi} kg/m² — Classification: {class} (WHO)",
+ "bmiRow": "IMC:{bmi} kg/m², classification: {class} (WHO)",
"complianceTitle": "Observance des médicaments",
"glp1Title": "Traitement GLP-1",
"glp1WeightSummary": "Poids sur la période du rapport : {start} kg → {end} kg ({delta} kg)",
@@ -204,7 +206,7 @@
"colExpected": "Attendues",
"colComplianceRate": "Taux d'observance",
"moodTitle": "Évolution de l’humeur",
- "moodSummary": "Moyenne:{avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+ "moodSummary": "Moyenne {avg} / 5.0, {count} entrées, plage {min}–{max}",
"colMood": "Humeur",
"colCount": "Nombre",
"colShare": "Part",
@@ -234,8 +236,8 @@
"typeGlucoseRandom": "Glycémie (indifférente)",
"typeGlucoseBedtime": "Glycémie (avant le coucher)",
"typeGlucoseUnspecified": "Glycémie (non précisée)",
- "glucoseClassificationTitle": "Glycémie — Plages de référence (ADA 2024 / DDG)",
- "glucoseRow": "{label} : moy. {avg} {unit} (cible {rangeMin}–{rangeMax}) — {class}",
+ "glucoseClassificationTitle": "Glycémie : plages de référence (ADA 2024 / DDG)",
+ "glucoseRow": "{label} : moy. {avg} {unit} (cible {rangeMin}–{rangeMax}), {class}",
"glucoseInTarget": "dans la cible",
"glucoseBelowTarget": "sous la cible",
"glucoseAboveTarget": "au-dessus de la cible",
@@ -287,7 +289,7 @@
"gmi": "Indicateur de gestion du glucose (GMI)",
"eA1c": "HbA1c estimée",
"cv": "Variabilité (CV%)",
- "spotCaveat": "Ce sont des estimations issues de mesures ponctuelles, et non un profil de capteur continu — à lire comme une tendance, pas comme un verdict clinique."
+ "spotCaveat": "Ce sont des estimations issues de mesures ponctuelles, et non un profil de capteur continu. À lire comme une tendance, pas comme un verdict clinique."
},
"illnessTitle": "Affections et maladies",
"illnessOngoing": "En cours",
@@ -376,12 +378,12 @@
"backToDashboard": "Retour au tableau de bord"
},
"offlineBanner": {
- "message": "Pas de connexion — les modifications ne peuvent pas être enregistrées tant que tu n'es pas de nouveau en ligne.",
- "saveFailed": "Enregistrement impossible — tu es hors ligne. Réessaie une fois de nouveau en ligne.",
+ "message": "Pas de connexion : les modifications ne peuvent pas être enregistrées tant que tu n'es pas de nouveau en ligne.",
+ "saveFailed": "Enregistrement impossible : tu es hors ligne. Réessaie une fois de nouveau en ligne.",
"showingCached": "Affichage de tes dernières données synchronisées."
},
"demoBanner": {
- "message": "Mode démo — les modifications ne sont pas enregistrées."
+ "message": "Mode démo : les modifications ne sont pas enregistrées."
},
"nav": {
"dashboard": "Tableau de bord",
@@ -407,6 +409,7 @@
"mood": "Humeur",
"coach": "Coach",
"skipToContent": "Aller au contenu",
+ "breadcrumb": "Vous êtes ici",
"sidebar": "Barre latérale",
"mainNavigation": "Navigation principale",
"mobileNavigation": "Navigation mobile",
@@ -426,7 +429,9 @@
"workout": "Entraînement",
"workoutDescription": "Une séance que vos appareils n’ont pas enregistrée",
"symptom": "Symptôme",
- "symptomDescription": "L'un de tes propres symptômes, avec son intensité"
+ "symptomDescription": "L'un de tes propres symptômes, avec son intensité",
+ "lifeEventDescription": "Un déménagement, une naissance, un nouvel emploi",
+ "lifeEvent": "Événement de vie"
},
"cycle": "Cycle",
"labs": "Analyses",
@@ -435,7 +440,8 @@
"illness": "Maladie",
"documents": "Documents",
"vaccinations": "Vaccinations",
- "mentalWellbeing": "Bien-être mental"
+ "mentalWellbeing": "Bien-être mental",
+ "timeline": "Chronologie"
},
"auth": {
"login": "Se connecter",
@@ -452,14 +458,13 @@
"hasAccount": "Déjà un compte ?",
"loginFailed": "Échec de la connexion",
"registerFailed": "Échec de l’inscription",
- "invitedBanner": "Vous avez été invité — créez votre compte même si les inscriptions sont fermées.",
+ "invitedBanner": "Vous avez été invité. Créez votre compte même si les inscriptions sont fermées.",
"alreadySignedIn": {
"title": "Tu es déjà connecté",
"body": "Tu es connecté en tant que {username}. Une invitation crée un nouveau compte. Déconnecte-toi pour l'accepter.",
"signOut": "Se déconnecter et accepter l'invitation"
},
"createAccount": "Créer un compte",
- "newAccount": "Créer un nouveau compte",
"backToPasskey": "Revenir à la passkey",
"passkeySetupLater": "Tu peux configurer les passkeys plus tard dans les paramètres.",
"passwordMinLength": "Min. 12 caractères",
@@ -512,6 +517,37 @@
"fixLan": "Serveur sur un réseau domestique sans TLS : mets SESSION_COOKIE_SECURE=false dans ses paramètres et redémarre-le.",
"fixHttps": "Serveur derrière un proxy HTTPS : ouvre plutôt l'adresse https:// et vérifie que le proxy transmet X-Forwarded-Proto: https.",
"localhostNote": "Cette page est sur http://localhost alors que le serveur ne pose son cookie de connexion qu'en https://. La plupart des navigateurs l'acceptent sur localhost ; si tu reviens ici après la connexion, mets SESSION_COOKIE_SECURE=false."
+ },
+ "claim": {
+ "title": "Reprenez votre dossier de santé",
+ "titleNamed": "Bienvenue, {name}",
+ "description": "Jusqu'ici, votre dossier de santé a été tenu pour vous. Choisissez comment vous connecter et il deviendra votre propre compte. Rien n'est copié ni déplacé.",
+ "loading": "Vérification de votre lien…",
+ "invalid": "Ce lien ne fonctionne plus",
+ "invalidHint": "Il est inconnu, expiré, retiré ou déjà utilisé. Demandez-en un nouveau à la personne qui vous l'a envoyé.",
+ "oidcOnly": "Ce serveur n'autorise que l'authentification unique, un profil ne peut donc pas encore être repris ici.",
+ "signedInTitle": "Déconnectez-vous d'abord",
+ "signedInBody": "Vous êtes connecté en tant que {username}. Ce lien crée une connexion pour quelqu'un d'autre, déconnectez-vous donc avant de l'utiliser.",
+ "signOut": "Se déconnecter et continuer",
+ "guardiansTitle": "Qui garde l'accès pour l'instant",
+ "guardiansHint": "Proposé par les personnes qui ont tenu votre dossier. Vous décidez juste après la connexion.",
+ "guardianUnnamed": "Un tuteur",
+ "usernameHint": "3 à 30 lettres, chiffres, _ ou -.",
+ "usernameReserved": "Les noms d'utilisateur commençant par managed- sont réservés.",
+ "emailHint": "Obligatoire, pour pouvoir revenir si vous oubliez votre mot de passe.",
+ "consentNote": "Vous confirmez vous-même l'avertissement et toute utilisation de l'IA. D'ici là, rien de votre dossier n'est envoyé à un fournisseur d'IA.",
+ "expires": "Ce lien est valable jusqu'au {date}.",
+ "submit": "Reprendre",
+ "submitting": "Reprise en cours…",
+ "errorTaken": "Ce nom d'utilisateur ou cette adresse e-mail est déjà utilisé.",
+ "errorRateLimit": "Trop de tentatives. Attendez quelques minutes puis réessayez.",
+ "errorFailed": "Cela n'a pas fonctionné. Réessayez dans un instant.",
+ "errorOffline": "Le serveur n'a pas été atteint. Vérifiez votre connexion et réessayez."
+ },
+ "inviteInvalid": {
+ "title": "Invitation invalide",
+ "description": "Ce lien d’invitation est incomplet ou a été mal copié. Demandez un nouveau lien à la personne qui vous a invité.",
+ "toLogin": "Aller à la connexion"
}
},
"dashboard": {
@@ -543,17 +579,17 @@
"sleepShort": "Sommeil",
"stepsShort": "Pas",
"vo2MaxShort": "VO₂ max",
- "bpInTargetWindow90": "TA-cible · 90 j",
- "bpInTargetWindowDynamic": "TA-cible · {days} j",
+ "bpInTargetWindow90": "TA-cible (90 j)",
+ "bpInTargetWindowDynamic": "TA-cible ({days} j)",
"bpInTargetCollecting": "collecte de données",
"medications": "Médicaments",
"quickAddMeasurement": "Saisir une mesure",
"quickAddMood": "Saisir une humeur",
"quickAddMedicationIntake": "Saisir une prise",
"quickAddWorkout": "Saisir un entraînement",
+ "quickAddLifeEvent": "Enregistrer un événement de vie",
"customizeDashboard": "Personnaliser le tableau de bord",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Saisir une prise de médicament",
"sheetDescription": "Enregistre la dose, l’heure et le médicament en quelques touches.",
"medicationLabel": "Médicament",
"medicationPlaceholder": "Sélectionner un médicament",
@@ -666,7 +702,8 @@
"body": "Sans copie de la clé, tes données ne peuvent pas être récupérées après une réinstallation. Ça prend une minute.",
"action": "Ouvrir l'étape"
},
- "quickAddSymptom": "Noter un symptôme"
+ "quickAddSymptom": "Noter un symptôme",
+ "todayCardVisible": "Afficher la carte du haut"
},
"measurements": {
"title": "Mesures",
@@ -738,7 +775,7 @@
"formReset": "Réinitialiser le formulaire",
"saveError": "Erreur lors de l’enregistrement",
"duplicateTimestamp": "Une mesure de ce type existe déjà à cet horodatage.",
- "serverOwnedSource": "Les valeurs provenant d’une source connectée ne peuvent pas être modifiées — l’horodatage et la note restent modifiables.",
+ "serverOwnedSource": "Les valeurs provenant d’une source connectée ne peuvent pas être modifiées, mais l’horodatage et la note restent modifiables.",
"deleteError": "Erreur lors de la suppression",
"deletedToast": "Mesure supprimée",
"restoredToast": "Mesure restaurée",
@@ -833,11 +870,12 @@
"durationHoursOnly": "{hours} h",
"durationMinutesOnly": "{minutes} min",
"durationInvalid": "Saisis une durée comme 7,5, 7:30 ou 7h 30m.",
- "durationTooLong": "Une saisie ne peut pas dépasser 24 heures."
+ "durationTooLong": "Une saisie ne peut pas dépasser 24 heures.",
+ "sourceHealthConnect": "Health Connect"
},
"metricCatalog": {
"title": "Ce que HealthLog peut suivre",
- "description": "Tous les domaines de mesures pris en charge par HealthLog, que tu aies déjà des données ou non — et ce qui les fournirait.",
+ "description": "Tous les domaines de mesures pris en charge par HealthLog, que tu aies déjà des données ou non, et ce qui les fournirait.",
"statusPresent": "Suivi",
"statusAbsent": "Pas encore suivi",
"statusModuleOff": "Désactivé",
@@ -845,11 +883,11 @@
"connectCta": "Connecter une source",
"viewCta": "Voir",
"ecg": {
- "source": "Tracés ECG à une dérivation depuis une montre compatible — Withings ScanWatch aujourd'hui — synchronisés automatiquement."
+ "source": "Tracés ECG à une dérivation depuis une montre compatible (Withings ScanWatch aujourd'hui), synchronisés automatiquement."
},
"watchdog": {
"title": "Surveillance automatique",
- "description": "HealthLog surveille aussi discrètement en arrière-plan — notifications de rythme irrégulier et de perturbations respiratoires envoyées par une montre connectée, alertes d'écart par rapport à ta valeur de référence, ainsi que des changements notables entre tes deux derniers bilans de laboratoire — et n'affiche une carte que si quelque chose se démarque. Si au moins une de ces sources est connectée, l'absence de carte est bon signe, pas une fonctionnalité manquante ; si aucune ne l'est encore, c'est simplement qu'il n'y a rien à surveiller."
+ "description": "HealthLog surveille aussi discrètement en arrière-plan (notifications de rythme irrégulier et de perturbations respiratoires envoyées par une montre connectée, alertes d'écart par rapport à ta valeur de référence, ainsi que des changements notables entre tes deux derniers bilans de laboratoire) et n'affiche une carte que si quelque chose se démarque. Si au moins une de ces sources est connectée, l'absence de carte est bon signe, pas une fonctionnalité manquante ; si aucune ne l'est encore, c'est simplement qu'il n'y a rien à surveiller."
}
},
"mood": {
@@ -1006,7 +1044,7 @@
"archive": "Archiver",
"restore": "Restaurer",
"archivedTitle": "Étiquettes archivées",
- "archivedDescription": "Les étiquettes archivées restent dans tes saisies passées — elles disparaissent seulement du sélecteur.",
+ "archivedDescription": "Les étiquettes archivées restent dans tes saisies passées. Elles disparaissent seulement du sélecteur.",
"archivedEmpty": "Rien d’archivé.",
"purgeTitle": "Supprimer l’étiquette définitivement ?",
"purgeBody": "L’étiquette sera retirée de {count} saisies passées. Cette action est irréversible.",
@@ -1031,7 +1069,7 @@
},
"sectionMoreTags": "Plus de tags",
"sectionNoteDetails": "Note et détails",
- "quickTagsHelp": "Vos tags fréquents — en un geste",
+ "quickTagsHelp": "Vos tags fréquents, en un geste",
"quickTagsExpand": "Plus",
"loadError": "Impossible de charger vos entrées d'humeur.",
"sectionLevelA": "État d'esprit",
@@ -1191,7 +1229,8 @@
"bodyLogged": "Consigné dans le journal de maladie : {count} symptômes",
"bodyNotLogged": "Rien n'est consigné dans le journal de maladie pour cette journée",
"openIllness": "Ouvrir"
- }
+ },
+ "sourceNotification": "Notification"
},
"medications": {
"title": "Médicaments",
@@ -1209,8 +1248,8 @@
"dialogDescription": "Ces médicaments sont à prendre maintenant. Confirmez pour les enregistrer tous comme pris.",
"confirm": "Tout prendre ({count})",
"successToast": "{count} prises enregistrées",
- "partialToast": "{taken} enregistrées, {failed} échouées – veuillez réessayer",
- "failedToast": "Aucune prise enregistrée – veuillez réessayer"
+ "partialToast": "{taken} enregistrées, {failed} échouées. Veuillez réessayer",
+ "failedToast": "Aucune prise enregistrée. Veuillez réessayer"
},
"logIntake": {
"dialogTitle": "Enregistrer une prise",
@@ -1284,6 +1323,14 @@
"disabledToast": "Suivi des prises désactivé",
"toggleFailed": "Impossible de modifier le suivi des prises."
},
+ "autoMarkTaken": {
+ "label": "Enregistrer les prises automatiquement",
+ "helper": "HealthLog enregistre chaque dose prévue comme prise à son heure. Vous pouvez toujours modifier ou sauter une dose à la main. Aucun rappel n'est envoyé.",
+ "cardHint": "Enregistré automatiquement",
+ "enabledToast": "Les prises sont désormais enregistrées automatiquement",
+ "disabledToast": "Les prises ne sont plus enregistrées automatiquement",
+ "toggleFailed": "Impossible de modifier l'enregistrement automatique."
+ },
"withoutNotification": "Sans notification",
"pausedSince": "En pause depuis",
"takeNow": "Prendre maintenant",
@@ -1299,7 +1346,7 @@
"complianceInfoLabel": "Que signifie l'observance ?",
"lastIntake": "Dernière prise :",
"nextIntake": "Prochaine prise :",
- "nextIntakeOverdue": "En retard · {time} — encore rattrapable",
+ "nextIntakeOverdue": "En retard depuis {time}, encore rattrapable",
"dayStreak": "jours de série",
"dayStreakOne": "jour de série",
"cycleDueToday": "À prendre aujourd'hui",
@@ -1308,10 +1355,10 @@
"skipped": "Sauté",
"intakeToastTaken": "{name} enregistré comme pris",
"intakeToastSkipped": "{name} sauté",
- "intakeToastFailed": "Impossible d'enregistrer {name} — réessayez",
+ "intakeToastFailed": "Impossible d'enregistrer {name}. Réessayez",
"intakeUndo": "Annuler",
"intakeUndone": "Entrée annulée",
- "intakeUndoFailed": "Annulation impossible — corrigez dans l'historique",
+ "intakeUndoFailed": "Annulation impossible. Corrigez dans l'historique",
"glp1LastInjection": "{label}",
"glp1NextInjectionDays": "{label} (dans {days} jours)",
"glp1NextInjectionToday": "aujourd’hui",
@@ -1393,10 +1440,10 @@
"error": {
"empty": "Saisis d'abord une description.",
"tooLong": "Garde la description sous 2000 caractères.",
- "rateLimit": "Trop de requêtes — attends un instant et réessaie.",
- "noProvider": "Aucun fournisseur d'IA configuré — configure-le dans les Paramètres.",
- "budget": "Budget IA quotidien atteint — réessaie demain.",
- "network": "Impossible de joindre le fournisseur d'IA — réessaie.",
+ "rateLimit": "Trop de requêtes. Attends un instant et réessaie.",
+ "noProvider": "Aucun fournisseur d'IA configuré. Configure-le dans les Paramètres.",
+ "budget": "Budget IA quotidien atteint. Réessaie demain.",
+ "network": "Impossible de joindre le fournisseur d'IA. Réessaie.",
"consent": "La lecture de votre description par IA nécessite d'abord votre consentement.",
"unavailable": "La lecture de descriptions par IA n'est pas disponible ici pour le moment."
}
@@ -1412,7 +1459,7 @@
"everyNMonths": "Tous les N mois un jour précis",
"yearly": "Une fois par an",
"rolling": "Tous les N jours depuis la dernière prise (flexible)",
- "rollingExplainer": "Compte à partir de votre dernière prise enregistrée — se met en pause si vous sautez une dose.",
+ "rollingExplainer": "Compte à partir de votre dernière prise enregistrée ; se met en pause si vous sautez une dose.",
"oneShot": "Dose unique"
},
"weekdays": {
@@ -1470,7 +1517,7 @@
"night": "Nuit"
},
"max": {
- "reached": "Maximum atteint — retirez une heure avant d’en ajouter une autre."
+ "reached": "Maximum atteint : retirez une heure avant d’en ajouter une autre."
}
},
"courseWindow": {
@@ -1520,7 +1567,7 @@
},
"step3": {
"title": "Quelle est la dose ?",
- "subline": "Un comprimé, une bouffée, une goutte — selon la forme.",
+ "subline": "Un comprimé, une bouffée, une goutte, selon la forme.",
"amountLabel": "Quantité",
"amountPlaceholder": "par exemple 5",
"unitLabel": "Unité",
@@ -1546,7 +1593,7 @@
},
"dosesPerUnitLabel": "Doses par unité",
"dosesPerUnitPlaceholder": "p. ex. 4",
- "dosesPerUnitHint": "Doses par stylo / flacon — laisser vide pour ne pas suivre le stock.",
+ "dosesPerUnitHint": "Doses par stylo / flacon. Laisser vide pour ne pas suivre le stock.",
"short": "Dose",
"unitsPerDoseLabel": "Unités par dose",
"unitsPerDoseHint": "p. ex. 2 comprimés de 2 mg pour une dose de 4 mg, ou ½ comprimé pour une dose fractionnée"
@@ -1558,7 +1605,7 @@
},
"step5": {
"title": "À quelle fréquence le prenez-vous ?",
- "subline": "Choisissez la cadence — les détails suivent à l’étape suivante.",
+ "subline": "Choisissez la cadence. Les détails suivent à l’étape suivante.",
"short": "Fréquence"
},
"step6": {
@@ -1580,7 +1627,7 @@
},
"step7": {
"title": "Quand le prenez-vous ?",
- "subline": "Une ou plusieurs heures de la journée — les rappels suivent cette liste.",
+ "subline": "Une ou plusieurs heures de la journée. Les rappels suivent cette liste.",
"presetsLabel": "Préréglages par moment de la journée",
"presets": {
"morning": "Matin",
@@ -1588,7 +1635,7 @@
"evening": "Soir",
"night": "Nuit"
},
- "overrideHint": "Facultatif — définissez une dose ou un nombre d'unités différent pour cet horaire uniquement. Laissez vide pour utiliser la valeur par défaut du médicament.",
+ "overrideHint": "Facultatif : définissez une dose ou un nombre d'unités différent pour cet horaire uniquement. Laissez vide pour utiliser la valeur par défaut du médicament.",
"doseOverrideLabel": "Dose pour cet horaire (facultatif)",
"doseOverridePlaceholder": "ex. un demi-comprimé",
"unitsOverrideLabel": "Unités consommées à cet horaire",
@@ -1596,11 +1643,11 @@
"short": "Heures"
},
"step8": {
- "title": "Terminé — HealthLog doit-il vous le rappeler ?",
+ "title": "Terminé ! HealthLog doit-il vous le rappeler ?",
"subline": "Vérifiez tout ci-dessous avant d’enregistrer.",
"remindersLabel": "Rappels activés",
- "remindersDescription": "Push, Telegram ou ntfy — selon vos réglages.",
- "noChannelHint": "Les rappels nécessitent un canal de notification — aucun n'est actif pour le moment.",
+ "remindersDescription": "Push, Telegram ou ntfy, selon vos réglages.",
+ "noChannelHint": "Les rappels nécessitent un canal de notification, et aucun n'est actif pour le moment.",
"noChannelHintLink": "Configurer les notifications",
"short": "Fini"
}
@@ -1649,7 +1696,7 @@
},
"asNeeded": {
"label": "Au besoin",
- "description": "Pas de plan fixe — enregistrez une prise quand vous en avez besoin (p. ex. antidouleur)."
+ "description": "Pas de plan fixe : enregistrez une prise quand vous en avez besoin (p. ex. antidouleur)."
}
},
"summary": {
@@ -1684,7 +1731,7 @@
}
},
"errors": {
- "submitFailed": "Impossible d’enregistrer le médicament — veuillez réessayer."
+ "submitFailed": "Impossible d’enregistrer le médicament. Veuillez réessayer."
}
},
"doseStrength": {
@@ -1700,11 +1747,11 @@
"empty": "Aucune étape d'escalade de dose enregistrée pour le moment. Ajoutez des changements de dose datés pour tracer le plan.",
"youAreHere": "Vous êtes ici",
"doseStep": "{dose} {unit}",
- "plannedFor": "Prévu · {date}"
+ "plannedFor": "Prévu pour {date}"
},
"injectionSiteBodyOutlineAriaLabel": "Silhouette du corps",
"injectionSitePickerAriaLabel": "Sélecteur de site d'injection",
- "injectionSiteRecommendedAriaLabel": "{site} — prochain site recommandé",
+ "injectionSiteRecommendedAriaLabel": "{site}, prochain site recommandé",
"injectionSiteLegendAriaLabel": "Légende des repères",
"injectionSiteLegendRecommended": "Prochain site recommandé",
"injectionSiteLegendLastUsed": "Dernier site utilisé",
@@ -1740,6 +1787,7 @@
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "via {origin}",
+ "intakeRecordedAutomatically": "Enregistrée automatiquement",
"intakeHistoryEmptyTitle": "Aucune prise pour l’instant",
"intakeHistoryEmptyDescription": "Dès que tu enregistreras une prise, elle apparaîtra ici avec l’heure et la source.",
"intakeHistoryEmptyAction": "Enregistrer une prise",
@@ -1751,7 +1799,7 @@
"intakeHistoryStatusSkipped": "Ignorée",
"intakeHistorySortByTaken": "Trier par heure de prise",
"intakeHistorySortByScheduled": "Trier par heure programmée",
- "intakeHistoryPageInfo": "Page {page} sur {total} · {count} entrées",
+ "intakeHistoryPageInfo": "Page {page} sur {total}, {count} entrées",
"intakeHistoryPrev": "Précédent",
"intakeHistoryNext": "Suivant",
"glp1": {
@@ -1792,7 +1840,7 @@
"cadence": {
"oneShotOn": "Dose unique le {date}.",
"oneShotPending": "Dose unique en attente.",
- "asNeeded": "Au besoin — pas de plan fixe."
+ "asNeeded": "Au besoin, pas de plan fixe."
},
"intake": {
"title": "Historique des prises",
@@ -1819,7 +1867,7 @@
"dialogTitle": "Modifier la prise",
"takenAtLabel": "Prise à",
"scheduledForHint": "Prévue : {dateTime}",
- "farFromScheduledWarning": "Très éloignée de l’heure prévue — vérifiez la date.",
+ "farFromScheduledWarning": "Très éloignée de l’heure prévue. Vérifiez la date.",
"skippedLabel": "Sautée",
"save": "Enregistrer",
"cancel": "Annuler",
@@ -1851,7 +1899,7 @@
"markAsTaken": "Marquer comme prise",
"markAsSkipped": "Marquer comme sautée",
"adHocTag": "hors planning",
- "takenDetail": "prévue {planned} · prise {taken}",
+ "takenDetail": "prévue {planned}, prise {taken}",
"doseTaken": "Dose : {dose}",
"status": {
"taken_on_time": "à l'heure",
@@ -1866,7 +1914,7 @@
"description": "Enregistrez une dose, même rétroactive ou hors planning.",
"takenAtLabel": "Prise à",
"skippedLabel": "Sautée",
- "nudgeHint": "Proche du créneau {slot} — à l'enregistrement, nous demandons s'il faut l'y rattacher.",
+ "nudgeHint": "Proche du créneau {slot} : à l'enregistrement, nous demandons s'il faut l'y rattacher.",
"cancel": "Annuler",
"submit": "Enregistrer"
},
@@ -1915,7 +1963,7 @@
},
"codes": {
"label": "Codes cliniques (ATC / RxNorm)",
- "note": "Facultatif. Utilisés lors de l'export FHIR du dossier de santé. Les codes sont repris tels qu'ils sont saisis — jamais devinés.",
+ "note": "Facultatif. Utilisés lors de l'export FHIR du dossier de santé. Les codes sont repris tels qu'ils sont saisis, jamais devinés.",
"atcLabel": "ATC",
"rxNormLabel": "RxNorm",
"saved": "Codes cliniques enregistrés",
@@ -1935,7 +1983,7 @@
"title": "Terminer le médicament",
"helper": "Arrête les rappels et marque le médicament comme terminé. L’historique reste visible.",
"button": "Terminer",
- "dialogTitle": "Terminer {name} — arrête les rappels, l’historique reste visible",
+ "dialogTitle": "Terminer {name} : arrête les rappels, l’historique reste visible",
"dialogBody": "Plus de rappels. Les anciennes entrées restent dans la chronologie.",
"toast": "Médicament terminé",
"failed": "Impossible de terminer le médicament."
@@ -1973,7 +2021,7 @@
"copyFailed": "Impossible de copier.",
"tokenCopied": "Jeton copié",
"copyToken": "Copier le jeton",
- "mintedHint": "Copiez ce jeton maintenant — il n’est affiché qu’une seule fois."
+ "mintedHint": "Copiez ce jeton maintenant : il n’est affiché qu’une seule fois."
},
"advanced": {
"dataPortability": {
@@ -2002,7 +2050,6 @@
"bestand": "Stock",
"verlauf": "Historique",
"injektion": "Injection",
- "api": "API",
"erweitert": "Avancé",
"wirkung": "Effet"
},
@@ -2014,9 +2061,9 @@
"statusTitle": "Statut",
"nextDoseLabel": "Prochaine dose",
"nextDoseNone": "Aucune dose à venir.",
- "nextDoseAsNeeded": "Au besoin — aucune dose planifiée.",
+ "nextDoseAsNeeded": "Au besoin, aucune dose planifiée.",
"nextDoseNotTracked": "Prises non suivies, conservé pour mémoire.",
- "pausedHint": "En pause — aucun rappel.",
+ "pausedHint": "En pause, aucun rappel.",
"reminderLabel": "Rappel",
"reminderOn": "Activé",
"reminderOff": "Désactivé",
@@ -2087,7 +2134,7 @@
"packagingDefaultPack": "Taille de boîte par défaut : {units} unités",
"packagingNoDefaultPack": "Aucune taille de boîte par défaut",
"packagingEdit": "Modifier",
- "packagingDescription": "Définis combien d’unités consomme une dose et la taille habituelle d’une nouvelle boîte — p. ex. après un changement de fabricant avec une autre taille de plaquette.",
+ "packagingDescription": "Définis combien d’unités consomme une dose et la taille habituelle d’une nouvelle boîte, p. ex. après un changement de fabricant avec une autre taille de plaquette.",
"packagingUnitsPerDoseLabel": "Unités par dose",
"packagingUnitsPerDoseHelper": "P. ex. deux comprimés de 2 mg pour une dose de 4 mg, ou 1,5 pour un et demi (plus de 0, jusqu’à 100). Les prises déjà enregistrées restent inchangées.",
"packagingDefaultPackLabel": "Taille de boîte par défaut (unités)",
@@ -2138,11 +2185,11 @@
"hideEras": "Masquer les périodes antérieures",
"addEra": "Ajouter une période antérieure",
"addEraTitle": "Ajouter une période antérieure",
- "addEraDescription": "Notez à quelles heures ce médicament était pris auparavant — par exemple avant de l'ajuster ici.",
+ "addEraDescription": "Notez à quelles heures ce médicament était pris auparavant, par exemple avant de l'ajuster ici.",
"editAria": "Modifier la période",
"editEraTitle": "Modifier la période",
"editEraDescription": "Ajustez les heures de prise et la plage de dates de cette période.",
- "archivedEditHint": "Cette période a été enregistrée automatiquement — votre modification corrige l'historique enregistré ; l'original est conservé en interne comme trace.",
+ "archivedEditHint": "Cette période a été enregistrée automatiquement. Votre modification corrige l'historique enregistré ; l'original est conservé en interne comme trace.",
"updatedToast": "Période mise à jour",
"updateError": "Impossible de mettre à jour la période. Elle ne doit pas chevaucher d'autres périodes et doit se terminer avant le plan actuel.",
"timesLabel": "Heures de prise",
@@ -2181,10 +2228,10 @@
}
},
"logInjectionSiteTitle": "Enregistrer le site d'injection",
- "logInjectionSiteDescription": "Où avez-vous injecté {name} ? Facultatif — vous pouvez l'ignorer.",
+ "logInjectionSiteDescription": "Où avez-vous injecté {name} ? Facultatif, vous pouvez l'ignorer.",
"logInjectionSiteSkip": "Ignorer",
"logInjectionSiteConfirm": "Enregistrer le site",
- "logInjectionSiteNoneAvailable": "Aucun site d'injection disponible — tous les sites sont exclus dans vos réglages.",
+ "logInjectionSiteNoneAvailable": "Aucun site d'injection disponible : tous les sites sont exclus dans vos réglages.",
"logInjectionSiteSaveFailed": "Impossible d’enregistrer le site d’injection. Veuillez réessayer.",
"perSlotUnits": "{units} par dose",
"trackInjectionSitesToggle": "Suivre les sites d'injection",
@@ -2209,10 +2256,9 @@
"tableStockDoses": "{count} doses",
"tableStockRunway": "≈ {days} jours restants",
"cardLowStockRunway": "Réserve : ≈ {days} jours",
- "takeActionFor": "Prendre – {name}",
- "skipActionFor": "Passer – {name}",
- "reorderTitle": "Modifier l'ordre",
- "reorderDescription": "Définis l'ordre de tes médicaments – il s'applique à la vue cartes et à la vue tableau.",
+ "takeActionFor": "Prendre : {name}",
+ "skipActionFor": "Passer : {name}",
+ "reorderDescription": "Définis l'ordre de tes médicaments pour la vue cartes et la vue tableau.",
"reorderSaved": "Ordre enregistré",
"reorderSaveFailed": "L'ordre n'a pas pu être enregistré",
"customize": "Personnaliser les médicaments",
@@ -2231,7 +2277,7 @@
"levelShift": "Un changement de niveau a été détecté autour du {date}.",
"plateau": {
"title": "Plateau de poids à la dose actuelle",
- "evidence": "Variation de poids de {delta} {unit} sur les {window} derniers jours ({readings} mesures) — {days} jours sous {dose}."
+ "evidence": "Variation de poids de {delta} {unit} sur les {window} derniers jours ({readings} mesures), {days} jours sous {dose}."
},
"retarget": {
"label": "Ce n'est pas la bonne mesure ?",
@@ -2241,16 +2287,16 @@
"error": "Impossible d'enregistrer cette modification. Réessaie."
},
"beforeAfter": {
- "summary": "Ø {before} avant → Ø {after} depuis — un écart de {delta}.",
+ "summary": "Ø {before} avant → Ø {after} depuis, soit un écart de {delta}.",
"counts": "D'après {beforeCount} mesures avant et {afterCount} depuis.",
- "needBefore": "Pas encore assez de mesures avant le début — il faut environ {weeks} semaines avant et après pour comparer.",
- "needAfter": "Pas encore assez de mesures depuis le début — il faut environ {weeks} semaines avant et après pour comparer.",
+ "needBefore": "Pas encore assez de mesures avant le début. Il faut environ {weeks} semaines avant et après pour comparer.",
+ "needAfter": "Pas encore assez de mesures depuis le début. Il faut environ {weeks} semaines avant et après pour comparer.",
"needStart": "Renseignez une date de début pour comparer les mesures avant et après le début.",
- "needData": "Pas encore assez de mesures — il faut environ {weeks} semaines avant et après le début pour comparer."
+ "needData": "Pas encore assez de mesures. Il faut environ {weeks} semaines avant et après le début pour comparer."
},
"insights": {
"title": "Effet des médicaments",
- "subtitle": "Ce qui coïncide avec les mesures visées par vos médicaments — association seulement, jamais un verdict.",
+ "subtitle": "Ce qui coïncide avec les mesures visées par vos médicaments : association seulement, jamais un verdict.",
"link": "Ouvrir la vue Effet",
"empty": "Pas encore assez de données pour relier vos médicaments à leurs mesures cibles."
},
@@ -2356,7 +2402,7 @@
},
"charts": {
"loadingLabel": "Chargement du graphique…",
- "loadingSlowHint": "Calcul des analyses — cela peut prendre un instant.",
+ "loadingSlowHint": "Calcul des analyses, cela peut prendre un instant.",
"weight": "Poids",
"bloodPressure": "Tension artérielle",
"pulse": "Pouls",
@@ -2378,6 +2424,7 @@
"days7Label": "7 j",
"days7Title": "7 derniers jours",
"days30Label": "30 j",
+ "daysNLabel": "{days} j",
"days30Title": "30 derniers jours",
"days90Label": "90 j",
"days90Title": "90 derniers jours",
@@ -2437,13 +2484,15 @@
"loadErrorTitle": "Impossible de charger le graphique",
"loadErrorReload": "Recharger",
"sparseDataCaption": "Davantage de jours étofferont la tendance.",
+ "tooltipHourlyMean": "{metric} (moyenne des heures)",
"dataTable": {
"toggle": "Points de données ({count})",
"columnDate": "Date",
"noValue": "Aucune valeur",
"captionDay": "Une ligne par jour pour {metric} : les mêmes points que ceux tracés par le graphique.",
"captionWeek": "Une ligne par semaine pour {metric}, la moyenne des jours de cette semaine : les mêmes points que ceux tracés par le graphique.",
- "captionMonth": "Une ligne par mois pour {metric}, la moyenne des jours de ce mois : les mêmes points que ceux tracés par le graphique."
+ "captionMonth": "Une ligne par mois pour {metric}, la moyenne des jours de ce mois : les mêmes points que ceux tracés par le graphique.",
+ "hourlyMeanNote": "{metric} est la moyenne de chaque jour, chaque heure comptant une seule fois, afin qu'un entraînement ne pèse pas plus que le reste de la journée."
},
"personalBaseline": "Ta moyenne",
"deltaVsBaseline": "{delta} vs. ta moyenne",
@@ -2471,7 +2520,8 @@
"moodHeatmapEmpty": "Carte thermique de l'humeur, aucun jour enregistré pour l'instant.",
"complianceHeatmap": "Carte thermique d'observance des médicaments du {start} au {end}, {days} jours actifs.",
"complianceHeatmapEmpty": "Carte thermique d'observance des médicaments, aucun jour actif pour l'instant."
- }
+ },
+ "medicationsEmptyAction": "Ajouter un médicament"
},
"chart": {
"overlay": {
@@ -2502,8 +2552,8 @@
},
"tooltipPrior": "Période précédente",
"unavailable": {
- "lastMonth": "Comparaison indisponible — pas de données du mois précédent",
- "lastYear": "Comparaison indisponible — pas de données de l’année précédente"
+ "lastMonth": "Comparaison indisponible : pas de données du mois précédent",
+ "lastYear": "Comparaison indisponible : pas de données de l’année précédente"
},
"notAvailableForCompliance": "Comparaison indisponible pour l’observance"
},
@@ -2515,7 +2565,9 @@
"dismissError": "Impossible d’ignorer ce schéma.",
"restoreError": "Impossible de restaurer ce schéma.",
"loadError": "Impossible de charger les schémas.",
- "empty": "Aucun schéma actuel n’a franchi le seuil de preuve."
+ "empty": "Aucun schéma actuel n’a franchi le seuil de preuve.",
+ "seasonalNote": "Depuis la version 1.42, HealthLog retire la saison et la tendance de fond avant de comparer les valeurs. Vous verrez moins de liens, mais ceux qui restent sont plus solides. Beaucoup des schémas d’avant n’étaient que le calendrier.",
+ "seasonalNoteDismiss": "Fermer la remarque"
},
"correlation": {
"direction": {
@@ -2547,29 +2599,37 @@
"ENV_DAYLIGHT": "la lumière du jour",
"ENV_PRECIP": "les précipitations",
"ENV_PRESSURE_MEAN": "la pression atmosphérique",
- "ENV_PRESSURE_DELTA": "la variation de pression sur la journée"
+ "ENV_PRESSURE_DELTA": "la variation de pression sur la journée",
+ "ENV_PM25": "les particules fines (PM2,5)",
+ "ENV_OZONE_8H": "l’ozone",
+ "ENV_POLLEN_MAX": "le pollen"
},
"daily": {
- "high": "Quand {behaviour} monte, {outcome} du lendemain tend à {direction} dans tes données — une tendance à surveiller, pas une cause.",
- "moderate": "Quand {behaviour} monte, {outcome} du lendemain semble avoir tendance à {direction}, au vu des données actuelles — une tendance à surveiller, pas une cause.",
- "faint": "Quand {behaviour} monte, tout au plus se dessine un faible indice que {outcome} du lendemain tende à {direction} dans tes données — trop ténu pour s'y fier, jamais une cause."
+ "high": "Quand {behaviour} monte, {outcome} du lendemain tend à {direction} dans tes données. C'est une tendance à surveiller, pas une cause.",
+ "moderate": "Quand {behaviour} monte, {outcome} du lendemain semble avoir tendance à {direction}, au vu des données actuelles. C'est une tendance à surveiller, pas une cause.",
+ "faint": "Quand {behaviour} monte, tout au plus se dessine un faible indice que {outcome} du lendemain tende à {direction} dans tes données. C'est trop ténu pour s'y fier et jamais une cause."
},
"lab": {
- "default": "Des valeurs de {lab} plus élevées vont de pair avec {outcome} qui tend à {direction} sur les mêmes périodes dans tes données — une association à surveiller avec ton médecin, jamais une cause.",
- "faint": "Des valeurs de {lab} plus élevées laissent tout au plus entrevoir que {outcome} tende à {direction} sur les mêmes périodes dans tes données — trop ténu pour s'y fier, une association à évoquer avec ton médecin, jamais une cause."
+ "default": "Des valeurs de {lab} plus élevées vont de pair avec {outcome} qui tend à {direction} sur les mêmes périodes dans tes données, une association à surveiller avec ton médecin, jamais une cause.",
+ "faint": "Des valeurs de {lab} plus élevées laissent tout au plus entrevoir que {outcome} tende à {direction} sur les mêmes périodes dans tes données. C'est trop ténu pour s'y fier et jamais une cause, mais à évoquer avec ton médecin."
},
"hypothesis": {
"bpCompliance": {
- "support": "Une meilleure observance des médicaments va de pair avec des valeurs systoliques plus basses — une tendance à surveiller.",
- "surprising": "L'observance et la systolique évoluent ensemble dans tes données — surprenant ; parles-en à ton médecin avant de changer quoi que ce soit.",
+ "support": "Une meilleure observance des médicaments va de pair avec des valeurs systoliques plus basses, une tendance à surveiller.",
+ "surprising": "L'observance et la systolique évoluent ensemble dans tes données, ce qui est surprenant. Parles-en à ton médecin avant de changer quoi que ce soit.",
"none": "L'observance et la systolique n'évoluent pas nettement ensemble sur cette période."
},
"moodPulse": {
- "support": "Les jours de moral plus bas vont de pair avec un pouls au repos plus élevé — une tendance à surveiller.",
- "surprising": "Les jours de meilleur moral vont de pair avec un pouls au repos plus élevé dans tes données — surprenant.",
+ "support": "Les jours de moral plus bas vont de pair avec un pouls au repos plus élevé, une tendance à surveiller.",
+ "surprising": "Les jours de meilleur moral vont de pair avec un pouls au repos plus élevé dans tes données, ce qui est surprenant.",
"none": "L'humeur et le pouls au repos n'évoluent pas nettement ensemble sur cette période."
},
- "weightWeekday": "Le poids du {weekday} est {delta} kg {direction} ta moyenne des autres jours — une tendance à surveiller."
+ "weightWeekday": "Le poids du {weekday} est {delta} kg {direction} ta moyenne des autres jours, une tendance à surveiller."
+ },
+ "dailySameDay": {
+ "high": "Plus de {behaviour} le jour même et la veille va souvent de pair avec {direction} {outcome} dans tes données : un schéma à surveiller, pas une cause.",
+ "moderate": "Plus de {behaviour} le jour même et la veille semble aller de pair avec {direction} {outcome} dans tes données, d’après ce qu’on voit jusqu’ici : un schéma à surveiller, pas une cause.",
+ "faint": "Plus de {behaviour} le jour même et la veille ne montre, au mieux, qu’un faible indice de {direction} {outcome} dans tes données : trop peu pour s’y fier, jamais une cause."
}
},
"noAnalysisYet": "Pas encore d’évaluation.",
@@ -2616,7 +2676,7 @@
"narrativeWeek": "Cette semaine",
"narrativeMonth": "Ce mois-ci",
"narrativeUpdating": "Mise à jour…",
- "heroFallbackSubtitle": "Un coup d’œil quotidien sur tes tendances — directement issu des chiffres que tu saisis.",
+ "heroFallbackSubtitle": "Un coup d’œil quotidien sur tes tendances, directement issu des chiffres que tu saisis.",
"heroGreetingMorning": "Bonjour",
"heroGreetingAfternoon": "Bonjour",
"heroGreetingEvening": "Bonsoir",
@@ -2681,7 +2741,7 @@
"referenceSleep": "Au moins {low} minutes par nuit ; un sommeil plus long n'est pas pénalisé",
"referenceAdiposity": "Rapport taille-hauteur inférieur à {high}",
"referenceHba1c": "HbA1c de {low} à {high} %",
- "referenceDetail": "{label} · {source} · {kind} · {low} à {high}",
+ "referenceDetail": "{label} : de {low} à {high} ({kind}, {source})",
"openBound": "ouvert",
"laboratoryReferenceSource": "Plages de référence du laboratoire",
"personalTargetSource": "Objectif personnel",
@@ -2716,7 +2776,8 @@
"coverage": {
"recommendedMet": "Couvre les trois domaines recommandés par le score",
"areas": "{present} domaines de santé sur {required}"
- }
+ },
+ "historyLink": "Évolution"
},
"dailyBriefing": {
"title": "Briefing du jour",
@@ -2726,7 +2787,7 @@
"emptyDescription": "Génère le briefing du jour pour obtenir un récap de tes tendances.",
"emptyAction": "Générer le briefing",
"loadingLabel": "Préparation du briefing",
- "refreshFailedHint": "Impossible d'actualiser le briefing — affichage du précédent.",
+ "refreshFailedHint": "Impossible d'actualiser le briefing. Affichage du précédent.",
"retryAction": "Réessayer",
"failedTitle": "Impossible de générer le briefing",
"failedDescription": "Le fournisseur d'IA a dépassé le délai ou renvoyé une erreur. Réessayez ou augmentez le délai de réponse dans les paramètres d'IA pour un modèle local lent.",
@@ -2775,7 +2836,7 @@
"experimentCta": "Lancer une expérience de 7 jours",
"experimentTooltip": "Bientôt disponible",
"emptyTitle": "Il faut plus de données pour afficher cette corrélation",
- "sourceChip": "sur la base de {n} mesures appariées · {window}",
+ "sourceChip": "sur la base de {n} mesures appariées ({window})",
"sourceWindowLast30": "30 derniers jours",
"confidenceHigh": "Confiance élevée",
"confidenceModerate": "Confiance moyenne",
@@ -2790,13 +2851,14 @@
}
},
"coach": {
- "refusalOutOfScope": "Je ne peux aider que pour les données de santé de ton journal — mesures, médicaments, humeur, tendances. Les questions hors de ce périmètre ne sont pas traitées.",
- "refusalInjection": "Ton message contient des instructions qui cherchent à remplacer mes consignes. Je suis le coach HealthLog et je ne résume que tes propres données de santé — reformule ta question dans ce cadre.",
- "errorBudget": "Tu as atteint la limite quotidienne du coach. Le quota se réinitialise à minuit (UTC).",
+ "refusalOutOfScope": "Je ne peux aider que pour les données de santé de ton journal : mesures, médicaments, humeur, tendances. Les questions hors de ce périmètre ne sont pas traitées.",
+ "refusalInjection": "Ton message contient des instructions qui cherchent à remplacer mes consignes. Je suis le coach HealthLog et je ne résume que tes propres données de santé. Reformule ta question dans ce cadre.",
+ "errorBudget": "Tu as atteint la limite quotidienne du coach. Le quota se réinitialise à minuit dans ton fuseau horaire.",
"errorProvider": "Le coach n’a pas pu joindre un fournisseur d’analyse maintenant. Réessaie dans un instant.",
+ "errorDemo": "Dans la démo, le Coach ne répond pas.",
"errorNoProvider": "Le coach lit tes données de santé enregistrées et répond aux questions à leur sujet (pas un avis médical). HealthLog a besoin d'un fournisseur d'IA pour cela : connecte ta propre clé ou un modèle local.",
"errorNoProviderAction": "Connecter un fournisseur d'IA",
- "errorNetwork": "Pas de connexion Internet — réessaie une fois reconnecté.",
+ "errorNetwork": "Pas de connexion Internet. Réessaie une fois reconnecté.",
"prefsSaveError": "Impossible d’enregistrer vos préférences du Coach. Réessayez.",
"errorCredentialExpired": "Ta connexion au fournisseur d'IA a expiré. Reconnecte-la dans les Réglages pour que le Coach continue de fonctionner.",
"tagline": "Coach personnel de santé, ancré sur tes données.",
@@ -2815,7 +2877,7 @@
"thinking": "Réflexion en cours…",
"unverifiedFiguresNotice": "Certains chiffres n'ont pas pu être vérifiés avec tes données et ont été omis.",
"composerPlaceholder": "Pose ta question …",
- "nudgeBubbleLabel": "Nouveau message du coach — ouvrir le chat",
+ "nudgeBubbleLabel": "Nouveau message du coach : ouvrir le chat",
"fabLabel": "Ouvrir le coach",
"readAloud": "Lire à voix haute",
"readAloudStop": "Arrêter la lecture",
@@ -2833,14 +2895,13 @@
},
"rememberMessage": {
"action": "Mémoriser",
- "done": "Enregistré dans vos informations personnelles",
- "duplicate": "Déjà présent dans vos informations personnelles",
- "failed": "Échec de l’enregistrement. Veuillez réessayer."
+ "failed": "Échec de l’enregistrement. Veuillez réessayer.",
+ "alreadyKept": "Le Coach s'en souvient déjà."
},
"guided": {
"offerTitle": "Le Coach a {count} questions pour vous",
"offerTitleOne": "Le Coach a une question pour vous",
- "offerBody": "De courtes réponses affinent vos informations personnelles — une question à la fois, ici dans le chat.",
+ "offerBody": "De courtes réponses affinent vos informations personnelles : une question à la fois, ici dans le chat.",
"offerStart": "Répondre maintenant",
"offerLater": "Plus tard",
"offerDismiss": "Ne plus demander",
@@ -2850,7 +2911,7 @@
"later": "Plus tard",
"dismissRemaining": "Ne plus demander",
"answerPlaceholder": "Votre réponse…",
- "summaryTitle": "C’est fait — merci pour vos réponses !",
+ "summaryTitle": "C’est fait, merci pour vos réponses !",
"summaryAnswered": "{answered} sur {total} répondues",
"summaryAdopted": "{adopted} ajoutées à vos informations personnelles",
"summaryAdoptedNone": "rien de nouveau ajouté à vos informations personnelles",
@@ -2862,6 +2923,7 @@
"historyEmpty": "Tes conversations s’affichent ici. Lance une question pour commencer.",
"historyDeleteAria": "Supprimer la conversation",
"historyDeleted": "Conversation supprimée",
+ "historyDeleteFailed": "La conversation n’a pas pu être supprimée.",
"rename": {
"action": "Renommer la conversation",
"input": "Titre de la conversation",
@@ -2875,7 +2937,6 @@
"sourcesFresh": "À jour",
"sourcesStale": "Obsolètes",
"sourcesFooter": "Le coach ne lit que les données que tu as connectées.",
- "evidenceLabel": "Sur quoi est-ce fondé ?",
"metric": {
"bp": "Tension artérielle",
"weight": "Poids",
@@ -2921,14 +2982,6 @@
"skin_temp": "Température cutanée",
"workouts": "Entraînements"
},
- "window": {
- "last7days": "7 derniers jours",
- "last30days": "30 derniers jours",
- "last90days": "90 derniers jours",
- "lastYear": "12 derniers mois",
- "allTime": "toute la période (12 mois en détail)"
- },
- "windowLabel": "Période",
"settingsAriaLabel": "Paramètres du coach",
"settingsTitle": "Paramètres du coach",
"settingsDescription": "Configure comment le coach te parle.",
@@ -2940,9 +2993,10 @@
"settingsVerbosityBrief": "Bref",
"settingsVerbosityDefault": "Standard",
"settingsVerbosityDetailed": "Détaillé",
- "settingsVerbosityConciseHint": "Le ton concis garde déjà les réponses courtes — la longueur reste fixée sur « Bref » tant qu'il est actif.",
- "settingsSourcesPointer": "Les groupes de données que le coach peut utiliser et la fenêtre d'analyse par défaut se gèrent directement dans le chat du coach (volet « Ce sur quoi je m'appuie »).",
- "settingsSourcesPointerLink": "Ouvrir le coach",
+ "settingsVerbosityConciseHint": "Le ton concis garde déjà les réponses courtes : la longueur reste fixée sur « Bref » tant qu'il est actif.",
+ "settingsSourcesSummary": "Le coach voit {count} domaines de données sur {total} et remonte sur {window}.",
+ "settingsSourcesSummaryAll": "Le coach voit {count} domaines de données sur {total} et tout l’historique.",
+ "settingsSourcesPointerLink": "Ouvrir les paramètres du coach",
"settingsExcludeLabel": "Exclure des métriques",
"settingsExcludeHint": "Le coach ne verra pas les données des métriques sélectionnées.",
"settingsContextLabel": "Envoyer le contexte",
@@ -2954,34 +3008,30 @@
"settingsSave": "Enregistrer",
"settingsSaved": "Enregistré",
"settingsCancel": "Annuler",
- "feedbackHelpful": "Utile",
- "feedbackUnhelpful": "Peu utile",
- "feedbackThanks": "Merci pour l’indication.",
- "feedbackError": "Impossible d'enregistrer le retour",
"dailyLimitTitle": "Limite quotidienne atteinte",
- "dailyLimitBody": "Limite quotidienne atteinte. Le quota se réinitialise à minuit (UTC).",
+ "dailyLimitBody": "Limite quotidienne atteinte. Le quota se réinitialise à minuit dans ton fuseau horaire.",
"providerRateLimitTitle": "Fournisseur limité en débit",
"providerRateLimitBody": "Le fournisseur est temporairement saturé ; réinitialisation dans ~5 min.",
"cluster": {
"cardio": {
"label": "Cardiovasculaire",
- "hint": "TA · pouls · VFC · FC repos & marche"
+ "hint": "TA, pouls, VFC, FC au repos et à la marche"
},
"body": {
"label": "Composition corporelle",
- "hint": "Poids · masse grasse · muscle · IMC"
+ "hint": "Poids, masse grasse, muscle et IMC"
},
"activity": {
"label": "Activité",
- "hint": "Pas · énergie · distance · VO₂ max"
+ "hint": "Pas, énergie, distance et VO₂ max"
},
"workouts": {
"label": "Entraînements",
- "hint": "Courses · sorties · séances"
+ "hint": "Courses, sorties et séances"
},
"sleep": {
"label": "Sommeil",
- "hint": "Durée · phases"
+ "hint": "Durée et phases"
},
"mood": {
"label": "Humeur",
@@ -2993,56 +3043,68 @@
},
"medication": {
"label": "Médication",
- "hint": "Observance · contexte GLP-1"
+ "hint": "Observance et contexte GLP-1"
},
"mobility": {
"label": "Mobilité & démarche",
- "hint": "Stabilité · asymétrie · démarche"
+ "hint": "Stabilité, asymétrie et démarche"
},
"environment": {
"label": "Environnement",
- "hint": "Bruit · lumière · temp. cutanée"
+ "hint": "Bruit, lumière du jour et température cutanée"
}
},
"settingsClustersLabel": "Données vues par le Coach",
"settingsClustersHint": "Choisissez les groupes de données que le Coach peut lire. Plus de données, des réponses plus riches et un prompt plus grand.",
"settingsClusterNoData": "Pas encore de données",
- "sourcesMemberCount": "{count} mesures",
+ "sourcesMemberCountOne": "1 mesure",
+ "sourcesMemberCountFew": "{count} mesures",
+ "sourcesMemberCountOther": "{count} mesures",
+ "lookback": {
+ "label": "Jusqu’où le Coach peut remonter",
+ "option": {
+ "last7days": "{days} jours",
+ "last30days": "{days} jours",
+ "last90days": "{days} jours",
+ "lastYear": "{months} mois",
+ "allTime": "Tout"
+ },
+ "detailDays": "Le Coach ne lit rien de plus ancien que {days} jours, dans aucune réponse.",
+ "detailMonths": "Le Coach ne lit rien de plus ancien que {months} mois, dans aucune réponse.",
+ "detailAll": "Tout ce que vous avez enregistré : {dailyMonths} mois au jour près, les mesures plus anciennes en valeurs mensuelles jusqu’à {years} ans en arrière, les analyses des {labMonths} derniers mois.",
+ "note": "Votre profil, vos médicaments actuels, une maladie en cours et vos prochains rendez-vous restent visibles ; les synthèses qui demandent plus d’historique sont raccourcies ou omises. Les réponses données plus tôt par le Coach dans d’anciennes conversations peuvent encore citer des valeurs plus anciennes."
+ },
"showConversations": "Afficher les conversations",
"hideConversations": "Masquer les conversations",
"heroGreeting": "Pose-moi une question sur tes données",
"heroSubline": "Pose une question sur tes tendances, médicaments ou mesures.",
- "thinkingElapsed": "{seconds}s",
"thinkingDuration": "A réfléchi pendant {seconds}s",
"thinkingExpandAria": "Montrer ce que le Coach a pris en compte",
"thinkingDetail": "Réponse établie à partir de ton aperçu santé.",
"tokensUsed": "{count} jetons",
- "tokensUsedWithModel": "{count} jetons · {model}",
"askAboutThis": "Interroger le coach à ce sujet",
"readStrip": {
"label": "Plage habituelle",
"baselineWithin": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} s'y situe.",
"baselineAbove": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} se situe au-dessus.",
"baselineBelow": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} se situe en dessous.",
- "insufficient": "J'apprends encore ta plage habituelle — continue à enregistrer quelques jours de plus.",
+ "sameHoursWithin": "À ce moment de la journée, tu es habituellement à {low}–{high} {unit} ; aujourd'hui jusqu'ici, {value} s'y situe.",
+ "sameHoursAbove": "À ce moment de la journée, tu es habituellement à {low}–{high} {unit} ; aujourd'hui jusqu'ici, {value} se situe au-dessus.",
+ "sameHoursBelow": "À ce moment de la journée, tu es habituellement à {low}–{high} {unit} ; aujourd'hui jusqu'ici, {value} se situe en dessous.",
+ "steadyWithin": "Tes valeurs sont habituellement à {low} {unit} ; aujourd'hui, {value} correspond.",
+ "steadyAbove": "Tes valeurs sont habituellement à {low} {unit} ; aujourd'hui, {value} se situe au-dessus.",
+ "steadyBelow": "Tes valeurs sont habituellement à {low} {unit} ; aujourd'hui, {value} se situe en dessous.",
+ "latestWithin": "Ta plage habituelle est de {low}–{high} {unit} ; ta dernière mesure, du {date}, était de {value}, dans cette plage.",
+ "latestAbove": "Ta plage habituelle est de {low}–{high} {unit} ; ta dernière mesure, du {date}, était de {value}, au-dessus.",
+ "latestBelow": "Ta plage habituelle est de {low}–{high} {unit} ; ta dernière mesure, du {date}, était de {value}, en dessous.",
+ "steadyLatestWithin": "Tes valeurs sont habituellement à {low} {unit} ; ta dernière mesure, du {date}, était de {value}, comme d'habitude.",
+ "steadyLatestAbove": "Tes valeurs sont habituellement à {low} {unit} ; ta dernière mesure, du {date}, était de {value}, au-dessus.",
+ "steadyLatestBelow": "Tes valeurs sont habituellement à {low} {unit} ; ta dernière mesure, du {date}, était de {value}, en dessous.",
+ "insufficient": "J'apprends encore ta plage habituelle. Continue à enregistrer quelques jours de plus.",
"driver": "{note}"
},
"scope": {
- "prefix": "Le Coach est déjà sur {metric}",
- "tap": "Touche pour demander",
- "question": "Explique-moi cette mesure — y a-t-il quelque chose à surveiller ?"
- },
- "seeded": {
- "prefix": "À regarder aujourd'hui : {signal}",
- "signal": {
- "readiness": "disponibilité",
- "recovery": "récupération"
- },
- "question": {
- "readiness": "Ma disponibilité est inhabituelle aujourd'hui — qu'est-ce qui l'explique ?",
- "recovery": "Ma récupération a changé — qu'est-ce qui se cache derrière ?"
- },
- "dismiss": "Ignorer"
+ "prefix": "Le Coach est déjà sur {metric}"
},
"settings": "Paramètres",
"history": {
@@ -3052,31 +3114,31 @@
"groupEarlier": "Plus tôt"
},
"seed": {
- "bloodPressure": "Passe en revue avec moi la tendance de ma tension artérielle sur les 30 derniers jours — y a-t-il quelque chose à surveiller ?",
+ "bloodPressure": "Passe en revue avec moi la tendance de ma tension artérielle sur les 30 derniers jours. Y a-t-il quelque chose à surveiller ?",
"weight": "Comment mon poids a-t-il évolué ces derniers temps, et la tendance mérite-t-elle d'agir ?",
"bmi": "Que m'indique la tendance de mon IMC, et comment dois-je l'interpréter ?",
- "pulse": "Passe en revue avec moi mes mesures de pouls — y a-t-il quelque chose d'inhabituel ?",
+ "pulse": "Passe en revue avec moi mes mesures de pouls. Y a-t-il quelque chose d'inhabituel ?",
"restingHr": "Comment ma fréquence cardiaque au repos a-t-elle évolué, et que dit-elle de ma condition physique ?",
"hrv": "Que m'indique ma variabilité de la fréquence cardiaque sur ma récupération et mon stress ces derniers temps ?",
- "sleep": "Passe en revue avec moi mon sommeil récent — durée, régularité et tout ce qui mériterait d'être changé.",
+ "sleep": "Passe en revue avec moi mon sommeil récent : durée, régularité et tout ce qui mériterait d'être changé.",
"mood": "Quels schémas observes-tu dans mon humeur ces dernières semaines ?",
"medications": "Où en est mon observance médicamenteuse, et correspond-elle à la façon dont je me sens ?",
"steps": "À quel point ai-je été actif ces derniers temps d'après mes pas, et comment cela se compare-t-il à un niveau de référence sain ?",
"activeEnergy": "Que dit ma tendance de dépense énergétique active sur mon niveau d'activité ces derniers temps ?",
"cardioFitness": "Que signifie la tendance de ma condition cardio (VO₂ max), et comment la faire évoluer dans le bon sens ?",
- "bloodGlucose": "Passe en revue avec moi mes dernières mesures de glycémie — sont-elles dans une fourchette saine ?",
+ "bloodGlucose": "Passe en revue avec moi mes dernières mesures de glycémie. Sont-elles dans une fourchette saine ?",
"oxygenSaturation": "Que m'indique ma tendance de saturation en oxygène (SpO₂) ?",
"respiratoryRate": "Que dit ma tendance de fréquence respiratoire sur ma santé ?",
- "workouts": "Passe en revue avec moi mes séances d'entraînement récentes — charge, fréquence et comment je récupère entre elles.",
+ "workouts": "Passe en revue avec moi mes séances d'entraînement récentes : charge, fréquence et comment je récupère entre elles.",
"recoveryPage": "Pourquoi ma récupération en est-elle là en ce moment, et qu'est-ce qui l'explique ?",
- "briefing": "Passe en revue avec moi le briefing d'aujourd'hui — sur quoi devrais-je me concentrer, et y a-t-il quelque chose à entreprendre ?",
+ "briefing": "Passe en revue avec moi le briefing d'aujourd'hui. Sur quoi devrais-je me concentrer, et y a-t-il quelque chose à entreprendre ?",
"healthScore": "Pourquoi mon score de santé est-il de {score} sur 100, et qu'est-ce qui le ferait le plus progresser ?",
- "periodWeek": "Passe en revue avec moi comment s'est déroulée ma dernière semaine sur mes indicateurs de santé — qu'est-ce qui ressort ?",
- "periodMonth": "Passe en revue avec moi comment s'est déroulé mon dernier mois sur mes indicateurs de santé — qu'est-ce qui ressort ?",
+ "periodWeek": "Passe en revue avec moi comment s'est déroulée ma dernière semaine sur mes indicateurs de santé. Qu'est-ce qui ressort ?",
+ "periodMonth": "Passe en revue avec moi comment s'est déroulé mon dernier mois sur mes indicateurs de santé. Qu'est-ce qui ressort ?",
"correlationBpCompliance": "Quel est le lien entre mon observance médicamenteuse et ma tension artérielle ? Explique-moi ce que cette corrélation signifie pour moi.",
"correlationMoodPulse": "Y a-t-il un lien réel entre mon humeur et mon pouls ? Aide-moi à interpréter cette corrélation.",
"correlationWeightWeekday": "Mon poids suit-il un schéma hebdomadaire ? Que m'indique cette corrélation selon le jour de la semaine ?",
- "recommendation": "Parle-moi davantage de cette recommandation : \"{text}\" — pourquoi s'applique-t-elle à moi, et que devrais-je faire ?"
+ "recommendation": "Parle-moi davantage de cette recommandation : \"{text}\". Pourquoi s'applique-t-elle à moi, et que devrais-je faire ?"
},
"assessmentPrompt": "Explique-moi mon évaluation de {metric} : qu'est-ce que cela signifie et que dois-je faire ?",
"attach": {
@@ -3111,7 +3173,102 @@
"errorNotPermitted": "Le Coach n'est pas disponible dans un dossier que quelqu'un partage avec vous.",
"errorUnavailable": "Le Coach n'a pas pu vérifier ses réglages pour l'instant. Réessayez dans un instant.",
"errorOperatorOff": "L'IA est désactivée sur ce serveur, le Coach ne peut donc pas répondre.",
- "errorCoachOff": "Le Coach est désactivé. Vous pouvez le réactiver dans Réglages, Modules."
+ "errorCoachOff": "Le Coach est désactivé. Vous pouvez le réactiver dans Réglages, Modules.",
+ "frame": {
+ "showPanel": "Afficher les conversations",
+ "hidePanel": "Masquer les conversations",
+ "panelDescription": "Tes conversations avec le coach, les plus récentes d’abord.",
+ "rowActions": "Actions pour {title}",
+ "rename": "Renommer",
+ "delete": "Supprimer",
+ "modelSection": "Modèle",
+ "provider": "Répond avec",
+ "modelManaged": "Le modèle de ce fournisseur se règle dans les paramètres IA.",
+ "noProvider": "Aucun fournisseur disponible",
+ "saving": "Enregistrement…",
+ "saveFailed": "Impossible d’enregistrer cette modification.",
+ "allSettings": "Tous les paramètres IA",
+ "providerNotActive": "{provider} n'a pas pu prendre le relais : aucune connexion ni clé valide. Configurez-le dans les réglages IA.",
+ "operatorCodex": "ChatGPT (partagé par l'opérateur)",
+ "operatorManaged": "L'opérateur de votre serveur gère cette connexion. Dès qu'un de vos propres fournisseurs est configuré, c'est lui qui répond en premier."
+ },
+ "answer": {
+ "scrollToBottom": "Aller au dernier message",
+ "rememberDone": "Mémorisé",
+ "timeYesterday": "hier {time}",
+ "timeEarlier": "{date}, {time}",
+ "info": "Détails de la réponse",
+ "infoModel": "Modèle : {model}"
+ },
+ "activity": {
+ "thinking": "Réfléchit…",
+ "fetching": "Récupère {domain}, {window}…",
+ "digestOne": "Résume {count} mesure…",
+ "digestFew": "Résume {count} mesures…",
+ "digestOther": "Résume {count} mesures…",
+ "digestDoneOne": "{count} mesure sur {areas}",
+ "digestDoneFew": "{count} mesures sur {areas}",
+ "digestDoneOther": "{count} mesures sur {areas}",
+ "areasOne": "{count} domaine",
+ "areasFew": "{count} domaines",
+ "areasOther": "{count} domaines",
+ "memoryOne": "Se souvient de {count} chose que tu lui as dite",
+ "memoryFew": "Se souvient de {count} choses que tu lui as dites",
+ "memoryOther": "Se souvient de {count} choses que tu lui as dites",
+ "remember": "Retient quelque chose…",
+ "plan": "Propose un plan…",
+ "asking": "Te pose une question…",
+ "answer": "Rédige la réponse…",
+ "stop": {
+ "budget": "Budget atteint, répond avec ce qu'il a",
+ "time": "Temps écoulé, répond avec ce qu'il a",
+ "cap": "Beaucoup d'étapes, conclut maintenant",
+ "noProgress": "Plus de nouvelles données, conclut"
+ },
+ "thoughtProcessOne": "Raisonnement ({count} étape)",
+ "thoughtProcessFew": "Raisonnement ({count} étapes)",
+ "thoughtProcessOther": "Raisonnement ({count} étapes)",
+ "awaitingAnswer": "Réponse attendue"
+ },
+ "memory": {
+ "saved": "Retenu : {fact}",
+ "undo": "Annuler",
+ "accept": "Oui, retiens-le",
+ "decline": "Non",
+ "confirmed": "C'est noté, je m'en souviendrai.",
+ "declined": "D'accord, je ne le ferai pas."
+ },
+ "plan": {
+ "accept": "Adopter ce plan",
+ "decline": "Pas maintenant",
+ "confirmed": "C'est noté. On fait le point ensemble dans {days} jours.",
+ "declined": "D'accord, pas maintenant."
+ },
+ "assumption": {
+ "line": "Hypothèse : {value}",
+ "change": "Modifier"
+ },
+ "suggestedReplies": {
+ "groupLabel": "Réponses suggérées"
+ },
+ "chart": {
+ "compare": {
+ "periods": "{a} contre {b}"
+ }
+ },
+ "reasoning": {
+ "label": "Profondeur de réflexion",
+ "hint": "Réfléchir plus en profondeur donne des réponses plus précises, avec un peu plus d'attente.",
+ "off": "Désactivé",
+ "minimal": "Minimale",
+ "low": "Faible",
+ "medium": "Moyenne",
+ "high": "Élevée",
+ "capped": "(limité par l'administrateur)",
+ "disabled": "L'administrateur a désactivé la réflexion approfondie."
+ },
+ "memoryLink": "Mémoire ({count})",
+ "newConversation": "Nouvelle conversation"
},
"relativeJustNow": "à l’instant",
"relativeMinutesAgoOne": "il y a {count} minute",
@@ -3174,7 +3331,7 @@
},
"hrv": {
"title": "VFC",
- "description": "Variabilité de la fréquence cardiaque dans le temps — signal de récupération du système nerveux, capté la nuit par l'Apple Watch et d'autres wearables compatibles.",
+ "description": "Variabilité de la fréquence cardiaque dans le temps : signal de récupération du système nerveux, capté la nuit par l'Apple Watch et d'autres wearables compatibles.",
"chartTitle": "Variabilité de la fréquence cardiaque",
"emptyState": {
"title": "Aucune mesure de VFC",
@@ -3183,17 +3340,17 @@
},
"restingHr": {
"title": "FC au repos",
- "description": "La fréquence cardiaque de base sur la journée — distincte des pointages ponctuels de la page Pulse. Apple Santé fournit une valeur par jour.",
+ "description": "La fréquence cardiaque de base sur la journée, distincte des pointages ponctuels de la page Pulse. Apple Santé fournit une valeur par jour.",
"chartTitle": "Fréquence cardiaque au repos",
"emptyState": {
"title": "Aucune FC au repos",
"description": "La FC au repos arrive dès que l'Apple Watch ou un appareil Withings compatible a porté une journée calme."
},
- "coachPrefill": "Je n'ai pas encore enregistré de fréquence cardiaque au repos — pourquoi la valeur quotidienne au repos diffère-t-elle d'un pouls ponctuel, et comment l'améliorer ?"
+ "coachPrefill": "Je n'ai pas encore enregistré de fréquence cardiaque au repos. Pourquoi la valeur quotidienne au repos diffère-t-elle d'un pouls ponctuel, et comment l'améliorer ?"
},
"oxygenSaturation": {
"title": "Saturation en oxygène",
- "description": "Oxygène sanguin (SpO₂) — plage clinique normale 95–100 %, avec zones cibles ombrées sur le graphique.",
+ "description": "Oxygène sanguin (SpO₂) : plage clinique normale 95–100 %, avec zones cibles ombrées sur le graphique.",
"chartTitle": "Saturation en oxygène",
"emptyState": {
"title": "Aucune mesure SpO₂",
@@ -3206,13 +3363,13 @@
"chartTitle": "Température corporelle",
"emptyState": {
"title": "Aucune température corporelle",
- "description": "Enregistre une mesure ou laisse ton wearable en synchroniser une — la tendance apparaîtra ici.",
+ "description": "Enregistre une mesure ou laisse ton wearable en synchroniser une, et la tendance apparaîtra ici.",
"cta": "Enregistrer la température"
}
},
"steps": {
"title": "Pas",
- "description": "Nombre de pas quotidien — une ligne par jour, depuis Apple Santé ou un appareil d'activité connecté.",
+ "description": "Nombre de pas quotidien : une ligne par jour, depuis Apple Santé ou un appareil d'activité connecté.",
"chartTitle": "Pas par jour",
"emptyState": {
"title": "Aucune donnée de pas",
@@ -3221,7 +3378,7 @@
},
"activeEnergy": {
"title": "Énergie active",
- "description": "Total quotidien d'énergie active en kilocalories — une ligne par jour, depuis Apple Santé ou Withings activité.",
+ "description": "Total quotidien d'énergie active en kilocalories : une ligne par jour, depuis Apple Santé ou Withings activité.",
"chartTitle": "Énergie active",
"emptyState": {
"title": "Aucune donnée d'énergie active",
@@ -3245,7 +3402,7 @@
"weightBody": "Le poids corporel est l'un des signaux les plus simples de l'équilibre énergétique à long terme et de la santé globale. La tendance sur plusieurs semaines compte bien plus qu'une mesure isolée, qui varie avec l'hydratation, l'alimentation et l'heure de la journée.",
"bmiBody": "L'IMC correspond à votre poids divisé par votre taille au carré, un seul chiffre qui vous situe dans une large catégorie de poids. C'est un dépistage de population, pas un diagnostic, et il ne distingue pas le muscle de la graisse.",
"sleepBody": "La durée du sommeil est le temps réellement dormi au cours de la nuit, réparti entre sommeil léger, profond et paradoxal. La plupart des adultes ont besoin d'environ sept à neuf heures ; une quantité et une qualité régulières soutiennent la récupération, l'humeur et le métabolisme.",
- "moodBody": "Le suivi de l'humeur consigne ce que vous ressentez au fil du temps sur une échelle simple. Le noter régulièrement révèle des tendances — comment le sommeil, l'activité ou les médicaments s'alignent sur votre ressenti — qu'une seule journée ne montre jamais.",
+ "moodBody": "Le suivi de l'humeur consigne ce que vous ressentez au fil du temps sur une échelle simple. Le noter régulièrement révèle des tendances (comment le sommeil, l'activité ou les médicaments s'alignent sur votre ressenti) qu'une seule journée ne montre jamais.",
"medicationsBody": "L'observance mesure la fiabilité avec laquelle vous prenez votre traitement comme prescrit, ici sous forme de la part des doses attendues que vous avez enregistrées. Une prise régulière est souvent ce qui permet à un traitement d'agir comme prévu.",
"restingHrBody": "La fréquence cardiaque au repos est votre rythme cardiaque lorsque vous êtes totalement au repos, généralement mesuré la nuit. Une fréquence plus basse indique souvent une meilleure forme cardiovasculaire, et une tendance à la hausse peut être un signe précoce de stress ou de maladie.",
"hrvBody": "La VFC est la petite variation du temps entre des battements consécutifs. Une variabilité plus élevée reflète généralement un système nerveux bien récupéré et adaptable, tandis qu'une baisse durable peut signaler de la fatigue, du stress ou une récupération insuffisante.",
@@ -3257,7 +3414,7 @@
"bodyWaterBody": "L'eau corporelle totale est la part de votre poids constituée de liquide, à l'intérieur et autour des cellules. Elle varie avec l'hydratation, l'alimentation et l'activité, si bien que la tendance sur plusieurs semaines en dit plus qu'une seule mesure.",
"boneMassBody": "La masse osseuse est le poids estimé des minéraux de votre squelette. Les estimations sur balance sont approximatives, mais une tendance stable ou en hausse soutient la solidité osseuse à long terme.",
"fatFreeMassBody": "La masse non grasse est tout ce qui, dans votre corps, n'est pas de la graisse : muscle, os, organes et eau. La maintenir ou la développer tout en perdant du poids signale en général une composition corporelle plus saine.",
- "fatMassBody": "La masse grasse est le poids total de la graisse de votre corps. Une part de graisse est indispensable aux hormones et au stockage d'énergie, mais un excès — surtout au niveau de l'abdomen — augmente le risque pour la santé à long terme (WHO 2024).",
+ "fatMassBody": "La masse grasse est le poids total de la graisse de votre corps. Une part de graisse est indispensable aux hormones et au stockage d'énergie, mais un excès, surtout au niveau de l'abdomen, augmente le risque pour la santé à long terme (WHO 2024).",
"bodyFatBody": "Le taux de masse grasse est la part de votre poids constituée de graisse. Les plages saines dépendent du sexe et de l'âge, et les balances l'estiment par bio-impédance : la tendance sur plusieurs semaines en dit plus qu'une mesure isolée.",
"muscleMassBody": "La masse musculaire est le poids estimé des muscles de votre corps. Plus de muscle soutient la force, le métabolisme et la mobilité, et demande généralement un effort actif pour être préservé avec l'âge.",
"visceralFatBody": "La graisse viscérale est la graisse stockée au plus profond de l'abdomen, autour des organes. Contrairement à la graisse sous la peau, un niveau élevé est étroitement lié aux maladies cardiaques et au diabète de type 2 : plus elle est basse, mieux c'est.",
@@ -3278,7 +3435,7 @@
"daylightBody": "Le temps à la lumière du jour correspond à la durée passée en lumière extérieure chaque jour. Une lumière naturelle régulière soutient un rythme veille-sommeil et une humeur sains (CDC 2024), et le temps passé dehors est lié à la santé oculaire.",
"bloodGlucoseBody": "La glycémie est la quantité de sucre dans votre sang, principale source d'énergie de l'organisme. Elle augmente après les repas et se stabilise entre eux ; des valeurs durablement élevées peuvent signaler un diabète ou un prédiabète.",
"skinTemperatureBody": "La température cutanée correspond ici à la petite variation nocturne de la température de votre poignet par rapport à votre propre référence, et non à une mesure absolue de fièvre. Les variations peuvent suivre le sommeil, l'environnement, une maladie ou la phase du cycle menstruel.",
- "cardioFitnessBody": "Le VO₂ max correspond à la quantité d'oxygène que votre corps peut utiliser à l'effort maximal — le meilleur indice de laboratoire de la forme aérobie, que les wearables estiment de façon indirecte. Une tendance plus élevée ou en hausse est un fort signe de santé cardiovasculaire.",
+ "cardioFitnessBody": "Le VO₂ max correspond à la quantité d'oxygène que votre corps peut utiliser à l'effort maximal. C'est le meilleur indice de laboratoire de la forme aérobie, que les wearables estiment de façon indirecte. Une tendance plus élevée ou en hausse est un fort signe de santé cardiovasculaire.",
"cardioRecoveryBody": "La récupération cardiaque correspond au nombre de battements par minute dont votre fréquence cardiaque diminue dans la première minute après l'arrêt d'un effort intense. Une baisse plus forte signifie que votre cœur se calme plus vite, signe d'une bonne condition physique.",
"wristTemperatureBody": "La température du poignet est mesurée pendant votre sommeil et indiquée comme l'écart par rapport à votre valeur de référence, et non comme une valeur absolue. Une valeur stable est normale, tandis qu'une hausse durable peut signaler une maladie, un entraînement intense ou un changement de cycle.",
"fallsBody": "Ceci compte les chutes lourdes détectées par votre wearable. L'objectif est zéro ; un nombre en hausse est un signal de risque de mobilité à évoquer avec un professionnel, surtout pour les personnes âgées (CDC STEADI 2024).",
@@ -3286,13 +3443,13 @@
"stairAscentSpeedBody": "C'est la vitesse à laquelle vous montez une volée de marches. C'est un marqueur sensible de la force des jambes et de la mobilité : un rythme stable ou en hausse est bon signe, un rythme en baisse mérite attention.",
"stairDescentSpeedBody": "C'est la vitesse à laquelle vous descendez une volée de marches. Avec la vitesse de montée, elle reflète le contrôle des jambes et l'équilibre : un rythme stable est bon signe, un rythme en baisse mérite attention.",
"breathingDisturbancesBody": "Un wearable peut estimer à quelle fréquence votre respiration a été interrompue pendant le sommeil. Un indice plus bas est préférable ; un niveau signalé comme élevé, surtout sur plusieurs nuits, mérite d'être évoqué avec un professionnel. C'est une approximation de dépistage, pas un diagnostic d'apnée du sommeil.",
- "stepsBody": "Le nombre de pas correspond au total de pas que vous faites chaque jour — la mesure la plus simple de l'activité quotidienne. Une habitude régulière compte plus qu'une seule grosse journée, et l'objectif rond de 10 000 pas est un slogan publicitaire, pas un résultat de recherche.",
+ "stepsBody": "Le nombre de pas correspond au total de pas que vous faites chaque jour : la mesure la plus simple de l'activité quotidienne. Une habitude régulière compte plus qu'une seule grosse journée, et l'objectif rond de 10 000 pas est un slogan publicitaire, pas un résultat de recherche.",
"readinessBody": "La disponibilité est une lecture quotidienne de la disposition de votre corps à affronter la journée, mêlant fréquence cardiaque au repos, variabilité de la fréquence cardiaque et sommeil. C'est une approximation de l'appareil rapportée à votre propre référence, pas une mesure clinique : un score élevé signifie récupéré, un score plus bas invite à lever le pied.",
- "sleepScoreBody": "Votre score de sommeil résume toute une nuit en un seul chiffre — durée, efficacité, régularité des horaires et équilibre entre sommeil léger, profond et paradoxal. Observez la tendance sur plusieurs semaines plutôt que de courir après un score parfait sur une seule nuit.",
+ "sleepScoreBody": "Votre score de sommeil résume toute une nuit en un seul chiffre : durée, efficacité, régularité des horaires et équilibre entre sommeil léger, profond et paradoxal. Observez la tendance sur plusieurs semaines plutôt que de courir après un score parfait sur une seule nuit.",
"recoveryBody": "La récupération reflète la qualité du rétablissement de votre corps, à partir de signaux nocturnes comme la variabilité de la fréquence cardiaque et la fréquence au repos. Un score élevé est un feu vert pour pousser ; un score plus bas suggère que votre organisme se reconstruit encore.",
"strainBody": "La charge mesure l'effort cardiovasculaire et physique que vous avez imposé à votre corps, des entraînements à une journée active debout. Dès qu'il y a assez d'historique, elle est évaluée par rapport à votre propre effort habituel, pas à un standard extérieur.",
"stressBody": "Le stress est ici une estimation de la charge physiologique sur votre système nerveux, lue surtout à partir des schémas de variabilité de la fréquence cardiaque et non de ce que vous ressentez consciemment. Un score plus bas indique un état calme et bien régulé ; un score plus élevé invite à prévoir de la récupération.",
- "recoveryPageBody": "La récupération rassemble les signaux nocturnes enregistrés par votre wearable — recharge, effort, charge ainsi que vos totaux de fréquence cardiaque et d'énergie du jour — pour montrer à quel point votre corps a récupéré. Observez la tendance sur plusieurs jours plutôt qu'un seul chiffre ; une valeur plus faible invite à lever le pied, ce n'est pas une alarme.",
+ "recoveryPageBody": "La récupération rassemble les signaux nocturnes enregistrés par votre wearable (recharge, effort, charge ainsi que vos totaux de fréquence cardiaque et d'énergie du jour) pour montrer à quel point votre corps a récupéré. Observez la tendance sur plusieurs jours plutôt qu'un seul chiffre ; une valeur plus faible invite à lever le pied, ce n'est pas une alarme.",
"pulsePressureBody": "La pression pulsée est l'écart entre votre valeur systolique et diastolique (systolique moins diastolique). À titre indicatif général, environ 40 mmHg est typique au repos ; un écart large au-dessus d'environ 60 mmHg à l'âge moyen peut refléter des artères rigidifiées, tandis qu'un écart étroit en dessous d'environ 25 mmHg peut accompagner un faible volume d'éjection (StatPearls/NIH 2023). C'est un sujet de discussion, pas un diagnostic.",
"meanArterialPressureBody": "La pression artérielle moyenne est la pression moyenne que voient vos organes sur un battement, soit grosso modo la diastolique plus un tiers de l'écart avec la systolique. À titre indicatif général, une valeur au repos se situe autour de 70–100 mmHg (StatPearls/NIH 2023). Le seuil de ~60 mmHg souvent cité est un concept de soins aigus, pas une alarme pour l'autosurveillance.",
"cycleEducationBody": "Votre cycle traverse quatre phases pilotées par les hormones, et sa durée et sa régularité sont une fenêtre accessible sur la santé globale. À titre indicatif général, les cycles chez l'adulte durent typiquement 21–35 jours (les références cliniques citent 24–38), avec des règles d'environ cinq jours (ACOG 2015). La phase lutéale est l'ancrage stable, donc l'ovulation s'estime au mieux en comptant à rebours depuis vos prochaines règles prévues, jamais à un « jour 14 » fixe. Une fenêtre fertile prédite est une estimation qui demande plusieurs cycles enregistrés et reste approximative, et la température basale confirme l'ovulation après coup plutôt qu'elle ne la prédit. Ce n'est pas une méthode de contraception.",
@@ -3311,7 +3468,7 @@
"mean": "Moyenne"
},
"showAllValues": "Afficher toutes les mesures",
- "valuesPageTitle": "{metric} — toutes les mesures",
+ "valuesPageTitle": "{metric} : toutes les mesures",
"valuesPageDescription": "Chaque mesure derrière cette métrique. Modifiez ou supprimez une entrée directement.",
"diversity": {
"trigger": "Conseil de mesure",
@@ -3351,11 +3508,11 @@
"inBed": "Au lit",
"nap": "Sieste",
"napCount": "{count} siestes",
- "unavailable": "Pas encore de données de phases — les phases de sommeil apparaîtront ici dès qu’une source connectée les fournira."
+ "unavailable": "Pas encore de données de phases. Les phases de sommeil apparaîtront ici dès qu’une source connectée les fournira."
},
"empty": {
"title": "Pas encore de données de sommeil",
- "description": "Connecte une source comme Apple Health ou un objet connecté — ou enregistre ton sommeil manuellement — et cette vue se remplira automatiquement.",
+ "description": "Connecte une source comme Apple Health ou un objet connecté (ou enregistre ton sommeil manuellement) et cette vue se remplira automatiquement.",
"action": "Gérer les appareils"
},
"debt": {
@@ -3364,21 +3521,21 @@
"caption": "Solde restant sur les {nights} dernières nuits",
"clearValue": "À jour",
"clearCaption": "Rien en attente sur les {nights} dernières nuits",
- "learning": "Votre sommeil est encore en cours d'analyse — {counted} nuits jusqu'ici, encore {remaining} pour un bilan fiable.",
+ "learning": "Votre sommeil est encore en cours d'analyse : {counted} nuits jusqu'ici, encore {remaining} pour un bilan fiable.",
"computedInfo": "Il s'agit de l'estimation propre à HealthLog : un solde glissant entre le besoin et le sommeil des dernières nuits, que le sommeil de récupération réduit. Elle peut différer de la dette de sommeil native d'un wearable, qui utilise son propre modèle de besoin."
},
"average": {
"title": "Moyenne par nuit",
"value": "{hours} h {minutes} min",
"caption": "Sommeil moyen sur les {nights} dernières nuits",
- "learning": "Votre sommeil est encore en cours d'analyse — {counted} nuits jusqu'ici, encore {remaining} pour une moyenne fiable."
+ "learning": "Votre sommeil est encore en cours d'analyse : {counted} nuits jusqu'ici, encore {remaining} pour une moyenne fiable."
},
"chronotype": {
"title": "Chronotype",
- "learning": "Votre rythme est encore en cours d'analyse — {counted} sur {total} nuits libres. Un chronotype a besoin de quelques matins sans réveil pour avoir du sens.",
+ "learning": "Votre rythme est encore en cours d'analyse : {counted} sur {total} nuits libres. Un chronotype a besoin de quelques matins sans réveil pour avoir du sens.",
"advancedToggle": "Avancé",
"socialJetlagValue": "{hours} h {minutes} min de jet lag social",
- "socialJetlagCaption": "L'écart entre vos horaires de sommeil les jours travaillés et les jours libres — comme un petit décalage horaire hebdomadaire.",
+ "socialJetlagCaption": "L'écart entre vos horaires de sommeil les jours travaillés et les jours libres, comme un petit décalage horaire hebdomadaire.",
"msfScValue": "Milieu de sommeil corrigé {clock}",
"msfScCaption": "Ajusté pour le sommeil rattrapé le week-end (MSFsc).",
"dayTypeNote": "Les jours libres sont supposés être les week-ends jusqu'à l'ajout du suivi du type de jour.",
@@ -3567,7 +3724,10 @@
"endInFuture": "Il se terminerait dans le futur. Indiquez comme début l’heure à laquelle l’entraînement a commencé.",
"distanceInvalid": "Saisissez la distance sous forme de nombre, dans une plage réaliste.",
"energyInvalid": "Saisissez l’énergie active sous forme de nombre, jusqu’à 20 000 kcal."
- }
+ },
+ "editTitle": "Modifier l’entraînement",
+ "editAction": "Modifier",
+ "updated": "Entraînement mis à jour"
}
},
"bloodGlucose": {
@@ -3586,7 +3746,7 @@
"labsLink": "Résultats de laboratoire ? Saisis une HbA1c ou une glycémie à jeun issue d'une prise de sang dans Laboratoire.",
"learningTitle": "Votre glucose se découvre encore",
"learningBody": "Pour l’instant, cela fait {count} sur {days}. Avec un peu plus d’historique, le tableau se précise : continuez à enregistrer ou laissez un appareil connecté compléter.",
- "learningBodyEmpty": "Dès que quelques mesures arrivent — saisies par vous ou synchronisées depuis un appareil — votre temps dans la cible, votre moyenne et votre variabilité prennent forme ici.",
+ "learningBodyEmpty": "Dès que quelques mesures arrivent (saisies par vous ou synchronisées depuis un appareil), votre temps dans la cible, votre moyenne et votre variabilité prennent forme ici.",
"readingsCount": "{count} mesures",
"readingsCountOne": "1 mesure",
"daysCount": "{days} jours",
@@ -3624,7 +3784,7 @@
"intro": "Des indicateurs de niveau recherche pour regarder de plus près. Chacun condense vos mesures en un seul chiffre pondéré par le risque.",
"jIndex": {
"label": "Indice J",
- "help": "Un score unique mêlant votre moyenne et sa dispersion — plus il est bas, plus le contrôle est serré."
+ "help": "Un score unique mêlant votre moyenne et sa dispersion : plus il est bas, plus le contrôle est serré."
},
"lbgi": {
"label": "Risque d’hypoglycémie (LBGI)",
@@ -3741,7 +3901,7 @@
"navSkinTemperature": "Temp. peau",
"pulseWaveVelocity": {
"title": "Vitesse de l’onde de pouls",
- "description": "Votre vitesse d’onde de pouls artérielle au fil du temps, en m/s — des valeurs plus élevées suggèrent des artères plus rigides.",
+ "description": "Votre vitesse d’onde de pouls artérielle au fil du temps, en m/s. Des valeurs plus élevées suggèrent des artères plus rigides.",
"chartTitle": "Vitesse de l’onde de pouls",
"emptyState": {
"title": "Aucune donnée",
@@ -3891,7 +4051,6 @@
"navWalkingSpeed": "Vitesse de marche",
"mood": {
"logLink": "Enregistrer une humeur",
- "mentalWellbeingLink": "Faire un bilan de bien-être mental",
"heatmapTitle": "Calendrier d'humeur",
"heatmapNoEntry": "Aucune entrée",
"legendGreat": "Excellent",
@@ -3902,7 +4061,7 @@
"distributionDaysUnit": "jours",
"weekdayTitle": "Moyenne par jour de la semaine",
"weekdayNoData": "Aucune donnée",
- "weekdayBestWorst": "Meilleur jour : {best} · Jour le plus difficile : {worst}",
+ "weekdayBestWorst": "Meilleur jour : {best}, jour le plus difficile : {worst}",
"tagsTitle": "Étiquettes et déclencheurs",
"tagAvgMood": "Humeur moyenne avec cette étiquette",
"correlationsTitle": "Humeur et autres mesures",
@@ -3918,7 +4077,7 @@
"strengthModerate": "Modérée",
"strengthWeak": "Faible",
"strengthNone": "Aucune",
- "source": "{n} jours appariés · r = {r}",
+ "source": "{n} jours appariés, r = {r}",
"emptyTitle": "Pas encore assez de données appariées",
"emptyDescription": "Enregistrez l'humeur et cette mesure les mêmes jours pour voir une corrélation.",
"pendingDescription": "{n} jours appariés sur {needed}. Une corrélation apparaît dès qu'il y a assez de jours pour distinguer un motif du hasard.",
@@ -3935,7 +4094,7 @@
"inTargetCaption": "des derniers jours dans votre plage de bonne humeur",
"narrative": {
"title": "Ce qui ressort",
- "weekdayDip": "Votre humeur baisse surtout le {weekday} — {delta} sous votre semaine.",
+ "weekdayDip": "Votre humeur baisse surtout le {weekday}, {delta} sous votre semaine.",
"weekdayPeak": "Le {weekday} est toujours votre meilleur jour, avec une moyenne de {value}.",
"trendUp": "Votre humeur progresse sur les {days} derniers jours (+{delta} par rapport à la période précédente).",
"trendDown": "Votre humeur recule sur les {days} derniers jours (-{delta} par rapport à la période précédente).",
@@ -3944,7 +4103,7 @@
"tagLift": "Les jours marqués « {tag} » dépassent votre humeur générale de {delta} en moyenne.",
"tagDrop": "Les jours marqués « {tag} » sont {delta} sous votre moyenne.",
"inTarget": "Vous avez été dans votre plage de bonne humeur {pct} % des 30 derniers jours.",
- "streak": "Vous avez noté votre humeur {days} jours d'affilée — continuez.",
+ "streak": "Vous avez noté votre humeur {days} jours d'affilée. Continuez.",
"timeOfDay": "Vous vous sentez mieux le {bucket}, en moyenne {value}."
},
"timeOfDay": {
@@ -3977,7 +4136,7 @@
"crosstab": {
"title": "Tags et vos métriques de santé",
"description": "Comment une métrique de santé diffère les jours où vous avez ajouté chaque tag, par rapport aux jours sans.",
- "pairLabel": "{tag} · {metric}",
+ "pairLabel": "{metric} les jours avec {tag}",
"metricActiveEnergy": "Énergie active",
"metricSleepDuration": "Sommeil",
"metricNextDayRecovery": "Récupération le lendemain",
@@ -3994,7 +4153,7 @@
"factorCrosstab": {
"title": "Vos facteurs notés et vos constantes",
"description": "Comment une constante diffère les jours où vous avez noté un facteur bas par rapport aux jours où vous l'avez noté haut.",
- "pairLabel": "{factor} · {metric}",
+ "pairLabel": "{metric} les jours avec {factor}",
"metricSleepDuration": "Sommeil",
"metricSteps": "Pas",
"metricRestingHeartRate": "Fréquence cardiaque au repos",
@@ -4020,7 +4179,7 @@
"moodDown": "Un {factor} plus élevé tend à coïncider avec une moins bonne humeur le lendemain dans vos données.",
"outcomeUp": "Une meilleure humeur tend à coïncider avec un {factor} plus élevé le lendemain dans vos données.",
"outcomeDown": "Une meilleure humeur tend à coïncider avec un {factor} plus bas le lendemain dans vos données.",
- "stat": "{n} jours appariés · r = {r} · q = {q}",
+ "stat": "{n} jours appariés, r = {r}, q = {q}",
"statLabel": "Comment ceci est calculé",
"explainerLabel": "Comment ceci est calculé"
},
@@ -4054,8 +4213,8 @@
"deviationAbove": "Ta propre évaluation se situe {amount} points au-dessus de cette plage.",
"deviationBelow": "Ta propre évaluation se situe {amount} points en dessous de cette plage.",
"weightedTitle": "Ce qui a le plus pesé dans la comparaison de cette journée",
- "weightedUp": "{label} — comptait vers une valeur attendue plus élevée",
- "weightedDown": "{label} — comptait vers une valeur attendue plus basse",
+ "weightedUp": "{label} : comptait vers une valeur attendue plus élevée",
+ "weightedDown": "{label} : comptait vers une valeur attendue plus basse",
"caveat": "C'est une comparaison avec tes propres journées passées, pas une affirmation sur la manière dont une journée s'est déroulée.",
"learning": "Phase d'apprentissage : {entries} journées sur {threshold} enregistrées. La comparaison apparaît une fois qu'il y en a assez.",
"noPattern": "Dans les {entries} journées enregistrées jusqu'ici, rien n'est assez régulier pour comparer une journée. Quand tu notes aussi le contexte d'une journée, tu en rends davantage comparable.",
@@ -4064,15 +4223,15 @@
},
"derived": {
"coverage": {
- "summary": "{present} entrées sur {required} · {percent}%",
+ "summary": "{present} entrées sur {required} ({percent}%)",
"ratioLabel": "{present}/{required}",
"historyDays": "Basé sur {count} jours d'historique",
"missing": "Enregistre ceci pour affiner la valeur : {list}",
- "summaryLabelled": "{label} · {percent}%"
+ "summaryLabelled": "{label} ({percent}%)"
},
"scoreRing": {
"aria": "Score {score} sur 100, {band}",
- "ariaProvisional": "Score pas encore disponible — données insuffisantes",
+ "ariaProvisional": "Score pas encore disponible : données insuffisantes",
"provisionalCaption": "Données encore insuffisantes",
"band": {
"green": "bon",
@@ -4086,7 +4245,7 @@
"inputsLabel": "Entrées",
"noInputs": "Aucune entrée ne soutient encore cette valeur.",
"methodLabel": "Méthode",
- "window": "Sur tes {days} derniers jours · données {source}",
+ "window": "Sur tes {days} derniers jours de données {source}",
"asOf": "au {date}",
"standardLink": "Norme : {name}"
},
@@ -4096,7 +4255,7 @@
"loadingLabel": "Chargement de vos constantes",
"loadError": "Impossible de charger vos constantes pour le moment.",
"infoLabel": "Comment ce résultat est-il calculé ?",
- "building": "Construction de votre plage habituelle — {count} sur {target} jours",
+ "building": "Construction de votre plage habituelle : {count} sur {target} jours",
"typicalRange": "Habituel pour vous : {low}–{high}",
"fitnessYounger": "Forme cardio d'une personne ~{years} ans plus jeune",
"fitnessOlder": "Forme cardio ~{years} ans au-dessus de votre âge",
@@ -4128,9 +4287,8 @@
"anatomy": {
"outOf": "/100",
"contributorsLabel": "Facteurs",
- "contributorValue": "{value} · {weight}% du poids",
+ "contributorValue": "{value}, pondération {weight}%",
"contributorMissing": "aucune donnée",
- "disclaimer": "Indicatif — pas une évaluation clinique.",
"insufficient": "Pas encore assez de données pour l’évaluer. Continuez à enregistrer et cela apparaîtra ici."
},
"composite": {
@@ -4149,15 +4307,14 @@
"READINESS": {
"title": "Disponibilité",
"description": "Un indicateur quotidien de bien-être selon la position de vos signaux par rapport à votre propre référence.",
- "method": "Un mélange de 0–100 entre l’écart de la fréquence cardiaque au repos, l’écart de la VFC (SDNN), la qualité du sommeil, la stabilité de la fréquence respiratoire et la stabilité de l’humeur — chacun mesuré par rapport à votre référence personnelle. Les poids sont renormalisés sur les composantes que vos données étayent. Un indicateur de bien-être, pas un score de récupération ni clinique.",
+ "method": "Un mélange de 0–100 entre l’écart de la fréquence cardiaque au repos, l’écart de la VFC (SDNN), la qualité du sommeil, la stabilité de la fréquence respiratoire et la stabilité de l’humeur, chacun mesuré par rapport à votre référence personnelle. Les poids sont renormalisés sur les composantes que vos données étayent. Un indicateur de bien-être, pas un score de récupération ni clinique.",
"component": {
"rhr": "Fréquence cardiaque au repos",
"hrv": "VFC (SDNN)",
"sleep": "Sommeil",
"respiratory": "Fréquence respiratoire",
"mood": "Stabilité de l’humeur"
- },
- "caveat": "Un indicateur de bien-être, pas un score de récupération ni clinique."
+ }
},
"VITALS_BASELINE": {
"method": "Votre plage typique personnelle est la médiane de vos valeurs quotidiennes sur la période, ± une dispersion robuste (écart absolu médian, mis à l’échelle). Elle suit VOS mesures, pas une norme de population : une valeur dans la bande est donc typique pour vous."
@@ -4182,40 +4339,39 @@
},
"RECOVERY_SCORE": {
"method": "Un indice de récupération nocturne de 0 à 100 combinant votre VFC, votre fréquence cardiaque au repos et votre sommeil par rapport à votre référence. Plus élevé signifie mieux récupéré. Calculé une fois par nuit et seulement relu ici, jamais recalculé à la volée.",
- "caveat": "Un indicateur de bien-être descriptif, pas une mesure de récupération clinique ou de niveau d’entraînement.",
"description": "Un indice de récupération nocturne à partir de votre VFC, fréquence au repos et sommeil."
},
"STRESS_SCORE": {
"method": "Un indice de stress nocturne de 0 à 100 à partir de la forme de votre VFC au cours de la journée par rapport à votre référence. Plus élevé signifie une charge physiologique plus forte.",
- "caveat": "Un indicateur dérivé de la VFC, PAS une mesure de stress par EDA / activité électrodermale : votre montre n’a pas de capteur EDA. Descriptif, non clinique.",
"description": "Un indice de stress nocturne à partir de la forme de votre VFC sur la journée."
},
"STRAIN_SCORE": {
"method": "Un indice d’effort nocturne de 0 à 100 à partir de votre charge d’activité pondérée par la fréquence cardiaque (un modèle d’impulsion d’entraînement de type TRIMP). Plus élevé signifie une journée plus exigeante.",
- "anchorPersonal": "Calé sur votre propre charge récente des jours d’entraînement — évalué par rapport à votre effort habituel, de sorte qu’une journée vraiment difficile pour vous ressort élevée.",
+ "anchorPersonal": "Calé sur votre propre charge récente des jours d’entraînement : évalué par rapport à votre effort habituel, de sorte qu’une journée vraiment difficile pour vous ressort élevée.",
"anchorPopulation": "Évalué par rapport à une référence générale pendant que vous constituez votre historique d’entraînement ; dès que vous en avez assez, le calcul passe à votre propre effort habituel.",
- "caveat": "Un indicateur descriptif de charge d’activité, pas une mesure de charge clinique ou de niveau d’entraînement.",
- "description": "Un indice de charge d’activité nocturne à partir de votre effort pondéré par la fréquence cardiaque, relatif à votre effort habituel."
+ "description": "Un indice de charge d’activité nocturne à partir de votre effort pondéré par la fréquence cardiaque, relatif à votre effort habituel.",
+ "deviceCaption": "Effort du jour de votre appareil : {value} sur {max}",
+ "deviceSource": "Tant que HealthLog n’a pas calculé sa propre valeur pendant la nuit, la carte affiche l’effort du jour de votre appareil, ramené sur 0–100."
},
"WRIST_TEMPERATURE_BASELINE": {
- "method": "Votre température nocturne typique au poignet est la médiane de vos relevés nocturnes sur la période, ± une dispersion robuste (écart absolu médian, mis à l’échelle). Elle suit VOS relevés — la nuit dernière est affichée comme un écart par rapport à votre propre référence, pas à une norme de population.",
- "caveat": "Une simple bande d’écart personnel — descriptive, ni un signe de maladie, de fièvre ou de phase du cycle. À vous de l’interpréter."
+ "method": "Votre température nocturne typique au poignet est la médiane de vos relevés nocturnes sur la période, ± une dispersion robuste (écart absolu médian, mis à l’échelle). Elle suit VOS relevés : la nuit dernière est affichée comme un écart par rapport à votre propre référence, pas à une norme de population.",
+ "caveat": "Une simple bande d’écart personnel, descriptive, ni un signe de maladie, de fièvre ou de phase du cycle. À vous de l’interpréter."
},
"STAIR_ASCENT_SPEED_BASELINE": {
- "method": "Votre allure typique en montant les escaliers est la médiane de vos valeurs quotidiennes sur la période, ± une dispersion robuste (écart absolu médian, mis à l’échelle). La bande est construite à partir de vos données — il n’existe pas de seuil de population pour l’allure dans les escaliers, car elle dépend de la géométrie des marches et de la longueur des jambes.",
- "caveat": "Une bande personnelle de tendance de mobilité — descriptive, ni une évaluation de fragilité ni de risque de chute."
+ "method": "Votre allure typique en montant les escaliers est la médiane de vos valeurs quotidiennes sur la période, ± une dispersion robuste (écart absolu médian, mis à l’échelle). La bande est construite à partir de vos données : il n’existe pas de seuil de population pour l’allure dans les escaliers, car elle dépend de la géométrie des marches et de la longueur des jambes.",
+ "caveat": "Une bande personnelle de tendance de mobilité, descriptive, ni une évaluation de fragilité ni de risque de chute."
},
"STAIR_DESCENT_SPEED_BASELINE": {
- "method": "Votre allure typique en descendant les escaliers est la médiane de vos valeurs quotidiennes sur la période, ± une dispersion robuste (écart absolu médian, mis à l’échelle). La bande est construite à partir de vos données — il n’existe pas de seuil de population pour l’allure dans les escaliers, car elle dépend de la géométrie des marches et de la longueur des jambes.",
- "caveat": "Une bande personnelle de tendance de mobilité — descriptive, ni une évaluation de fragilité ni de risque de chute."
+ "method": "Votre allure typique en descendant les escaliers est la médiane de vos valeurs quotidiennes sur la période, ± une dispersion robuste (écart absolu médian, mis à l’échelle). La bande est construite à partir de vos données : il n’existe pas de seuil de population pour l’allure dans les escaliers, car elle dépend de la géométrie des marches et de la longueur des jambes.",
+ "caveat": "Une bande personnelle de tendance de mobilité, descriptive, ni une évaluation de fragilité ni de risque de chute."
},
"SIX_MINUTE_WALK_BAND": {
- "method": "La distance de marche de six minutes ESTIMÉE par votre appareil, située par rapport à la référence d’Enright et Sherrill pour votre âge, votre taille, votre poids et votre sexe, en pourcentage de la valeur prédite. La distance est l’estimation de l’appareil — jamais recalculée ici. Sans votre âge, taille, poids et sexe, le pourcentage est masqué et seules la distance et la tendance sont affichées.",
- "caveat": "Une estimation reformulée par rapport à une référence publiée — un indicateur de capacité fonctionnelle, ni un test de marche de six minutes clinique ni un diagnostic. La référence a été établie pour les âges de 40 à 80 ans ; en dehors de cette plage, la valeur prédite est une extrapolation."
+ "method": "La distance de marche de six minutes ESTIMÉE par votre appareil, située par rapport à la référence d’Enright et Sherrill pour votre âge, votre taille, votre poids et votre sexe, en pourcentage de la valeur prédite. La distance est l’estimation de l’appareil, jamais recalculée ici. Sans votre âge, taille, poids et sexe, le pourcentage est masqué et seules la distance et la tendance sont affichées.",
+ "caveat": "Une estimation reformulée par rapport à une référence publiée : un indicateur de capacité fonctionnelle, ni un test de marche de six minutes clinique ni un diagnostic. La référence a été établie pour les âges de 40 à 80 ans ; en dehors de cette plage, la valeur prédite est une extrapolation."
},
"TRAJECTORY": {
"method": "Une droite des moindres carrés ajustée à vos valeurs journalières récentes et projetée sur un court horizon, avec l’intervalle de prédiction classique qui s’élargit à mesure qu’il s’éloigne. Calculée uniquement lorsque la tendance récente est suffisamment marquée et que votre historique est assez long et récent ; sinon aucune ligne n’est tracée.",
- "caveat": "Une projection de votre propre tendance récente — ni une prédiction, ni un objectif, ni un pronostic clinique. Elle ne tient que si le schéma récent se poursuit ; la bande ombrée montre la vitesse à laquelle cette certitude s’estompe."
+ "caveat": "Une projection de votre propre tendance récente, ni une prédiction, ni un objectif, ni un pronostic clinique. Elle ne tient que si le schéma récent se poursuit ; la bande ombrée montre la vitesse à laquelle cette certitude s’estompe."
},
"SAME_TIME_BASELINE": {
"method": "Votre total cumulé du jour comparé à la médiane de cette même heure locale sur vos quatre dernières semaines, avec une bande robuste médiane ± MAD autour. Seuls les jours suffisamment détaillés comptent, et aucune comparaison n'apparaît avant d'en avoir au moins quatorze.",
@@ -4230,7 +4386,9 @@
"recovery": "Score de récupération",
"recoveryCrossLink": "Voir les données de récupération et d'effort de votre wearable",
"stress": "Stress",
- "strain": "Effort"
+ "strain": "Effort",
+ "historyTitle": "Évolution",
+ "historyWindow": "{days} derniers jours"
},
"coincident": {
"label": "Plusieurs constantes hors référence",
@@ -4240,31 +4398,31 @@
"allClear": "Aujourd’hui, toutes vos constantes sont dans leur plage personnelle.",
"allClearMeta": "{count} de vos constantes vérifiées par rapport à votre propre plage habituelle.",
"watch": "Aujourd’hui, une constante est hors de sa plage habituelle.",
- "watchVital": "Hors plage : {vital}. Souvent ponctuel — à surveiller ces prochains jours.",
+ "watchVital": "Hors plage : {vital}. Souvent ponctuel, à surveiller ces prochains jours.",
"firedHeadline": "Aujourd’hui, {count} de vos constantes sont hors de leur plage personnelle.",
- "factors": "Facteurs possibles — jamais une cause : un entraînement intense, un mauvais sommeil, l’alcool, l’altitude, le stress ou une maladie. C’est un repère issu de vos propres références, pas un diagnostic.",
- "building": "Construction de vos références personnelles — enregistrez quelques jours de plus et cela apparaîtra.",
+ "factors": "Facteurs possibles, jamais une cause : un entraînement intense, un mauvais sommeil, l’alcool, l’altitude, le stress ou une maladie. C’est un repère issu de vos propres références, pas un diagnostic.",
+ "building": "Construction de vos références personnelles. Enregistrez quelques jours de plus et cela apparaîtra.",
"illnessExplained": "Ces écarts reflètent probablement l’affection que vous avez consignée."
},
"trajectory": {
"cardTitle": "Projection à court terme",
- "headline": "Si ce schéma se poursuit — les {days} prochains jours",
+ "headline": "Si ce schéma se poursuit : les {days} prochains jours",
"range": "Projeté autour de {low}–{high} {unit} dans {days} jours.",
"caveat": "Une projection de votre propre tendance récente, pas une prédiction. La bande ombrée s’élargit pour montrer la vitesse à laquelle l’incertitude augmente.",
- "insufficient": "Pas encore de tendance assez nette pour projeter — continuez à enregistrer et une projection apparaîtra dès que le schéma récent sera suffisamment marqué.",
+ "insufficient": "Pas encore de tendance assez nette pour projeter. Continuez à enregistrer et une projection apparaîtra dès que le schéma récent sera suffisamment marqué.",
"daysNeeded": "Encore environ {count} jours d’enregistrement nécessaires avant qu’une projection puisse apparaître."
},
"sameTime": {
"sectionTitle": "Aujourd'hui jusqu'ici",
"sectionSubtitle": "Vos totaux cumulés face à ce qui est habituel chez vous à cette heure",
- "learning": "Apprentissage de votre journée habituelle — {count} jours sur {target}",
+ "learning": "Apprentissage de votre journée habituelle : {count} jours sur {target}",
"framing": "Habituel à {time} : {typical} {unit}",
"comparisonLabel": "Votre journée habituelle"
}
},
"cardioFitness": {
"title": "Condition physique",
- "description": "Votre VO₂ max au fil du temps — la principale mesure de la forme aérobie.",
+ "description": "Votre VO₂ max au fil du temps, la principale mesure de la forme aérobie.",
"chartTitle": "VO₂ max (mL/(kg·min))",
"emptyState": {
"title": "Pas encore de données de condition physique",
@@ -4344,7 +4502,7 @@
"navBreathingDisturbances": "Respiration",
"rhythmEvents": {
"sectionTitle": "Notifications de santé de l’appareil",
- "sectionIntro": "Notifications que votre montre a envoyées d’elle-même. HealthLog se contente de les afficher — il ne les détecte, ne les confirme ni ne les interprète.",
+ "sectionIntro": "Notifications que votre montre a envoyées d’elle-même. HealthLog se contente de les afficher : il ne les détecte, ne les confirme ni ne les interprète.",
"loadError": "Impossible de charger les notifications de votre appareil.",
"disclaimer": "Ce sont des alertes que votre appareil a produites avec sa propre détection intégrée. Elles sont affichées ici uniquement à titre informatif. Il ne s’agit pas d’une évaluation médicale de HealthLog, et HealthLog ne pose aucun diagnostic. Si une notification vous inquiète, parlez-en à votre médecin.",
"event": {
@@ -4389,9 +4547,9 @@
"detail": "Les pages de détail masquées disparaissent aussi de la navigation supérieure."
},
"customize": "Personnaliser les Analyses",
- "regenerateRateLimited": "Limite d'actualisation atteinte — réessayez plus tard.",
- "regenerateUnavailable": "Aucune nouvelle analyse reçue — réessayez plus tard.",
- "regenerateError": "Actualisation en cours — réessayez dans un instant.",
+ "regenerateRateLimited": "Limite d'actualisation atteinte. Réessayez plus tard.",
+ "regenerateUnavailable": "Aucune nouvelle analyse reçue. Réessayez plus tard.",
+ "regenerateError": "Actualisation en cours. Réessayez dans un instant.",
"regenerateSettling": "L’évaluation est encore en cours de préparation",
"regenerateSettleFailed": "L’actualisation n’a pas pu aboutir. Réessayez plus tard.",
"deviceScore": {
@@ -4413,9 +4571,9 @@
"emptyTitle": "Pas encore de données de récupération",
"empty": "Pas encore de données de récupération. Connectez un compte WHOOP, Polar ou Oura et ces scores se rempliront dès que votre appareil se synchronise.",
"block": {
- "ansChargeExplainer": "À quel point votre système nerveux autonome est rechargé pour la journée — une mesure de recharge issue de vos signaux nocturnes de cœur et de respiration.",
+ "ansChargeExplainer": "À quel point votre système nerveux autonome est rechargé pour la journée : une mesure de recharge issue de vos signaux nocturnes de cœur et de respiration.",
"dayStrainExplainer": "La charge cardiovasculaire de la journée sur l'échelle d'effort de votre appareil. Lisez-la au regard de votre récupération, pas comme un objectif en soi.",
- "workoutStrainExplainer": "À quel point une séance a été exigeante sur la même échelle d'effort — le coût cardiovasculaire de cette séance.",
+ "workoutStrainExplainer": "À quel point une séance a été exigeante sur la même échelle d'effort : le coût cardiovasculaire de cette séance.",
"cardioLoadExplainer": "Une estimation de la charge d'entraînement qui suit la charge cardiovasculaire accumulée. Comparez-la à vos propres journées récentes.",
"averageHeartRateExplainer": "Votre fréquence cardiaque moyenne sur toute la journée ou le cycle, telle que rapportée par votre wearable.",
"maxHeartRateExplainer": "Votre fréquence cardiaque maximale sur toute la journée ou le cycle, telle que rapportée par votre wearable.",
@@ -4428,7 +4586,7 @@
},
"resilience": {
"explainer": "Comment votre corps équilibre la charge quotidienne et la récupération, d'après vos signaux nocturnes des derniers jours.",
- "learning": "Votre résilience est encore en cours d'apprentissage — quelques jours de plus stabiliseront le niveau.",
+ "learning": "Votre résilience est encore en cours d'apprentissage. Quelques jours de plus stabiliseront le niveau.",
"band": {
"limited": "Limitée",
"adequate": "Adéquate",
@@ -4459,6 +4617,7 @@
"shiftUp": "{vital} a augmenté par rapport à votre normale récente.",
"shiftDown": "{vital} a diminué par rapport à votre normale récente.",
"rangeContext": "Actuellement {value}, habituellement {low}–{high}.",
+ "rangeContextTimeOfDay": "Actuellement {value}, habituellement {low}–{high} à ce moment de la journée.",
"shiftContext": "Environ {before} avant, {after} depuis."
},
"breathingScreening": {
@@ -4472,7 +4631,7 @@
"nights": "{count} nuits suivies",
"indexLine": "indice moyen récent {value}",
"eventsFlagged": "{count} événements signalés par votre appareil",
- "disclaimer": "Il s'agit d'un signal de dépistage, pas d'un diagnostic — parlez-en à un professionnel de santé."
+ "disclaimer": "Il s'agit d'un signal de dépistage, pas d'un diagnostic. Parlez-en à un professionnel de santé."
},
"labsChanges": {
"sectionTitle": "Depuis votre dernier bilan",
@@ -4495,7 +4654,7 @@
"description": "Enregistrez une mesure avec un dynamomètre à main (notez le meilleur de plusieurs serrages) et la tendance apparaîtra ici.",
"cta": "Enregistrer la force de préhension"
},
- "coachPrefill": "Je n'ai pas encore enregistré de force de préhension — que dit-elle sur ma santé musculaire, et comment la mesurer et l'améliorer ?"
+ "coachPrefill": "Je n'ai pas encore enregistré de force de préhension. Que dit-elle sur ma santé musculaire, et comment la mesurer et l'améliorer ?"
},
"pain": {
"title": "Douleur (0–10)",
@@ -4506,7 +4665,7 @@
"description": "Enregistrez votre douleur actuelle sur l'échelle 0–10 et la tendance apparaîtra ici.",
"cta": "Enregistrer un score de douleur"
},
- "coachPrefill": "Je n'ai pas encore enregistré de score de douleur — comment suivre la douleur sur l'échelle 0–10, et qu'est-ce qu'un changement significatif ?"
+ "coachPrefill": "Je n'ai pas encore enregistré de score de douleur. Comment suivre la douleur sur l'échelle 0–10, et qu'est-ce qu'un changement significatif ?"
},
"waist": {
"title": "Tour de taille",
@@ -4517,7 +4676,7 @@
"description": "Mesurez votre tour de taille à mi-chemin entre la dernière côte et l'os de la hanche, en fin d'expiration normale, et enregistrez-le ici.",
"cta": "Enregistrer le tour de taille"
},
- "coachPrefill": "Je n'ai pas encore enregistré de tour de taille — comment le mesurer correctement, et que signifie ce chiffre pour ma santé ?"
+ "coachPrefill": "Je n'ai pas encore enregistré de tour de taille. Comment le mesurer correctement, et que signifie ce chiffre pour ma santé ?"
},
"waistToHeight": {
"title": "Rapport taille-hauteur",
@@ -4528,7 +4687,7 @@
"description": "Enregistrez un rapport taille-hauteur et la tendance apparaîtra ici. Règle simple : gardez votre tour de taille sous la moitié de votre taille.",
"cta": "Enregistrer le rapport taille-hauteur"
},
- "coachPrefill": "Je n'ai pas encore enregistré de rapport taille-hauteur — pourquoi est-ce un chiffre utile, et comment le maintenir sous 0,5 ?"
+ "coachPrefill": "Je n'ai pas encore enregistré de rapport taille-hauteur. Pourquoi est-ce un chiffre utile, et comment le maintenir sous 0,5 ?"
},
"ecg": {
"sectionTitle": "Enregistrements ECG",
@@ -4569,9 +4728,9 @@
"emptyCta": "Connecter un appareil"
},
"intradayPulse": {
- "title": "Le rythme de votre journée",
- "caption": "Votre fréquence cardiaque au fil de la journée, moyennée par tranches de 10 minutes.",
- "hourlyNote": "Moyenne horaire affichée pour cette journée — les jours récents affichent le détail complet par tranches de 10 minutes.",
+ "title": "Pouls au fil de la journée",
+ "caption": "Moyenné par tranches de 10 minutes.",
+ "hourlyNote": "Moyenne horaire affichée pour cette journée. Les jours récents affichent le détail complet par tranches de 10 minutes.",
"empty": "Pas encore assez de mesures pour cette journée pour en afficher le rythme.",
"coverageOne": "Basé sur 1 mesure sur {hours}h.",
"coverageMany": "Basé sur {count} mesures sur {hours}h.",
@@ -4580,10 +4739,10 @@
"previousDay": "Jour précédent",
"nextDay": "Jour suivant",
"tension": {
- "morning": "Une période élevée au repos ce matin — peut-être un peu de tension.",
- "afternoon": "Une période élevée au repos cet après-midi — peut-être un peu de tension.",
- "evening": "Une période élevée au repos ce soir — peut-être un peu de tension.",
- "night": "Une période élevée au repos cette nuit — peut-être un peu de tension."
+ "morning": "Une période élevée au repos ce matin, peut-être un peu de tension.",
+ "afternoon": "Une période élevée au repos cet après-midi, peut-être un peu de tension.",
+ "evening": "Une période élevée au repos ce soir, peut-être un peu de tension.",
+ "night": "Une période élevée au repos cette nuit, peut-être un peu de tension."
}
},
"navEcg": "ECG",
@@ -4594,6 +4753,19 @@
"settingsBody": "HealthLog fonctionne entièrement sans IA. Connectez un fournisseur ci-dessous si vous voulez un briefing quotidien, de courtes notes sur vos mesures et le Coach.",
"action": "Configurer l'IA",
"dismiss": "Pas maintenant"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Je n’ai pas encore indiqué ma taille. Pourquoi l’IMC compte-t-il, et que dois-je savoir avant de la renseigner ?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Je n’ai encore ajouté aucun médicament. Que dois-je savoir avant de suivre ici l’observance ?"
+ },
+ "scoreHistory": {
+ "healthTitle": "Évolution du score de santé",
+ "healthDescription": "Ton score de santé jour après jour, tel qu’il s’affichait chaque jour.",
+ "emptyDescription": "Aucun score n’a encore été enregistré sur cette période.",
+ "usualRange": "En grisé : ta plage habituelle, de {lo} à {hi}.",
+ "seam": "La ligne s’interrompt là où ce qui compte pour le score a changé."
}
},
"thresholds": {
@@ -4623,14 +4795,15 @@
"metricBodyFat": "Masse grasse",
"metricSleep": "Durée du sommeil",
"metricSteps": "Pas/jour",
- "metricGlucoseFasting": "Glycémie — à jeun",
- "metricGlucosePostprandial": "Glycémie — postprandiale",
- "metricGlucoseRandom": "Glycémie — indifférente",
- "metricGlucoseBedtime": "Glycémie — avant le coucher",
+ "metricGlucoseFasting": "Glycémie (à jeun)",
+ "metricGlucosePostprandial": "Glycémie (postprandiale)",
+ "metricGlucoseRandom": "Glycémie (indifférente)",
+ "metricGlucoseBedtime": "Glycémie (avant le coucher)",
"metricBodyWater": "Eau corporelle",
"metricBoneMass": "Masse osseuse",
"metricOxygenSaturation": "Saturation en oxygène (SpO₂)",
- "unsetExplanation": "Pas de valeur personnelle — la valeur par défaut calculée s’applique."
+ "unsetExplanation": "Pas de valeur personnelle : la valeur par défaut calculée s’applique.",
+ "cardTitle": "Plages par métrique"
},
"onboarding": {
"errorGeneric": "Une erreur est survenue à la fin de la configuration. Réessaie.",
@@ -4643,11 +4816,11 @@
"steps": {
"dashboardOverview": {
"title": "Votre tableau de bord",
- "body": "Le tableau de bord rassemble tous les modules d’un coup d’œil. Appuyez longuement sur une tuile pour la réorganiser ou la masquer (Réglages → Disposition du tableau de bord). Une tuile ne vaut que par les données derrière elle — les modules suivants les remplissent."
+ "body": "Le tableau de bord rassemble tous les modules d’un coup d’œil. Appuyez longuement sur une tuile pour la réorganiser ou la masquer (Réglages → Disposition du tableau de bord). Une tuile ne vaut que par les données derrière elle, et les modules suivants les remplissent."
},
"quickAdd": {
"title": "Saisi en quelques secondes",
- "body": "Avec « + Ajouter », saisissez n’importe quelle mesure en quelques secondes — poids, tension, pouls, glycémie. Atout caché : antidater les valeurs oubliées. Tout ce que vous saisissez ici alimente les tendances, le score santé et les analyses IA."
+ "body": "Avec « + Ajouter », saisissez n’importe quelle mesure en quelques secondes : poids, tension, pouls, glycémie. Atout caché : antidater les valeurs oubliées. Tout ce que vous saisissez ici alimente les tendances, le score santé et les analyses IA."
},
"measurements": {
"title": "Mesures et plages cibles",
@@ -4659,7 +4832,7 @@
},
"labs": {
"title": "Valeurs de laboratoire",
- "body": "Laboratoire recense les biomarqueurs avec plages de référence — hémogramme, cholestérol, HbA1c. Atout : import et export FHIR. Les valeurs hors plage réapparaissent comme preuves dans les analyses et le rapport médical."
+ "body": "Laboratoire recense les biomarqueurs avec plages de référence : hémogramme, cholestérol, HbA1c. Atout : import et export FHIR. Les valeurs hors plage réapparaissent comme preuves dans les analyses et le rapport médical."
},
"illness": {
"title": "Journal des maladies",
@@ -4675,15 +4848,15 @@
},
"mood": {
"title": "Humeur",
- "body": "Humeur saisit votre ressenti et des tags en deux secondes. Atout : vos propres tags. Les tendances d’humeur se corrèlent au sommeil, aux médicaments et au cycle — le Coach repère les schémas."
+ "body": "Humeur saisit votre ressenti et des tags en deux secondes. Atout : vos propres tags. Les tendances d’humeur se corrèlent au sommeil, aux médicaments et au cycle, et le Coach repère les schémas."
},
"insights": {
"title": "Analyses",
- "body": "Analyses, c’est le cœur : une analyse dynamique et fondée sur des preuves plutôt que des conseils génériques — chaque affirmation pointe vos données réelles. Caché : la disposition est personnalisable. Analyses réunit tout ce que les modules ci-dessus fournissent."
+ "body": "Analyses, c’est le cœur : une analyse dynamique et fondée sur des preuves plutôt que des conseils génériques, où chaque affirmation pointe vos données réelles. Caché : la disposition est personnalisable. Analyses réunit tout ce que les modules ci-dessus fournissent."
},
"coach": {
"title": "Coach",
- "body": "Le Coach répond à vos questions sur vos données en dialogue, en utilisant antécédents, médicaments et résultats comme contexte. Vous choisissez le fournisseur IA (votre clé ou local). Il ne recalcule rien — il lit les mêmes chiffres que le tableau de bord."
+ "body": "Le Coach répond à vos questions sur vos données en dialogue, en utilisant antécédents, médicaments et résultats comme contexte. Vous choisissez le fournisseur IA (votre clé ou local). Il ne recalcule rien : il lit les mêmes chiffres que le tableau de bord."
},
"integrations": {
"title": "Intégrations",
@@ -4691,15 +4864,15 @@
},
"export": {
"title": "Rapport médical",
- "body": "Le rapport médical regroupe mesures, médicaments, valeurs de laboratoire et examens en PDF ou FHIR pour le rendez-vous. Tous les modules convergent ici — un rapport complet ne vient que de données bien tenues."
+ "body": "Le rapport médical regroupe mesures, médicaments, valeurs de laboratoire et examens en PDF ou FHIR pour le rendez-vous. Tous les modules convergent ici : un rapport complet ne vient que de données bien tenues."
},
"achievements": {
"title": "Réussites",
- "body": "Les réussites récompensent la régularité — séries et jalons. Une petite boucle de motivation qui aide à tenir bon."
+ "body": "Les réussites récompensent la régularité : séries et jalons. Une petite boucle de motivation qui aide à tenir bon."
},
"wrapUp": {
"title": "C’est la fin de la visite",
- "body": "Cette visite guidée ne s'affiche qu'une fois — elle ne se rouvrira pas d'elle-même. Tout ce qu'elle a montré reste en place, et vous pouvez activer ou désactiver des fonctionnalités à tout moment dans Réglages → Modules."
+ "body": "Cette visite guidée ne s'affiche qu'une fois et ne se rouvrira pas d'elle-même. Tout ce qu'elle a montré reste en place, et vous pouvez activer ou désactiver des fonctionnalités à tout moment dans Réglages → Modules."
}
}
},
@@ -4730,7 +4903,7 @@
"title": "Tout est prêt",
"body": "Bienvenue à bord. Votre tableau de bord vous attend.",
"returnCta": "Ouvrir le tableau de bord",
- "learning": "Certaines analyses — glycémie, rythme de sommeil, tendances de tension, cycle — s'affinent au cours de la première semaine ou deux à mesure que vos données s'accumulent. Des tuiles vides signifient simplement que nous apprenons encore votre référence.",
+ "learning": "Certaines analyses (glycémie, rythme de sommeil, tendances de tension, cycle) s'affinent au cours de la première semaine ou deux à mesure que vos données s'accumulent. Des tuiles vides signifient simplement que nous apprenons encore votre référence.",
"connectCta": "Connecter un appareil",
"logCta": "Enregistrez votre première mesure",
"importCta": "Importer un historique existant",
@@ -4753,7 +4926,7 @@
"conditionsHint": "Aide HealthLog à replacer vos mesures dans leur contexte plutôt que de signaler l'attendu.",
"allergiesLabel": "Allergies ou intolérances",
"allergiesPlaceholder": "p. ex. pénicilline, lactose",
- "privacyNote": "Stocké chiffré et utilisé uniquement pour un meilleur contexte. Nous ne demandons pas vos médicaments ici — ajoutez-les quand vous voulez depuis la page Médicaments."
+ "privacyNote": "Stocké chiffré et utilisé uniquement pour un meilleur contexte. Nous ne demandons pas vos médicaments ici : ajoutez-les quand vous voulez depuis la page Médicaments."
},
"ai": {
"panelTitle": "Voyez à quoi ressemblent les insights",
@@ -4762,18 +4935,18 @@
"sampleTitle": "Briefing quotidien",
"sampleTag": "Exemple",
"sampleAriaLabel": "Exemple de briefing quotidien",
- "sampleBody": "Poids en baisse de 1,2 kg en deux semaines. Pouls au repos stable à 58 bpm. Tension dans la plage 6 jours sur 7 — la seule valeur élevée a suivi un dîner tardif et salé.",
- "sampleCaption": "Exemple uniquement — pas vos données. Voilà à quoi ressemble un briefing une fois les insights activés.",
+ "sampleBody": "Poids en baisse de 1,2 kg en deux semaines. Pouls au repos stable à 58 bpm. Tension dans la plage 6 jours sur 7 ; la seule valeur élevée a suivi un dîner tardif et salé.",
+ "sampleCaption": "Exemple uniquement, pas vos données. Voilà à quoi ressemble un briefing une fois les insights activés.",
"ladderTitle": "Trois façons de l'activer",
- "ladderLocalTitle": "Modèle local — les requêtes d’IA restent sur votre réseau",
- "ladderLocalBody": "Pointez HealthLog vers Ollama ou LM Studio sur votre propre machine. Gratuit et totalement privé — le choix calme par défaut.",
+ "ladderLocalTitle": "Modèle local : les requêtes d’IA restent sur votre réseau",
+ "ladderLocalBody": "Pointez HealthLog vers Ollama ou LM Studio sur votre propre machine. Gratuit et totalement privé, le choix calme par défaut.",
"ladderByokTitle": "Apportez votre clé",
"ladderByokBody": "Utilisez votre propre clé OpenAI ou Anthropic. Elle fonctionne avec votre compte et votre facturation, et le niveau API exclut vos données de l'entraînement par défaut.",
"ladderOauthTitle": "Connectez-vous avec un abonnement",
- "ladderOauthBody": "Reliez un compte ChatGPT — sans coût par jeton, mais les conditions du niveau grand public peuvent utiliser le contenu pour l'entraînement : c'est donc un choix délibéré, jamais l'option par défaut pour vos données de santé.",
- "keylessLine": "HealthLog est pleinement utile sans IA. Vos tuiles, graphiques, tendances et exports fonctionnent tous — l'IA n'ajoute que la lecture en langage clair par-dessus.",
+ "ladderOauthBody": "Reliez un compte ChatGPT, sans coût par jeton, mais les conditions du niveau grand public peuvent utiliser le contenu pour l'entraînement : c'est donc un choix délibéré, jamais l'option par défaut pour vos données de santé.",
+ "keylessLine": "HealthLog est pleinement utile sans IA. Vos tuiles, graphiques, tendances et exports fonctionnent tous ; l'IA n'ajoute que la lecture en langage clair par-dessus.",
"setupCta": "Configurez-le (environ une minute)",
- "sharedKeyNote": "Cette instance fournit déjà un fournisseur d'IA partagé : les insights fonctionnent pour vous dès maintenant — aucune configuration nécessaire.",
+ "sharedKeyNote": "Cette instance fournit déjà un fournisseur d'IA partagé : les insights fonctionnent pour vous dès maintenant, aucune configuration nécessaire.",
"offer": {
"title": "Cette instance peut rédiger votre résumé en langage clair.",
"body": "L’exploitant met à disposition un fournisseur qui a fonctionné au cours des dernières 24 heures. Une seule touche active le langage clair ; vos données partent alors vers ce seul fournisseur et vers personne d’autre.",
@@ -4990,7 +5163,7 @@
"profileDescription": "Taille, date de naissance et sexe débloquent des objectifs personnalisés.",
"profileCta": "Ouvrir le profil",
"measurementTitle": "Saisir la première mesure",
- "measurementDescription": "Une mesure suffit à activer la tuile — la tendance se dessine à partir de la 5ᵉ mesure.",
+ "measurementDescription": "Une mesure suffit à activer la tuile ; la tendance se dessine à partir de la 5ᵉ mesure.",
"measurementCta": "Ajouter une mesure",
"medicationTitle": "Ajouter un médicament",
"medicationDescription": "Suis l’observance et reçois des rappels au moment où tu en as besoin.",
@@ -4999,7 +5172,7 @@
"dataSourceDescription": "Synchronisez poids, tension, sommeil et plus depuis Withings, WHOOP, Oura, Polar, Nightscout, Fitbit ou Apple Health.",
"dataSourceCta": "Connecter",
"notificationsTitle": "Configurer les notifications",
- "notificationsDescription": "Choisis comment HealthLog te prévient — Telegram, ntfy ou Web Push.",
+ "notificationsDescription": "Choisis comment HealthLog te prévient : Telegram, ntfy ou Web Push.",
"notificationsCta": "Ouvrir les notifications",
"insightsTitle": "Activer les insights (facultatif)",
"insightsDescription": "Ajoutez un modèle local ou votre propre clé IA pour débloquer le briefing quotidien et Coach.",
@@ -5088,11 +5261,6 @@
"brief": "Bref",
"default": "Par défaut",
"detailed": "Détaillé",
- "defaultWindow": "Période par défaut",
- "last7days": "7 derniers jours",
- "last30days": "30 derniers jours",
- "last90days": "90 derniers jours",
- "allTime": "Depuis toujours",
"excludedMetrics": "Mesures exclues",
"dataClusters": "Groupes de données",
"customClusters": "Utiliser une sélection personnalisée de groupes",
@@ -5152,12 +5320,10 @@
"sections": {
"account": {
"title": "Compte",
- "description": "Profil, mot de passe, clés d’accès et votre visite guidée.",
"subtitle": "Votre profil, votre identité et vos identifiants."
},
"integrations": {
"title": "Intégrations",
- "description": "Services connectés, canaux de diffusion et priorité des sources au même endroit.",
"subtitle": "Connectez et gérez vos sources de données et appareils.",
"connectionsHeading": "Connexions",
"channelsHeading": "Canaux de diffusion",
@@ -5166,16 +5332,15 @@
},
"notifications": {
"title": "Notifications",
- "description": "Tous les rappels au même endroit — quoi et quand, et où c'est envoyé.",
"subtitle": "Choisissez les rappels que vous recevez et où ils sont envoyés.",
"remindersHeading": "Rappels",
"matrixLink": "Notifications par canal",
- "channelsLink": "Canaux de diffusion"
+ "channelsLink": "Canaux de diffusion",
+ "displayHeading": "Sur ton téléphone"
},
"layout": {
- "title": "Apparence",
- "description": "Choisissez l'apparence et l'agencement du tableau de bord, des analyses et de vos modules de suivi.",
- "backToHub": "Retour à Apparence",
+ "title": "Disposition",
+ "backToHub": "Retour à Disposition",
"dashboard": {
"title": "Tableau de bord",
"description": "Disposition et ordre des tuiles."
@@ -5184,7 +5349,7 @@
"title": "Analyses",
"description": "Ordre de la vue d'ensemble et des onglets de navigation."
},
- "subtitle": "Personnalisez l'apparence du tableau de bord, des analyses et des modules de suivi.",
+ "subtitle": "Choisissez ce qu'affichent le tableau de bord, les analyses et les modules de suivi, et dans quel ordre.",
"medications": {
"title": "Médicaments",
"description": "Vue en liste et ordre manuel."
@@ -5204,6 +5369,10 @@
"mood": {
"title": "Étiquettes d’humeur",
"description": "Groupes, étiquettes et étiquettes archivées du sélecteur d’humeur."
+ },
+ "documents": {
+ "title": "Documents",
+ "description": "Affichage et disposition des mois."
}
},
"dashboard": {
@@ -5211,15 +5380,13 @@
},
"thresholds": {
"title": "Objectifs",
- "description": "Par métrique : votre propre plage cible pour des valeurs saines.",
"subtitle": "Définissez vos plages cibles personnelles par mesure."
},
"sources": {
"title": "Priorité des sources",
- "description": "Choisissez quelle source prévaut quand plusieurs enregistrent la même métrique le même jour.",
"integrationsHint": "Connectez ou déconnectez des sources de données sur la page Intégrations.",
"integrationsHintLink": "Ouvrir les intégrations",
- "cardTitle": "Priorité des sources par métrique",
+ "cardTitle": "Classement par métrique",
"help": "Si plusieurs sources fournissent la même valeur le même jour, la source en tête compte comme canonique.",
"resetDefaults": "Rétablir les valeurs par défaut",
"resetDefaultsTitle": "Réinitialiser l’ordre des sources ?",
@@ -5232,7 +5399,7 @@
"resetSuccess": "Priorité des sources rétablie aux valeurs par défaut",
"perMetricToggle": "Réglages par métrique ({count})",
"perMetricHelp": "Métriques que vous avez personnalisées par rapport à l’ordre par défaut. Utilisez cette liste pour rétablir une métrique donnée à sa valeur par défaut sans toucher aux autres.",
- "perMetricEmpty": "Aucun réglage par métrique — chaque métrique utilise l’ordre global ci-dessus.",
+ "perMetricEmpty": "Aucun réglage par métrique : chaque métrique utilise l’ordre global ci-dessus.",
"resetMetric": "Réinitialiser",
"deviceTypeToggle": "Priorité par type d’appareil ({count})",
"deviceTypeHelp": "Au sein d’une source, quel appareil l’emporte. Lorsqu’une Apple Watch et un iPhone enregistrent tous deux les pas, le type d’appareil le mieux classé compte comme la lecture canonique.",
@@ -5257,7 +5424,8 @@
"respiratoryRate": "Fréquence respiratoire",
"recovery": "Récupération",
"stress": "Stress",
- "sleepDebt": "Dette de sommeil"
+ "sleepDebt": "Dette de sommeil",
+ "hrvRmssd": "VFC (RMSSD)"
},
"sourceLabels": {
"WITHINGS": "Withings",
@@ -5274,7 +5442,8 @@
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava",
- "EXTERNAL": "Appareil externe"
+ "EXTERNAL": "Appareil externe",
+ "HEALTH_CONNECT": "Health Connect"
},
"deviceLabels": {
"watch": "Montre",
@@ -5296,26 +5465,23 @@
"moduleHintLink": "Gérer les modules",
"detail": "Lecture seule : l'app source reste propriétaire des valeurs."
},
- "helpDetail": "Les autres restent enregistrées pour la piste d’audit."
+ "helpDetail": "Les autres restent enregistrées pour la piste d’audit. Seules les sources connectées ou ayant fourni des données sont listées.",
+ "noneInUse": "Aucune source à classer pour l'instant. Connectez-en une dans Intégrations ou saisissez une valeur."
},
"ai": {
"title": "Fournisseur d'IA",
- "description": "Fournisseur, modèle et clé API pour les analyses, ainsi que votre contexte « à propos de moi ».",
"subtitle": "Choisissez votre fournisseur d'IA et utilisez votre propre clé."
},
"api": {
- "title": "API & Tokens",
- "description": "Jetons Bearer pour vos propres scripts et applis — enregistrez des mesures ou des prises de médicaments depuis n’importe où.",
+ "title": "API et jetons",
"subtitle": "Gérez les points d'accès API et les jetons d'accès."
},
"advanced": {
"title": "Avancé",
- "description": "La sortie : effacer définitivement toutes les données de santé ou fermer le compte.",
"subtitle": "Réinitialisation des données et suppression du compte."
},
"export": {
"title": "Export et import",
- "description": "Téléchargez vos données en CSV ou en sauvegarde JSON complète, ou importez depuis Apple Santé, JSON ou CSV. Le compte rendu médical se trouve désormais dans Dossier de santé.",
"otherOptionsHeading": "Autres options d'export",
"downloadFailed": "Échec du téléchargement de l'export. Veuillez réessayer.",
"actions": {
@@ -5342,7 +5508,7 @@
"fullBackup": {
"title": "Sauvegarde complète",
"description": "L'ensemble de votre dossier dans un fichier JSON.",
- "scopeNote": "Les métadonnées des documents et le résumé IA, s'il existe, sont inclus en texte clair ; les fichiers originaux des documents ainsi que les tracés GPS/données d'échantillons des entraînements ne le sont pas — téléchargez-les séparément depuis le coffre de documents ou l'export des entraînements.",
+ "scopeNote": "Les métadonnées des documents et le résumé IA, s'il existe, sont inclus en texte clair ; les fichiers originaux des documents ainsi que les tracés GPS/données d'échantillons des entraînements ne le sont pas. Téléchargez-les séparément depuis le coffre de documents ou l'export des entraînements.",
"encryptToggle": "Chiffrer avec une phrase secrète",
"passphrasePlaceholder": "Phrase secrète (12 caractères min.)",
"passphraseHelp": "Dérive la clé de l'archive avec Argon2id et la scelle avec AES-256-GCM. Enregistrez le fichier avec l'extension .hlx.",
@@ -5371,8 +5537,11 @@
"failureInterrupted": "L'import a été interrompu avant la fin, généralement par un redémarrage du serveur ou un manque de mémoire. Vos données sont en sécurité ; renvoyez l'archive pour réessayer.",
"failureInsufficientMemory": "La limite de mémoire du serveur est trop basse pour une archive de cette taille. Demandez à la personne qui administre le serveur d'augmenter la limite de mémoire de Node.js (NODE_OPTIONS=--max-old-space-size), puis renvoyez l'archive.",
"rowsImported": "{count} lignes importées jusqu'ici",
- "doneSummary": "Terminé — {imported} enregistrements sur {read} importés, {skipped} enregistrements cliniques ignorés.",
+ "doneSummary": "Terminé : {imported} enregistrements sur {read} importés, {skipped} enregistrements cliniques ignorés.",
"doneNothing": "Rien n'a été importé de cette archive : 0 enregistrement sur {read} est arrivé.",
+ "writtenByHealthLogOne": "{count} enregistrement que HealthLog a écrit lui-même dans Santé a été ignoré, car il figure déjà dans vos données.",
+ "writtenByHealthLogOther": "{count} enregistrements que HealthLog a écrits lui-même dans Santé ont été ignorés, car ils figurent déjà dans vos données.",
+ "writtenByHealthLogFew": "{count} enregistrements que HealthLog a écrits lui-même dans Santé ont été ignorés, car ils figurent déjà dans vos données.",
"ecgResult": "ECG : {discovered} détectés, {imported} importés, {updated} mis à jour, {skipped} ignorés et {failed} échoués. Les erreurs d’ECG n’annulent pas les enregistrements compatibles importés depuis export.xml.",
"estimateWarning": "Les totaux cumulés de {count} jour(s) ont été estimés à partir de la source d’export la plus élevée. Les statistiques Apple Santé natives restent la référence.",
"phase": {
@@ -5381,12 +5550,12 @@
"parsing": "Lecture de l'export…",
"upserting": "Import des enregistrements…"
},
- "skipUnsupported": "{count} échantillons ignorés — des types de données que HealthLog n'importe pas.",
+ "skipUnsupported": "{count} échantillons ignorés : des types de données que HealthLog n'importe pas.",
"skipRefused": "{count} échantillons n'ont pas pu être écrits (illisibles, hors plage ou échec de l'enregistrement).",
"skipBreakdown": "Afficher le détail ({count} types)",
"skipBreakdownMore": "+ {count} autres types",
"cycleSummary": "{samples} échantillons de cycle fusionnés en {days} journaux quotidiens.",
- "cycleModuleOff": "{count} échantillons de cycle ignorés — le suivi du cycle est désactivé pour ce compte.",
+ "cycleModuleOff": "{count} échantillons de cycle ignorés, car le suivi du cycle est désactivé pour ce compte.",
"cycleDaysFailed": "{count} jours de cycle n'ont pas pu être écrits : {reason}"
},
"json": {
@@ -5471,8 +5640,8 @@
"failed": "L'import a échoué. Rien n'a été écrit.",
"columnsHeading": "Ce que devient chaque colonne",
"jsonOnly": "JSON uniquement",
- "previewLine": "Lignes lues : {rows} · seraient importées : {queued} · refusées : {refused}",
- "submittedLine": "Lignes lues : {rows} · en file d'import : {queued}",
+ "previewLine": "{rows} lignes lues, {queued} seraient importées, {refused} refusées",
+ "submittedLine": "{rows} lignes lues, {queued} en file d'import",
"unmatched": "Aucun médicament enregistré ne correspond à : {names}",
"ambiguous": "Correspond à plusieurs médicaments : {names}",
"mirrored": "Copié depuis une autre application, aucune prise ne peut y être ajoutée : {names}",
@@ -5508,18 +5677,59 @@
"tooManyRows": "Ce fichier contient plus de lignes qu'un import peut en prendre. Découpez-le et importez les parties."
},
"detail": "Un fichier couvre tous les médicaments qu'il contient."
+ },
+ "healthConnect": {
+ "title": "Export Health Connect",
+ "description": "Importe le fichier ZIP que l’app Health Connect sur Android crée lors de l’export de tes données.",
+ "choose": "Choisir le fichier ZIP",
+ "uploading": "Envoi en cours…",
+ "uploadFailed": "L’envoi a échoué. Réessaie.",
+ "tooLarge": "Le fichier dépasse la taille acceptée par ce serveur.",
+ "doneSummary": "{count} valeurs importées.",
+ "failed": "L’import a échoué : {reason}",
+ "phase": {
+ "queued": "En attente de démarrage",
+ "unpacking": "Décompression du fichier",
+ "parsing": "Lecture des enregistrements",
+ "upserting": "Enregistrement des valeurs",
+ "done": "Terminé",
+ "failed": "Échec"
+ },
+ "steps": {
+ "export": "Sur ton téléphone Android, ouvre Health Connect, va dans Gérer les données, puis Exporter et importer, et lance une exportation.",
+ "cloud": "Health Connect enregistre l’exportation uniquement dans une app de stockage cloud comme Google Drive, Dropbox ou Nextcloud, jamais sur le téléphone lui-même. Choisis-en une.",
+ "upload": "Télécharge le fichier ZIP de ce cloud sur cet appareil et importe-le ici."
+ },
+ "dropLabel": "Dépose ici le ZIP de Health Connect",
+ "dropHint": "ou clique pour le choisir. Jusqu’à 1 Go.",
+ "fileInputLabel": "Fichier ZIP de l’exportation Health Connect",
+ "privacyNote": "Le fichier est supprimé du serveur dès la fin de l’importation. Les enregistrements des apps que tu connectes déjà directement, comme Withings ou Fitbit, sont ignorés pour que rien n’arrive en double. Importer à nouveau le même fichier ne change rien.",
+ "rateLimited": "Trop d’envois en peu de temps. Attends une minute et réessaie.",
+ "busy": "Une importation est déjà en cours pour ton compte. Attends qu’elle soit terminée.",
+ "rowsSoFar": "{count} valeurs enregistrées jusqu’ici",
+ "doneNothing": "L’importation est terminée, mais il n’y avait rien de nouveau à ajouter.",
+ "workouts": "Entraînements",
+ "leftOutConnected": "{count} enregistrements d’apps que tu connectes déjà directement ont été ignorés.",
+ "nutrientsModuleOff": "Les totaux d’eau et de nutriments n’ont pas été importés, car le module Nutriments est désactivé.",
+ "cycleModuleOff": "Les données de règles n’ont pas été importées, car le suivi du cycle est désactivé.",
+ "failure": {
+ "unsupportedVersion": "Cette exportation Health Connect est trop ancienne pour être lue. Mets à jour Health Connect sur le téléphone et exporte à nouveau.",
+ "notHealthConnect": "Ce fichier n’est pas une exportation Health Connect. Choisis le fichier ZIP enregistré par Health Connect.",
+ "unsafeSchema": "Ce fichier n’a pas la structure d’une exportation Health Connect et n’a pas été lu.",
+ "tooLarge": "L’exportation est plus volumineuse que ce que ce serveur accepte.",
+ "stagingMissing": "Le fichier envoyé n’était plus sur le serveur. Envoie-le à nouveau.",
+ "interrupted": "Un redémarrage du serveur a interrompu l’importation. Envoie le fichier à nouveau."
+ }
}
},
"subtitle": "Exportez vos données ou importez depuis une sauvegarde."
},
"about": {
"title": "À propos de cette app",
- "description": "Version, licence, liens.",
"subtitle": "Version, licence et liens du projet."
},
"sharing": {
"title": "Partage",
- "description": "Créez un lien en lecture seule pour partager votre dossier de santé avec un professionnel de santé.",
"subtitle": "Partagez un accès en lecture seule à vos données."
},
"insights": {
@@ -5554,12 +5764,10 @@
},
"gesundheitsakte": {
"title": "Dossier médical",
- "description": "Exportez votre dossier médical complet au format PDF, FHIR R4 ou en paquet ZIP combiné.",
"subtitle": "Exportez un dossier de santé complet, prêt pour le médecin."
},
"coach": {
"title": "Coach",
- "description": "Activez le Coach, le ton et la portée, les souvenirs enregistrés.",
"subtitle": "Réglez le Coach, ses suggestions proactives et sa mémoire."
},
"security": {
@@ -5568,8 +5776,7 @@
},
"privacy": {
"title": "Données et confidentialité",
- "subtitle": "Vos données, leur conservation, le chiffrement et la sécurité du compte au même endroit.",
- "description": "Export, suppression, conservation, état du chiffrement, sessions et activité de sécurité."
+ "subtitle": "Vos données, leur conservation, le chiffrement et la sécurité du compte au même endroit."
},
"mcp": {
"title": "Connecteur MCP",
@@ -5577,8 +5784,7 @@
},
"anamnesis": {
"title": "Antécédents médicaux",
- "subtitle": "Gérez vos allergies et vos antécédents familiaux.",
- "description": "Dossiers structurés des allergies et des antécédents familiaux."
+ "subtitle": "Vos antécédents, votre mode de vie, vos informations d’urgence et ce que l’IA peut lire."
},
"environment": {
"title": "Localisation et météo",
@@ -5612,7 +5818,8 @@
"add": "Ajouter {label}",
"remove": "Supprimer la période de localisation",
"removeConfirmTitle": "Retirer ce voyage ?",
- "removeConfirmBody": "Les mesures de cette période restent dans ton dossier ; seule l'entrée du voyage disparaît."
+ "removeConfirmBody": "Les mesures de cette période restent dans ton dossier ; seule l'entrée du voyage disparaît.",
+ "range": "Du {start} au {end}"
},
"backfill": {
"title": "Récupérer les jours passés",
@@ -5622,7 +5829,21 @@
"noResults": "Aucun lieu correspondant trouvé.",
"fetchFailedOne": "La dernière mise à jour météo en arrière-plan a échoué ({when}).",
"fetchFailedFew": "Les {count} dernières mises à jour météo en arrière-plan ont échoué, la plus récente {when}.",
- "fetchFailedOther": "Les {count} dernières mises à jour météo en arrière-plan ont échoué, la plus récente {when}."
+ "fetchFailedOther": "Les {count} dernières mises à jour météo en arrière-plan ont échoué, la plus récente {when}.",
+ "airQuality": {
+ "title": "Qualité de l’air et pollen",
+ "description": "Qualité de l’air, pollen et UV quotidiens pour ton lieu, issus du modèle CAMS via Open-Meteo.",
+ "toggle": "Inclure la qualité de l’air et le pollen",
+ "operatorDisabled": "La qualité de l’air est désactivée sur ce serveur.",
+ "coverage": "{count} jours avec des données de qualité de l’air",
+ "pollenEuropeOnly": "Les données de pollen ne couvrent que l’Europe.",
+ "historyProgress": "Récupération de la qualité de l’air : {done} jours sur {total}",
+ "historyComplete": "La qualité de l’air est complétée pour tous les jours passés avec des saisies que la source couvre ({total} jours). Les jours plus anciens restent vides.",
+ "attribution": "Sources de données et licences",
+ "saveError": "Le réglage n’a pas pu être enregistré.",
+ "latest": "Jour le plus récent ({date})",
+ "modelNote": "Valeurs extérieures modélisées pour ta zone, pas ton exposition personnelle."
+ }
},
"score": {
"title": "Score de santé",
@@ -5749,7 +5970,6 @@
},
"changePassword": "Changer le mot de passe",
"changePasswordDescription": "Remplacez votre mot de passe actuel par un nouveau.",
- "passwordReset": "Réinitialisation du mot de passe",
"currentPassword": "Mot de passe actuel",
"newPassword": "Nouveau mot de passe",
"confirmNewPassword": "Confirmer le nouveau mot de passe",
@@ -5794,9 +6014,9 @@
"botToken": "Jeton du bot",
"chatId": "ID du chat",
"enableNotifications": "Activer les notifications",
- "telegramStep1": "1. Créez un bot via @BotFather dans Telegram et copiez le jeton.",
- "telegramStep2": "2. Envoyez /start à votre bot pour activer le chat.",
- "telegramStep3": "3. Trouvez votre ID de chat via @userinfobot ou l’API du bot.",
+ "telegramStep1": "Créez un bot via @BotFather dans Telegram et copiez le jeton.",
+ "telegramStep2": "Envoyez /start à votre bot pour activer le chat.",
+ "telegramStep3": "Trouvez votre ID de chat via @userinfobot ou l’API du bot.",
"ntfy": "ntfy",
"ntfyDescription": "Notifications via ntfy (auto-hébergé ou ntfy.sh).",
"ntfyEnable": "Activer ntfy",
@@ -5805,7 +6025,7 @@
"ntfyAuthToken": "Jeton d’authentification (facultatif)",
"ntfyAuthTokenHint": "Nécessaire uniquement pour les sujets privés avec contrôle d’accès.",
"webhook": "Webhook",
- "webhookDescription": "Envoyer les notifications par POST vers n’importe quelle URL — Gotify, Discord, Slack, Matrix ou Home Assistant.",
+ "webhookDescription": "Notifications par POST vers une URL : Discord, Slack, Gotify, Home Assistant, n8n ou tout relais acceptant du JSON.",
"webhookEnable": "Activer le webhook",
"webhookUrl": "URL du webhook",
"webhookUrlHint": "L’URL complète pour le POST. Une adresse sur un réseau privé doit être autorisée par l’administrateur du serveur.",
@@ -5813,6 +6033,7 @@
"webhookHeaderValue": "Valeur d’en-tête (facultatif)",
"webhookFormat": "Format du message",
"webhookFormatGeneric": "JSON générique",
+ "webhookFormatGenericHint": "Collez telle quelle une URL de webhook Discord ou Slack. Home Assistant et n8n lisent les champs title, message, eventType et priority.",
"webhookFormatGotify": "Gotify",
"webhookFormatGotifyHint": "Utilisez l’URL se terminant par /message et placez le jeton de l’application dans l’en-tête X-Gotify-Key.",
"email": "E-mail",
@@ -5829,7 +6050,6 @@
"webPushSubscribed": "Notifications push activées !",
"webPushUnsubscribed": "Notifications push désactivées.",
"webPushSubscribeFailed": "Échec de l’activation",
- "kiInsightsDescription": "Facultatif : enregistrez votre clé OpenAI pour des analyses quotidiennes et automatiques dans Insights.",
"codexConnected": "ChatGPT connecté ! Insights est maintenant actif.",
"codexDisconnected": "Connexion à ChatGPT supprimée.",
"codexConnectionFailed": "Échec de la connexion à ChatGPT. Veuillez réessayer.",
@@ -5839,7 +6059,7 @@
"rawDataWarning": "Historique détaillé activé : chaque analyse envoie aussi les moyennes journalières des 90 derniers jours, les moyennes hebdomadaires de l’année précédente et les moyennes mensuelles jusqu’à 5 ans en arrière.",
"regenerateInsights": "Régénérer les rapports",
"regenerateSuccess": "Rapports régénérés",
- "regenerateRateLimit": "Veuillez patienter — vous avez atteint la limite horaire d’analyses.",
+ "regenerateRateLimit": "Veuillez patienter : vous avez atteint la limite horaire d’analyses.",
"lastGeneratedAt": "Dernière génération",
"ai": {
"chatgptConnectedBadge": "ChatGPT connecté",
@@ -5849,11 +6069,11 @@
"deviceCodeHeading": "Terminez la connexion sur chatgpt.com",
"deviceCodeStep1": "Ouvrez ce lien sur n’importe quel appareil :",
"deviceCodeStep2": "Saisissez ce code à usage unique :",
- "deviceCodeStep3": "Approuvez la connexion — cette page se met à jour automatiquement.",
+ "deviceCodeStep3": "Approuvez la connexion. Cette page se met à jour automatiquement.",
"deviceCodeCopy": "Copier",
"deviceCodeWaiting": "En attente d’approbation…",
"deviceCodeCancel": "Annuler",
- "oauthNotConfigured": "L’OAuth ChatGPT n’est pas configuré sur cette instance — utilisez plutôt votre propre clé API ci-dessous.",
+ "oauthNotConfigured": "L’OAuth ChatGPT n’est pas configuré sur cette instance. Utilisez plutôt votre propre clé API ci-dessous.",
"modelLabel": "Modèle",
"modelOptionDefault": "— Par défaut —",
"modelOptionCustom": "Personnalisé…",
@@ -5861,7 +6081,7 @@
"anthropicKeyLabel": "Clé API Anthropic",
"baseUrlLabel": "Base URL",
"localKeyLabel": "Clé API (facultative)",
- "localDescription": "Tout point de terminaison compatible OpenAI fonctionne ici — un modèle local (Ollama, LM Studio) comme une passerelle telle que LiteLLM, OpenRouter ou vLLM. L'URL de base se termine par /v1 ; la clé API est facultative.",
+ "localDescription": "Tout point de terminaison compatible OpenAI fonctionne ici : un modèle local (Ollama, LM Studio) comme une passerelle telle que LiteLLM, OpenRouter ou vLLM. L'URL de base se termine par /v1 ; la clé API est facultative.",
"savedPreview": "(enregistrée {preview})",
"savedShort": "(enregistrée)",
"saveCta": "Enregistrer",
@@ -5877,7 +6097,7 @@
"admin-openai": "OpenAI administrateur"
},
"title": "Chaîne de secours",
- "description": "Si le fournisseur principal échoue, HealthLog parcourt la chaîne dans l’ordre. Faites glisser les lignes pour les réorganiser ; utilisez l’interrupteur pour désactiver un fournisseur sans le supprimer.",
+ "description": "Si le fournisseur actif échoue, HealthLog essaie le suivant activé, dans cet ordre.",
"moveUp": "Monter",
"moveDown": "Descendre",
"removeFromChain": "Retirer de la chaîne",
@@ -5890,11 +6110,8 @@
"resetConfirmTitle": "Réinitialiser la chaîne aux valeurs par défaut ?",
"resetConfirmBody": "La chaîne revient à Codex → OpenAI → Anthropic → Local → OpenAI administrateur. Vos identifiants enregistrés ne sont pas affectés."
},
- "providerCardTitle": "Fournisseur d'IA",
"activeProviderHeading": "Fournisseur actif",
- "activeProviderBody": "Choisissez le fournisseur à utiliser en premier. Le formulaire ci-dessous configure uniquement le fournisseur sélectionné ; la chaîne de secours en bas détermine ce qui se passe en cas d’échec.",
"activeProviderLabel": "Fournisseur principal",
- "providerConfigTitle": "Configuration du fournisseur",
"providerSelect": {
"codex": "Compte ChatGPT (Codex)",
"openai": "OpenAI (votre clé API)",
@@ -5904,7 +6121,7 @@
"admin-openai": "OpenAI fourni par l’administrateur"
},
"compat": {
- "description": "Un fournisseur à part entière pour tout point de terminaison qui parle l'API /v1/chat/completions d'OpenAI : LiteLLM, OpenRouter, vLLM ou une autre passerelle. L'URL de base se termine par /v1, la clé API est facultative et le modèle est celui que votre passerelle achemine. Votre clé OpenAI n'atteint jamais ce point de terminaison — ce fournisseur a la sienne.",
+ "description": "Un fournisseur à part entière pour tout point de terminaison qui parle l'API /v1/chat/completions d'OpenAI : LiteLLM, OpenRouter, vLLM ou une autre passerelle. L'URL de base se termine par /v1, la clé API est facultative et le modèle est celui que votre passerelle achemine. Votre clé OpenAI n'atteint jamais ce point de terminaison : ce fournisseur a la sienne.",
"keyLabel": "Clé API (facultative)",
"modelHint": "Le nom de modèle attendu par votre passerelle. Laissé vide, le modèle enregistré pour vos autres fournisseurs est utilisé ; si aucun n'est défini, ce fournisseur est ignoré."
},
@@ -5940,25 +6157,24 @@
"notConfigured": "L’opérateur n’a pas configuré de clé OpenAI partagée sur cette instance."
},
"testProvider": "Tester le fournisseur actif",
- "testSuccess": "OK — {provider} ({model})",
+ "testSuccess": "OK : {provider} ({model})",
"testFailedShort": "Échec du test : {message}",
- "testUnexpectedResponse": "Échec de la connexion au fournisseur d’IA — réponse inattendue du serveur.",
- "testReasonCredentials": "Le fournisseur a rejeté les identifiants — réauthentifiez-vous dans les réglages IA.",
- "testReasonRateLimited": "Le fournisseur a limité la requête — réessayez sous peu.",
+ "testUnexpectedResponse": "Échec de la connexion au fournisseur d’IA : réponse inattendue du serveur.",
+ "testReasonCredentials": "Le fournisseur a rejeté les identifiants. Réauthentifiez-vous dans les réglages IA.",
+ "testReasonRateLimited": "Le fournisseur a limité la requête. Réessayez sous peu.",
"testReasonServerError": "Le fournisseur d’IA a renvoyé une erreur serveur.",
- "testReasonBadRequest": "Le fournisseur a rejeté la requête (HTTP {status}) — le point de terminaison a répondu ; le problème vient donc du format de la requête ou du nom du modèle, pas de la connexion.",
+ "testReasonBadRequest": "Le fournisseur a rejeté la requête (HTTP {status}). Le point de terminaison a répondu ; le problème vient donc du format de la requête ou du nom du modèle, pas de la connexion.",
"testReasonUnreachable": "Impossible de joindre le fournisseur d’IA.",
"coachMemory": {
"title": "Ce dont le Coach se souvient",
"description": "Les informations durables que vous avez confiées au Coach.",
- "empty": "Le Coach n'a encore enregistré aucune information. Au fil de vos échanges, il retient les détails durables que vous partagez — vos objectifs, vos affections, vos préférences — et vous pouvez les effacer ici à tout moment.",
+ "empty": "Le Coach n'a encore enregistré aucune information. Au fil de vos échanges, il retient les détails durables que vous partagez (vos objectifs, vos affections, vos préférences) et vous pouvez les effacer ici à tout moment.",
"learnedPrefix": "appris {when}",
"categoryPreference": "Préférences",
"categoryCondition": "Affections",
"categoryGoal": "Objectifs",
"categoryConstraint": "Contraintes",
"categoryContext": "Contexte",
- "forget": "Oublier",
"forgetAria": "Oublier cette information",
"forgetTitle": "Oublier ce fait ?",
"forgetBody": "Le Coach cesse de l’utiliser dès le message suivant. Un fait oublié ne peut pas être récupéré.",
@@ -5968,13 +6184,14 @@
"forgetAllConfirmBody": "Cela supprime toutes les informations enregistrées. Le Coach repartira de zéro lors de votre prochaine conversation. Cette action est irréversible.",
"forgetAllConfirmAction": "Tout oublier",
"cancel": "Annuler",
- "summaryNote": "Le Coach peut aussi conserver un bref résumé évolutif de chaque conversation. Il n'existe pas de commande individuelle pour cela — supprimer une conversation supprime son résumé.",
+ "summaryNote": "Le Coach peut aussi conserver un bref résumé évolutif de chaque conversation. Il n'existe pas de commande individuelle pour cela : supprimer une conversation supprime son résumé.",
"loadError": "Impossible de charger ce dont le Coach se souvient.",
"forgotToast": "Information oubliée.",
"forgotError": "Impossible d'oublier cette information. Veuillez réessayer.",
"clearedToast": "{count} informations enregistrées oubliées.",
"clearError": "Impossible d'effacer les informations enregistrées. Veuillez réessayer.",
- "detail": "Il s'en sert pour garder ses réponses pertinentes. Supprimez tout ce qu'il vaut mieux qu'il oublie."
+ "detail": "Il s'en sert pour garder ses réponses pertinentes. Supprimez tout ce qu'il vaut mieux qu'il oublie.",
+ "categoryMedication": "Médicaments"
},
"coachReminders": {
"title": "Tes rappels",
@@ -5988,7 +6205,7 @@
"markDone": "Terminé",
"dismiss": "Ignorer",
"deleteAria": "Supprimer ce rappel",
- "note": "Les rappels sont stockés chiffrés. Supprime-en un à tout moment — il disparaît de ce que le Coach peut voir.",
+ "note": "Les rappels sont stockés chiffrés. Supprime-en un à tout moment : il disparaît de ce que le Coach peut voir.",
"status": {
"proposed": "Suggéré",
"active": "Enregistré",
@@ -6035,19 +6252,19 @@
"title": "Lire les documents automatiquement avec l'IA",
"subLabel": "Les documents sont lus et indexés par votre fournisseur d'IA configuré, sans validation document par document.",
"honesty": "Lorsque cette option est activée, chaque document que vous téléversez est envoyé à votre fournisseur d'IA configuré pour être lu, décrit et indexé pour la recherche, sans confirmation par document. L'activation agit aussi rétroactivement : les documents que vous avez déjà téléversés et qui n'ont pas encore de résumé sont envoyés eux aussi, jusqu'à 200 à chaque activation. Cela se produit sur le serveur et le contenu du document quitte cette machine vers ce fournisseur. Si votre fournisseur configuré est une connexion par abonnement (un compte d'IA connecté plutôt qu'une clé API), ses propres paramètres de données s'appliquent : les fournisseurs par abonnement peuvent utiliser le contenu pour améliorer leurs modèles, et aucun accord de traitement des données ne le couvre. Les documents lus par un modèle local auto-hébergé ne quittent jamais votre serveur. Désactivé par défaut ; activez-le uniquement si vous acceptez ce compromis pour vos documents.",
- "confirm": "Compris — activer",
+ "confirm": "Compris, activer",
"cancel": "Annuler"
},
"centralCodex": {
"title": "Utiliser l'accès IA partagé du serveur",
- "subLabel": "Acheminez les requêtes IA via la connexion que l'opérateur a configurée pour ce serveur, en complément de vos propres fournisseurs comme solution de repli.",
+ "subLabel": "Les requêtes IA passent par la connexion partagée du serveur, vos propres fournisseurs servant de repli.",
"honesty": "L'accès partagé est un unique compte IA connecté que l'opérateur a relié pour tout le monde sur ce serveur, et non votre propre clé. Les requêtes que vous y faites servent par défaut à entraîner les modèles du fournisseur et aucun accord de traitement des données ne les couvre, les limites horaires et hebdomadaires du compte sont partagées avec tous les autres qui ont accepté, et vos propres fournisseurs sont essayés en premier. Ne l'activez que si vous acceptez ce compromis.",
- "confirm": "Compris — utiliser la connexion",
+ "confirm": "Compris, utiliser la connexion",
"cancel": "Annuler"
},
"consent": {
"title": "Consentement IA",
- "activeSince": "Donné le {date}. Tes données de santé peuvent être transmises au fournisseur configuré pour les fonctions IA.",
+ "activeSince": "Donné le {date} : vos données de santé peuvent être transmises au fournisseur configuré pour les fonctions IA.",
"withdrawn": "Retiré. Sauf si un autre consentement le couvre, le Coach et Insights n'utilisent plus l'accès IA fourni par ce serveur, et aucun modèle en ligne ne lit vos documents. Avec votre propre clé ou un modèle local, le Coach et Insights continuent de fonctionner.",
"withdraw": "Retirer le consentement",
"confirmBody": "Sauf si un autre consentement les couvre encore, ceci s'arrête immédiatement : le Coach et Insights lorsqu'ils utilisent l'accès IA fourni par ce serveur, et la lecture de documents par un modèle en ligne. Avec votre propre clé ou un modèle local, le Coach et Insights continuent de fonctionner. Sauf si un autre consentement couvre encore Insights, les notes qu'un modèle a écrites pour vous sont supprimées : notes d'état, briefing quotidien, résumés de période écrits par un modèle, lignes d'arrivée et notes d'entraînement. Vos conversations avec le Coach, ce que le Coach a retenu, vos plans et les résumés de vos documents sont conservés, et vous pouvez les lire ou les supprimer vous-même.",
@@ -6058,7 +6275,7 @@
"error": "Le retrait n'a pas pu être enregistré. Réessaie.",
"documentReading": {
"title": "Consentement à la lecture de documents",
- "activeSince": "Donné le {date}. Les documents, les comptes rendus d'analyses numérisés et les descriptions de médicaments peuvent être envoyés à votre service d'IA pour lecture.",
+ "activeSince": "Donné le {date} : documents, analyses numérisées et textes de médicaments peuvent être lus par votre service d'IA.",
"confirmBody": "Les documents, les comptes rendus d'analyses numérisés et les descriptions de médicaments ne sont plus envoyés pour lecture. Rien de ce que vous avez déjà n'est supprimé."
}
},
@@ -6080,7 +6297,21 @@
"title": "Plans du Coach",
"description": "Les plans convenus avec le Coach, à lire ou à supprimer.",
"open": "Ouvrir les plans"
- }
+ },
+ "reasoning": {
+ "label": "Raisonnement",
+ "hint": "Les modèles de raisonnement comme Gemma ou Qwen réfléchissent avant de répondre. Désactivé, tout le budget de réponse sert à la réponse elle-même.",
+ "options": {
+ "default": "Par défaut",
+ "none": "Désactivé",
+ "low": "Faible",
+ "medium": "Moyen",
+ "high": "Élevé"
+ },
+ "coachNote": "Dans le Coach, c'est la profondeur de réflexion des réglages du Coach qui s'applique."
+ },
+ "providerCardDescription": "Le fournisseur que HealthLog sollicite en premier, avec sa clé et son modèle.",
+ "runtimeTitle": "Vérifier et actualiser"
},
"dashboard": {
"units": {
@@ -6104,8 +6335,8 @@
"withingsClientId": "ID client",
"withingsClientSecret": "Secret client",
"withingsCredentialsSaved": "Identifiants enregistrés",
- "withingsCredentialsSavedPlaceholder": "Enregistré — saisissez-en un nouveau pour remplacer",
- "withingsCredentialsSavedPlaceholderSecret": "Enregistré — saisissez-en un nouveau pour remplacer",
+ "withingsCredentialsSavedPlaceholder": "Enregistré. Saisissez-en un nouveau pour remplacer",
+ "withingsCredentialsSavedPlaceholderSecret": "Enregistré. Saisissez-en un nouveau pour remplacer",
"withingsSaveCredentials": "Enregistrer",
"configured": "Configuré",
"withingsSync": "Synchroniser maintenant",
@@ -6120,7 +6351,7 @@
},
"withingsFullSyncResult": "{count} mesures entièrement synchronisées",
"withingsSyncFailed": "Échec de la synchronisation",
- "withingsOauthConnected": "Withings connecté — la première synchronisation démarre en arrière-plan.",
+ "withingsOauthConnected": "Withings connecté. La première synchronisation démarre en arrière-plan.",
"withingsOauthFailed": "Échec de la connexion Withings",
"withingsOauthError": {
"csrf1": "Le contrôle de sécurité a échoué car le cookie du navigateur manquait. Relancez la connexion dans le même navigateur.",
@@ -6145,8 +6376,8 @@
"whoopClientId": "Client ID",
"whoopClientSecret": "Client Secret",
"whoopCredentialsSaved": "Identifiants enregistrés",
- "whoopCredentialsSavedPlaceholder": "Enregistré — saisir de nouveaux pour remplacer",
- "whoopCredentialsSavedPlaceholderSecret": "Enregistré — saisir de nouveaux pour remplacer",
+ "whoopCredentialsSavedPlaceholder": "Enregistré. Saisir de nouveaux pour remplacer",
+ "whoopCredentialsSavedPlaceholderSecret": "Enregistré. Saisir de nouveaux pour remplacer",
"whoopSaveCredentials": "Enregistrer",
"whoopSync": "Synchroniser",
"whoopFullSync": "Synchroniser toutes les données",
@@ -6161,7 +6392,7 @@
"whoopConnect": "Se connecter avec WHOOP",
"whoopNoCredentials": "Veuillez saisir vos identifiants API ci-dessus pour connecter WHOOP.",
"whoopBackfillInProgress": "Importation de votre historique WHOOP en arrière-plan…",
- "whoopOauthConnected": "WHOOP connecté — la première synchronisation démarre en arrière-plan.",
+ "whoopOauthConnected": "WHOOP connecté. La première synchronisation démarre en arrière-plan.",
"whoopOauthFailed": "Échec de la connexion à WHOOP",
"whoopOauthError": {
"csrf1": "La vérification de sécurité a échoué car le cookie du navigateur était absent. Veuillez relancer la connexion dans le même navigateur.",
@@ -6175,14 +6406,14 @@
},
"fitbit": "Fitbit",
"fitbitTag": "Fitbit et Pixel",
- "fitbitDescription": "Connecte Fitbit pour synchroniser tes données Fitbit et Pixel Watch — poids, fréquence cardiaque et plus.",
+ "fitbitDescription": "Connecte Fitbit pour synchroniser tes données Fitbit et Pixel Watch : poids, fréquence cardiaque et plus.",
"fitbitExperimentalBadge": "Expérimental",
"fitbitCredentials": "Identifiants d'API",
"fitbitClientId": "Client ID",
"fitbitClientSecret": "Client Secret",
"fitbitCredentialsSaved": "Identifiants enregistrés",
- "fitbitCredentialsSavedPlaceholder": "Enregistré — saisissez-en de nouveaux pour remplacer",
- "fitbitCredentialsSavedPlaceholderSecret": "Enregistré — saisissez-en de nouveaux pour remplacer",
+ "fitbitCredentialsSavedPlaceholder": "Enregistré. Saisissez-en de nouveaux pour remplacer",
+ "fitbitCredentialsSavedPlaceholderSecret": "Enregistré. Saisissez-en de nouveaux pour remplacer",
"fitbitSaveCredentials": "Enregistrer",
"fitbitSync": "Synchroniser maintenant",
"fitbitFullSync": "Tout synchroniser",
@@ -6197,18 +6428,18 @@
"fitbitConnect": "Se connecter à Fitbit",
"fitbitNoCredentials": "Veuillez saisir vos identifiants d'API ci-dessus pour connecter Fitbit.",
"fitbitBackfillInProgress": "Importation de votre historique Fitbit en arrière-plan…",
- "fitbitOauthConnected": "Fitbit connecté — la première synchronisation démarre en arrière-plan.",
+ "fitbitOauthConnected": "Fitbit connecté. La première synchronisation démarre en arrière-plan.",
"fitbitOauthFailed": "Échec de la connexion à Fitbit",
"googleHealth": "Google Health",
"googleHealthTag": "Fitbit et Pixel",
"googleHealthBetaBadge": "Bêta",
- "googleHealthDescription": "Connecte Fitbit, Pixel Watch et Fitbit Air via ton compte Google — poids, fréquence cardiaque, sommeil et activité. Cela remplace la connexion Fitbit classique, que Google arrête en septembre 2026.",
+ "googleHealthDescription": "Connecte Fitbit, Pixel Watch et Fitbit Air via ton compte Google : poids, fréquence cardiaque, sommeil et activité. Cela remplace la connexion Fitbit classique, que Google arrête en septembre 2026.",
"googleHealthCredentials": "Identifiants d'API",
"googleHealthClientId": "ID client",
"googleHealthClientSecret": "Secret client",
"googleHealthCredentialsSaved": "Identifiants enregistrés",
- "googleHealthCredentialsSavedPlaceholder": "Enregistré — saisir de nouveaux pour remplacer",
- "googleHealthCredentialsSavedPlaceholderSecret": "Enregistré — saisir de nouveaux pour remplacer",
+ "googleHealthCredentialsSavedPlaceholder": "Enregistré. Saisir de nouveaux pour remplacer",
+ "googleHealthCredentialsSavedPlaceholderSecret": "Enregistré. Saisir de nouveaux pour remplacer",
"googleHealthSaveCredentials": "Enregistrer",
"googleHealthSync": "Synchroniser maintenant",
"googleHealthSyncCheckStatus": "Actualiser le statut",
@@ -6242,9 +6473,9 @@
"googleHealthConnect": "Se connecter avec Google",
"googleHealthNoCredentials": "Saisis tes identifiants d'API ci-dessus pour connecter Google Health.",
"googleHealthBackfillInProgress": "Importation de ton historique Google Health en arrière-plan…",
- "googleHealthReauthBanner": "Google a expiré la connexion — cela arrive régulièrement tant que ton application OAuth reste en mode Test. Reconnecte-toi pour reprendre la synchronisation.",
+ "googleHealthReauthBanner": "Google a expiré la connexion. Cela arrive régulièrement tant que ton application OAuth reste en mode Test. Reconnecte-toi pour reprendre la synchronisation.",
"googleHealthReconnect": "Se reconnecter",
- "googleHealthOauthConnected": "Google Health connecté — la première synchronisation démarre en arrière-plan.",
+ "googleHealthOauthConnected": "Google Health connecté. La première synchronisation démarre en arrière-plan.",
"googleHealthOauthFailed": "Échec de la connexion à Google Health",
"googleHealthOauthError": {
"csrf1": "La vérification de sécurité a échoué car le cookie du navigateur était manquant. Relance la connexion dans le même navigateur.",
@@ -6279,10 +6510,10 @@
},
"integrationPill": {
"connected": "Connecté",
- "errorReconnect": "Erreur — reconnecter",
+ "errorReconnect": "Erreur : reconnecter",
"warningServerError": "Échec de synchronisation",
"failureCount": "{count}/{threshold}",
- "parkedReconnect": "En pause — reconnecter",
+ "parkedReconnect": "En pause : reconnecter",
"notConnected": "Non connecté",
"justNow": "à l’instant",
"minutesAgo": "il y a {count} min",
@@ -6297,7 +6528,7 @@
},
"integrationFreshness": {
"title": "Données reçues",
- "summary": "{count} types de données · plus récent {relative}",
+ "summary": "{count} types de données, plus récent {relative}",
"quiet": "{count} silencieux",
"workouts": "Séances"
},
@@ -6311,31 +6542,33 @@
"fitbitViewData": "Voir votre sommeil",
"googleHealthViewData": "Voir vos pas",
"apiTokens": "Jetons API",
- "apiTokensDescription": "Consultez et révoquez les jetons API pour l’enregistrement externe des prises de médicaments.",
+ "apiTokensDescription": "Consultez et révoquez les jetons des mesures, entraînements, documents et prises de médicaments.",
"measurementsToken": {
"title": "Jetons de mesures",
"description": "Créez un jeton pour qu'une balance, une montre ou votre domotique ajoutent des mesures.",
"detail": "Le jeton n'est affiché qu'une fois.",
"scopeNote": "Il peut uniquement ajouter de nouvelles mesures à votre propre dossier : il ne peut pas lire vos données, ni modifier ou supprimer quoi que ce soit, ni créer un autre jeton. Les mesures sont signalées comme provenant d'un appareil externe, ce qui vous permet de les reconnaître plus tard, et vous pouvez toujours les corriger.",
- "createFailed": "Le jeton n'a pas pu être créé. Veuillez réessayer."
+ "createFailed": "Le jeton n'a pas pu être créé. Veuillez réessayer.",
+ "namePlaceholder": "Nom, p. ex. Home Assistant"
},
"workoutsToken": {
"title": "Jetons d'entraînements",
"description": "Créez un jeton pour qu'une passerelle de montre ou un script de synchronisation ajoutent vos entraînements.",
"detail": "Le jeton n'est affiché qu'une fois.",
"scopeNote": "Il peut uniquement ajouter de nouveaux entraînements à votre propre dossier. Il ne peut pas lire vos données, ni modifier ou supprimer quoi que ce soit, ni créer un autre jeton. Les entraînements qu'il ajoute sont signalés comme provenant d'un appareil externe, et si votre téléphone a enregistré la même séance, c'est la copie du téléphone que vous voyez.",
- "createFailed": "Le jeton n'a pas pu être créé. Veuillez réessayer."
+ "createFailed": "Le jeton n'a pas pu être créé. Veuillez réessayer.",
+ "namePlaceholder": "Nom, p. ex. Garmin"
},
"documentsToken": {
"title": "Jetons de documents",
"description": "Créez un jeton pour que Paperless-ngx, Papra ou le script d'import ajoutent des documents à votre coffre.",
"detail": "Le jeton n'est affiché qu'une fois.",
"scopeNote": "Il peut uniquement ajouter de nouveaux documents à votre propre coffre. Il ne peut rien lister, ouvrir, télécharger, modifier ou supprimer, ni créer un autre jeton. Il apprend seulement si un fichier a été enregistré, pas comment vous l'avez classé.",
- "createFailed": "Le jeton n'a pas pu être créé. Veuillez réessayer."
+ "createFailed": "Le jeton n'a pas pu être créé. Veuillez réessayer.",
+ "namePlaceholder": "Nom, p. ex. Paperless-ngx"
},
- "tokenMintMovedDescription": "Les jetons sont émis là où ils s'appliquent : ci-dessus pour les mesures et les documents, ou dans le point de terminaison API d'un médicament pour ce seul médicament.",
- "tokenNamePlaceholder": "Nom du jeton (par exemple Raccourci iPhone)",
- "tokenCreated": "Jeton créé — copiez-le maintenant ! Il ne sera plus affiché.",
+ "tokenMintMovedDescription": "Les jetons sont émis là où ils s'appliquent : ci-dessus pour les mesures, les entraînements et les documents, ou dans le point de terminaison API d'un médicament pour ce seul médicament.",
+ "tokenCreated": "Jeton créé, copiez-le maintenant ! Il ne sera plus affiché.",
"tokenCopy": "Copier le jeton",
"tokenCopied": "Jeton copié dans le presse-papiers",
"tokenCopyFailed": "Impossible de copier le jeton",
@@ -6374,7 +6607,7 @@
"deleteAccountCta": "Supprimer le compte",
"deleteAccountConfirmTitle": "Supprimer définitivement le compte ?",
"deleteAccountConfirmDescription": "Votre compte, toutes vos données de santé, vos clés d’accès, vos sessions et vos entrées d’audit seront définitivement supprimés. Vous serez déconnecté immédiatement après.",
- "deleteAccountSuccess": "Compte supprimé — déconnexion en cours.",
+ "deleteAccountSuccess": "Compte supprimé, déconnexion en cours.",
"deleteAccountFailed": "Le compte n’a pas pu être supprimé",
"deleteAccountFinal": "Supprimer définitivement",
"saved": "Enregistré",
@@ -6397,7 +6630,7 @@
"testing": "Test en cours…",
"ok": "Connecté (latence {latency} ms)",
"errors": {
- "credentials_rejected": "Identifiants rejetés — vérifiez votre jeton",
+ "credentials_rejected": "Identifiants rejetés : vérifiez votre jeton",
"rate_limited": "Limité par le service en amont",
"timeout": "Délai de la requête dépassé",
"upstream_error": "Le service en amont a renvoyé une erreur",
@@ -6408,7 +6641,7 @@
"private_origin_not_approved": "Cible sur un réseau privé. L’opérateur doit ajouter cette origine à NOTIFICATION_PRIVATE_ORIGINS",
"private_origin_not_grantable": "Cette adresse est une adresse de lien local, de métadonnées ou non spécifiée ; aucune autorisation de l’opérateur ne peut l’ouvrir. Utilisez l’adresse sur laquelle le relais écoute",
"url_invalid": "L’URL n’est pas valide",
- "redirected": "Le point de terminaison redirige — vérifiez l’URL",
+ "redirected": "Le point de terminaison redirige : vérifiez l’URL",
"endpoint_not_found": "Point de terminaison introuvable à l’URL",
"credentials_unreadable": "Les identifiants ne peuvent pas être déchiffrés",
"upstream_invalid_json": "Le service en amont a envoyé un JSON non valide",
@@ -6419,7 +6652,7 @@
},
"notificationStatus": {
"title": "Fiabilité des canaux",
- "description": "État de chaque canal — « Désactivé automatiquement » signifie arrêté après des erreurs permanentes.",
+ "description": "État de chaque canal ; « Désactivé automatiquement » signifie arrêté après des erreurs permanentes.",
"emptyDescription": "Ajoutez un canal dans Paramètres › Intégrations › Canaux de diffusion.",
"stateActive": "Actif",
"stateAutoDisabled": "Désactivé automatiquement",
@@ -6445,7 +6678,7 @@
},
"healthRecord": {
"title": "Exporter le dossier de santé",
- "description": "Un dossier complet pour votre médecin — en PDF, en FHIR ou les deux.",
+ "description": "Un dossier complet pour votre médecin, en PDF, en FHIR ou les deux.",
"format": "Format",
"format_pdf": "PDF",
"format_fhir": "Structuré (FHIR)",
@@ -6477,7 +6710,7 @@
"createTitle": "Nouveau lien de partage",
"createDescription": "Le lien est en lecture seule et limité dans le temps.",
"label": "Libellé",
- "labelPlaceholder": "ex. Dr Martin — bilan annuel",
+ "labelPlaceholder": "ex. Dr Martin, bilan annuel",
"range": "Jours d'historique",
"rangeHint": "Jusqu'où remonte la fenêtre partagée.",
"expiry": "Expire dans (jours)",
@@ -6497,7 +6730,7 @@
"statusRevoked": "Révoqué",
"statusExpired": "Expiré",
"inactiveTitle": "Révoqués et expirés ({count})",
- "shownOnce": "Affiché une seule fois. Enregistrez le code QR et la phrase secrète maintenant — ils ne pourront pas être récupérés ensuite.",
+ "shownOnce": "Affiché une seule fois. Enregistrez le code QR et la phrase secrète maintenant : ils ne pourront pas être récupérés ensuite.",
"qrAlt": "Code QR contenant le lien de partage et la phrase secrète",
"linkLabel": "Lien",
"passphraseLabel": "Phrase secrète",
@@ -6508,12 +6741,12 @@
"attachTitle": "Joindre des documents",
"attachButton": "Choisir des documents",
"attachCount": "{count} sur {max} sélectionnés",
- "attachFrozen": "La sélection de documents est figée à la création du lien — impossible d'ajouter ou de retirer des documents ensuite.",
+ "attachFrozen": "La sélection de documents est figée à la création du lien : impossible d'ajouter ou de retirer des documents ensuite.",
"attachRemove": "Retirer {title}",
"exifNote": "Les métadonnées de l'appareil photo (EXIF/GPS) sont supprimées des images JPEG, PNG et WebP lors du partage. Les fichiers PDF, TIFF, HEIC et Office sont transmis tels quels.",
"documentCount": "Documents joints : {count}",
"qrScanTitle": "Scannez pour ouvrir sur un autre appareil",
- "qrScanHint": "Pointez l'appareil photo d'un téléphone vers le code pour ouvrir le dossier partagé — la phrase secrète est déjà incluse.",
+ "qrScanHint": "Pointez l'appareil photo d'un téléphone vers le code pour ouvrir le dossier partagé. La phrase secrète est déjà incluse.",
"pickerTitle": "Joindre des documents",
"pickerDescription": "Recherchez dans votre coffre et sélectionnez les documents à partager. Jusqu'à {max}.",
"pickerSearch": "Rechercher des documents",
@@ -6526,7 +6759,8 @@
"statusNeedsReselect": "Choisir à nouveau le contenu",
"reselectReason": "Ce lien diffusait un périmètre que personne n'avait choisi ; il a donc été fermé. Créez-en un nouveau et choisissez son contenu.",
"reselectAction": "Créer un remplacement",
- "createDetail": "Le jeton n'est affiché qu'une fois à la création — conservez-le alors ; il ne peut pas être récupéré."
+ "createDetail": "Le jeton n'est affiché qu'une fois à la création. Conservez-le alors ; il ne peut pas être récupéré.",
+ "activeDescription": "Les liens que vous avez partagés, leur expiration et le nombre d'ouvertures."
},
"genderOther": "Autre",
"nightscout": "Nightscout",
@@ -6534,10 +6768,10 @@
"nightscoutDescription": "Connecte ton propre site Nightscout pour synchroniser les mesures de glucose en continu dans le panneau glucose.",
"nightscoutViewData": "Voir votre glycémie",
"nightscoutUrl": "URL Nightscout",
- "nightscoutUrlSavedPlaceholder": "Enregistré — saisissez une nouvelle URL pour la remplacer",
+ "nightscoutUrlSavedPlaceholder": "Enregistré. Saisissez une nouvelle URL pour la remplacer",
"nightscoutToken": "Jeton d'API",
"nightscoutTokenOptional": "Facultatif pour un site public",
- "nightscoutTokenSavedPlaceholder": "Enregistré — saisissez un nouveau jeton pour le remplacer",
+ "nightscoutTokenSavedPlaceholder": "Enregistré. Saisissez un nouveau jeton pour le remplacer",
"nightscoutTokenHelp": "Collez un jeton d'accès depuis les outils d'administration de Nightscout. Laissez vide si votre site est lisible publiquement.",
"nightscoutPrivateOperatorTitle": "L’accès à un Nightscout privé nécessite l’autorisation de l’administrateur du serveur.",
"nightscoutPrivateOperatorDescription": "Demandez à l’administrateur du serveur d’autoriser précisément cette origine Nightscout.",
@@ -6558,7 +6792,7 @@
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
"polarCredentialsSaved": "Identifiants enregistrés",
- "polarCredentialsSavedPlaceholder": "Enregistré — saisir de nouveaux pour remplacer",
+ "polarCredentialsSavedPlaceholder": "Enregistré. Saisir de nouveaux pour remplacer",
"polarSaveCredentials": "Enregistrer",
"polarUnavailable": "Polar n'est pas configuré sur ce serveur. Ajoutez vos propres identifiants OAuth Polar ci-dessus, ou demandez à votre administrateur de configurer l'application partagée.",
"polarConnect": "Connecter Polar",
@@ -6568,7 +6802,7 @@
"polarDisconnect": "Déconnecter",
"polarDisconnectTitle": "Déconnecter Polar ?",
"polarDisconnectDescription": "Les synchronisations futures s'arrêtent. Les mesures déjà importées restent dans votre historique.",
- "polarOauthConnected": "Polar connecté — la première synchronisation démarre en arrière-plan.",
+ "polarOauthConnected": "Polar connecté. La première synchronisation démarre en arrière-plan.",
"polarOauthFailed": "Échec de la connexion à Polar",
"polarOauthError": {
"csrf1": "La vérification de sécurité a échoué car le cookie du navigateur était absent. Veuillez relancer la connexion dans le même navigateur.",
@@ -6587,7 +6821,7 @@
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
"ouraCredentialsSaved": "Identifiants enregistrés",
- "ouraCredentialsSavedPlaceholder": "Enregistré — saisir de nouveaux pour remplacer",
+ "ouraCredentialsSavedPlaceholder": "Enregistré. Saisir de nouveaux pour remplacer",
"ouraSaveCredentials": "Enregistrer",
"ouraUnavailable": "Oura n'est pas configuré sur ce serveur. Ajoutez vos propres identifiants OAuth Oura ci-dessus, ou demandez à votre administrateur de configurer l'application partagée.",
"ouraConnect": "Connecter Oura",
@@ -6597,7 +6831,7 @@
"ouraDisconnect": "Déconnecter",
"ouraDisconnectTitle": "Déconnecter Oura ?",
"ouraDisconnectDescription": "Les synchronisations futures s'arrêtent. Les mesures déjà importées restent dans votre historique.",
- "ouraOauthConnected": "Oura connecté — la première synchronisation démarre en arrière-plan.",
+ "ouraOauthConnected": "Oura connecté. La première synchronisation démarre en arrière-plan.",
"ouraOauthFailed": "Échec de la connexion à Oura",
"ouraOauthError": {
"csrf1": "La vérification de sécurité a échoué car le cookie du navigateur était absent. Veuillez relancer la connexion dans le même navigateur.",
@@ -6607,6 +6841,7 @@
"nocreds": "Oura n'est pas configuré sur ce serveur. Demandez à votre administrateur d'ajouter les identifiants OAuth Oura.",
"token": "L'échange de jeton avec Oura a échoué. Veuillez relancer la connexion.",
"rate_limited": "Trop de tentatives de connexion. Patientez un instant et réessayez.",
+ "issuer": "La réponse de connexion ne provenait pas du serveur d'autorisation d'Oura, la connexion a donc été interrompue par sécurité. Veuillez relancer la connexion. Si cela se reproduit, prévenez votre administrateur.",
"generic": "Erreur inconnue lors de la connexion. Veuillez relancer la connexion."
},
"integrationSetupGuide": "Guide de configuration",
@@ -6618,10 +6853,17 @@
"savedOn": "Coach activé.",
"savedOff": "Coach masqué.",
"saveError": "Impossible d enregistrer la modification. Veuillez réessayer."
+ },
+ "memory": {
+ "edit": "Modifier",
+ "source": {
+ "user": "de toi",
+ "coach": "du Coach"
+ }
}
},
"integrationRedirectGuide": {
- "intro": "Cette connexion passe par votre propre application développeur {provider} — environ 5 minutes de configuration :",
+ "intro": "Cette connexion passe par votre propre application développeur {provider}, environ 5 minutes de configuration :",
"step1": "Créez une application dans le portail développeur {provider}.",
"step2": "Réglez son URL de redirection/callback sur :",
"step3": "Collez ci-dessus l'ID client et le secret client fournis."
@@ -6681,7 +6923,7 @@
},
"recovery": {
"saveNow": "Enregistrez ces codes de récupération maintenant",
- "saveNowHint": "Chaque code ne fonctionne qu'une fois. Ils ne sont affichés que cette fois — conservez-les en lieu sûr. Si vous perdez votre authentificateur, un code de récupération est le seul moyen de revenir.",
+ "saveNowHint": "Chaque code ne fonctionne qu'une fois. Ils ne sont affichés que cette fois : conservez-les en lieu sûr. Si vous perdez votre authentificateur, un code de récupération est le seul moyen de revenir.",
"download": "Télécharger",
"remainingOne": "{count} code de récupération restant.",
"remainingOther": "{count} codes de récupération restants.",
@@ -6745,12 +6987,11 @@
},
"stored": {
"title": "Ce qui est stocké",
- "measurements": "Mesures (poids, tension, pouls, glycémie, sommeil, composition corporelle)",
- "medications": "Médicaments et historique des prises",
- "moodLabs": "Entrées d'humeur et résultats de laboratoire",
- "coach": "Conversations avec le coach",
+ "measurements": "Mesures : poids, tension, pouls et toute autre valeur enregistrée par vous ou un appareil",
"integrations": "Jetons d'intégration (chiffrés)",
- "security": "Activité du compte et de sécurité (sessions, journal de connexion)"
+ "security": "Activité du compte et de sécurité (sessions, journal de connexion)",
+ "profile": "Votre profil, vos antécédents médicaux et les informations du dossier de santé",
+ "modules": "Données de chaque module : {list}"
},
"retention": {
"title": "Conservation",
@@ -6781,13 +7022,11 @@
"mcp": {
"enableTitle": "Point de terminaison MCP distant",
"enableDescription": "Autorisez un assistant compatible MCP (Claude.ai, ChatGPT) à lire vos données de santé via un jeton restreint.",
- "enabled": "Activé",
- "disabled": "Désactivé",
"enableToggleLabel": "Activer le point de terminaison MCP",
"endpointLabel": "URL du connecteur",
"connectDescription": "Collez cette URL dans la configuration du connecteur personnalisé de votre assistant, puis autorisez avec le jeton ci-dessous ou connectez-vous lorsque vous y êtes invité.",
"tokensTitle": "Jetons du connecteur",
- "tokensDescription": "Générez un jeton pour connecter un assistant — lecture seule, sauf si vous autorisez l'écriture ci-dessous.",
+ "tokensDescription": "Générez un jeton pour connecter un assistant, en lecture seule sauf si vous autorisez l'écriture ci-dessous.",
"scopeNote": "Les nouveaux jetons ne peuvent que lire vos propres données. L'accès en écriture (health:write) s'accorde par jeton avec le commutateur ci-dessous. Un jeton ne peut jamais atteindre les fonctions d'administration.",
"writeScopeToggleLabel": "Autoriser l'enregistrement (health:write)",
"writeScopeNote": "Ajoute la portée health:write pour que l'assistant puisse enregistrer de nouvelles mesures et entrées d'humeur dans votre compte. Chaque écriture est d'abord confirmée. Le jeton ne peut toujours pas supprimer ni modifier des entrées existantes, changer les médicaments, ni atteindre les fonctions REST ou d'administration. Laissez désactivé pour un accès en lecture seule.",
@@ -6801,7 +7040,8 @@
"connectionRevokeDescription": "Cela met fin immédiatement à l'accès de l'assistant et l'empêche de se renouveler. Une nouvelle autorisation sera nécessaire pour le reconnecter.",
"connectionRevokeFailed": "Impossible de déconnecter l'assistant.",
"tokensDetail": "Le jeton n'est affiché qu'une seule fois.",
- "connectionsDetail": "Révoquer une connexion met fin à son accès immédiatement, y compris sa capacité à se renouveler."
+ "connectionsDetail": "Révoquer une connexion met fin à son accès immédiatement, y compris sa capacité à se renouveler.",
+ "tokenNamePlaceholder": "Nom, p. ex. Claude Desktop"
},
"strava": "Strava",
"stravaDescription": "Connecte ton compte Strava pour synchroniser tes activités avec ton historique d'entraînement.",
@@ -6810,7 +7050,7 @@
"stravaClientId": "ID client",
"stravaClientSecret": "Secret client",
"stravaCredentialsSaved": "Identifiants enregistrés",
- "stravaCredentialsSavedPlaceholder": "Enregistrés — saisis-en de nouveaux pour remplacer",
+ "stravaCredentialsSavedPlaceholder": "Enregistrés. Saisis-en de nouveaux pour remplacer",
"stravaSaveCredentials": "Enregistrer",
"stravaUnavailable": "Strava n'est pas configuré sur ce serveur. Ajoute tes propres identifiants API Strava ci-dessus ou demande à ton administrateur de configurer l'application partagée.",
"stravaConnect": "Connecter Strava",
@@ -6820,7 +7060,7 @@
"stravaDisconnect": "Déconnecter",
"stravaDisconnectTitle": "Déconnecter Strava ?",
"stravaDisconnectDescription": "Les synchronisations futures s'arrêtent. Les activités déjà importées restent dans ton historique.",
- "stravaOauthConnected": "Strava connecté — la première synchronisation démarre en arrière-plan.",
+ "stravaOauthConnected": "Strava connecté. La première synchronisation démarre en arrière-plan.",
"stravaOauthFailed": "La connexion à Strava a échoué",
"stravaOauthError": {
"csrf1": "Le contrôle de sécurité a échoué car le cookie du navigateur était manquant. Relance la connexion dans le même navigateur.",
@@ -6848,26 +7088,33 @@
"title": "Progression de la synchronisation",
"receivedLabel": "Enregistrements reçus",
"oldestLabel": "Mesure la plus ancienne atteinte",
- "flowing": "Des données sont arrivées au cours de la dernière heure — l'app iPhone continue d'envoyer.",
- "waiting": "En attente de nouvelles données de l'app iPhone. Le transfert reprend quand l'app est ouverte ou quand iOS lui accorde du temps en arrière-plan — une accalmie pendant le premier grand rattrapage est normale, pas une erreur."
+ "flowing": "Des données sont arrivées au cours de la dernière heure. L'app iPhone continue d'envoyer.",
+ "waiting": "En attente de nouvelles données de l'app iPhone. Le transfert reprend quand l'app est ouverte ou quand iOS lui accorde du temps en arrière-plan. Une accalmie pendant le premier grand rattrapage est normale, pas une erreur."
},
"delivery": {
"title": "Livraison",
"lastTriggerLabel": "La dernière synchronisation est arrivée",
"lastBackgroundLabel": "Dernière synchronisation en arrière-plan",
- "backgroundNever": "Jamais — jusqu'ici chaque synchronisation est arrivée avec l'app ouverte",
+ "backgroundNever": "Jamais : jusqu'ici chaque synchronisation est arrivée avec l'app ouverte",
"noSyncYet": "Aucune synchronisation n'est encore arrivée, il n'y a donc rien à signaler ici.",
"trigger": {
"foreground": "avec l'app ouverte",
"background": "en arrière-plan",
"push": "depuis un réveil push",
- "unknown": "déclencheur non signalé par l'app"
+ "unknown": "déclencheur non signalé par l'app",
+ "manual": "via « Tout synchroniser »"
}
+ },
+ "freshness": {
+ "title": "Par type de donnée",
+ "lastReceived": "Dernière réception {relative}",
+ "lastNewValue": "Dernière nouvelle valeur {relative}",
+ "never": "Pas encore reçu"
}
},
"garminInfo": {
"title": "Garmin",
- "body": "Garmin n'a pas de connexion directe ici. Connecte-le via Apple Health (iOS) ou Google Health Connect (Android). Les scores propriétaires de Garmin — Body Battery, Training Load, statut VFC, VO₂ max — ne sont partagés par Garmin avec aucune des deux plateformes.",
+ "body": "Garmin n'a pas de connexion directe ici. Connecte-le via Apple Health (iOS) ou Google Health Connect (Android). Les scores propriétaires de Garmin (Body Battery, Training Load, statut VFC, VO₂ max) ne sont partagés par Garmin avec aucune des deux plateformes.",
"docsLink": "Lire le guide"
},
"glucoseReference": {
@@ -6923,6 +7170,12 @@
"description": "Cette action partage ou exporte vos données de santé. Confirmez qu'il s'agit bien de vous pour continuer.",
"signInAgain": "Reconnectez-vous, puis réessayez. Votre compte n'a ni mot de passe ni clé d'accès à confirmer ici.",
"failed": "Cela n'a pas fonctionné. Vérifiez votre saisie et réessayez."
+ },
+ "passwordTitle": "Mot de passe",
+ "tokenNameLabel": "Nom du jeton",
+ "displayCard": {
+ "title": "Affichage",
+ "description": "Langue, unités et formats, enregistrés dès que vous les modifiez."
}
},
"admin": {
@@ -6942,27 +7195,27 @@
"loadingStatus": "Chargement de l'état...",
"systemStatusLoadError": "Échec du chargement de l'état du système.",
"systemStatusRetry": "Réessayer",
- "adminSettingsLoadError": "Échec du chargement des paramètres d'administration. Les commutateurs ci-dessous peuvent ne pas refléter l'état actuel — actualisez la page.",
+ "adminSettingsLoadError": "Échec du chargement des paramètres d'administration. Les commutateurs ci-dessous peuvent ne pas refléter l'état actuel : actualisez la page.",
"hostMetrics": {
"title": "Charge de l'hôte",
"load1": "Charge (1 min)",
"memUsedPercent": "Mémoire",
"diskBusyPercent": "Disk I/O",
"last2hours": "2 dernières heures",
- "empty": "Échantillonnage démarré — les données apparaissent dans 1 à 2 minutes.",
+ "empty": "Échantillonnage démarré. Les données apparaissent dans 1 à 2 minutes.",
"loadError": "Échec du chargement des métriques de l'hôte. Essayez d'actualiser la page."
},
"aiServerKey": {
"title": "Clé IA du serveur",
"description": "Une clé de l'opérateur pour les comptes qui n'en ont pas configuré une.",
- "byokHint": "La clé personnelle de l'utilisateur (BYOK, connexion ChatGPT ou fournisseur local) a toujours la priorité — la clé du serveur est la dernière étape de la chaîne de fournisseurs.",
+ "byokHint": "La clé personnelle de l'utilisateur (BYOK, connexion ChatGPT ou fournisseur local) a toujours la priorité ; la clé du serveur est la dernière étape de la chaîne de fournisseurs.",
"consentHint": "Les requêtes via la clé du serveur nécessitent le consentement IA actif de l'utilisateur ; sans justificatif de consentement, rien n'est envoyé.",
"baseUrlHint": "L'URL de base détermine l'API : une URL Anthropic telle que https://api.anthropic.com/v1 est reconnue, tout autre hôte est appelé via l'API compatible OpenAI.",
"statusConfigured": "Configurée",
"statusNotConfigured": "Non configurée",
"keyLabel": "Clé API",
"keyPlaceholder": "sk-...",
- "keyPlaceholderSet": "Enregistrée ({preview}) — saisissez une nouvelle clé pour la remplacer",
+ "keyPlaceholderSet": "Enregistrée ({preview}). Saisissez une nouvelle clé pour la remplacer",
"modelLabel": "Modèle",
"baseUrlLabel": "URL de base",
"save": "Enregistrer",
@@ -6996,13 +7249,16 @@
"typeCodex": "Connexion ChatGPT (personnelle)",
"typeLocal": "Modèle local (personnel)",
"typeGateway": "Passerelle compatible OpenAI (personnelle)",
- "spendToday": "Aujourd'hui ({date}) : {total} jetons, dont {operator} sur la clé de l'opérateur."
+ "spendToday": "Aujourd'hui, selon le jour civil de chaque compte : {total} jetons, dont {operator} sur la clé de l'opérateur (comptes avec consommation : {accounts}).",
+ "statusPaused": "En pause",
+ "pausedHint": "En pause après des échecs répétés pour {count} utilisateurs jusqu’au {at}. Il sera ensuite réessayé automatiquement.",
+ "modelNotServed": "Le point d’accès ne propose plus le modèle {model}. Choisissez dans les réglages IA un modèle qu’il propose."
},
"aiQuality": {
"title": "Qualité des analyses",
"loading": "Chargement du résumé des retours...",
"loadError": "Échec du chargement du résumé de qualité des analyses. Essayez d'actualiser la page.",
- "empty": "Aucun retour pour l'instant. L'agrégateur s'exécute chaque jour à 04:00 — dès que les utilisateurs évaluent les recommandations, les taux d'utilité apparaissent ici.",
+ "empty": "Aucun retour pour l'instant. L'agrégateur s'exécute chaque jour à 04:00 ; dès que les utilisateurs évaluent les recommandations, les taux d'utilité apparaissent ici.",
"windowLabel": "{days} derniers jours",
"generatedAtLabel": "Généré",
"colSeverity": "Gravité",
@@ -7017,7 +7273,7 @@
"title": "Retours sur le Coach",
"loading": "Chargement du résumé des retours sur le Coach...",
"loadError": "Échec du chargement du résumé des retours sur le Coach. Essayez d'actualiser la page.",
- "empty": "Aucun retour sur le Coach pour l'instant. L'agrégateur s'exécute chaque jour à 04:00 — dès que les utilisateurs évaluent les réponses du Coach, les taux d'utilité apparaissent ici.",
+ "empty": "Aucun retour sur le Coach pour l'instant. L'agrégateur s'exécute chaque jour à 04:00 ; dès que les utilisateurs évaluent les réponses du Coach, les taux d'utilité apparaissent ici.",
"windowLabel": "{days} derniers jours",
"generatedAtLabel": "Généré",
"colPromptVersion": "Version du prompt",
@@ -7040,9 +7296,9 @@
"defaultLanguage": "Langue par défaut",
"defaultLanguageDescription": "Utilisé pour les nouveaux utilisateurs et comme solution de repli.",
"defaultUserTimezone": "Fuseau horaire par défaut pour les nouveaux comptes",
- "defaultUserTimezoneDescription": "Utilisé pour les nouveaux comptes sans fuseau détecté par le navigateur. N'affecte pas les utilisateurs existants.",
+ "defaultUserTimezoneDescription": "Utilisé pour les nouveaux comptes sans fuseau détecté par le navigateur ; les comptes existants gardent le leur.",
"defaultUserTimezoneFallback": "Europe/Berlin (par défaut)",
- "servicesGlobal": "Services (global)",
+ "servicesGlobal": "Canaux et API",
"servicesGlobalDescription": "Les services désactivés ne sont disponibles pour aucun utilisateur.",
"telegramGlobal": "Autoriser les notifications Telegram.",
"ntfyGlobal": "Autoriser les notifications ntfy.",
@@ -7050,19 +7306,21 @@
"integrationWebPush": "Web Push",
"offlineGeoLabel": "Données géographiques",
"offlineGeoEnabled": "Hors ligne (GeoLite2)",
- "offlineGeoFallback": "En ligne ({host}) — montez les bases GeoLite2 et définissez GEOLITE2_DIR",
+ "offlineGeoFallback": "En ligne ({host}) : montez les bases GeoLite2 et définissez GEOLITE2_DIR",
+ "offlineGeoOff": "Désactivé",
+ "offlineGeoChosen": "En ligne ({host})",
"apiGlobal": "Autoriser les points de terminaison d'API et les jetons d'API.",
"umamiTitle": "Umami",
- "umamiDescription": "Analyse web avec Umami",
+ "umamiDescription": "Analyse web avec Umami.",
"glitchtipTitle": "GlitchTip",
- "glitchtipDescription": "Suivi des erreurs avec GlitchTip",
+ "glitchtipDescription": "Suivi des erreurs avec GlitchTip.",
"webPushVapidTitle": "Web Push VAPID",
- "umamiEnabled": "Activer le suivi Umami.",
+ "umamiEnabled": "Activer le suivi Umami",
"umamiScriptUrl": "URL du script Umami",
"umamiScriptUrlPlaceholder": "https://analytics.example.com/script.js",
"umamiWebsiteId": "ID du site web Umami",
"umamiWebsiteIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "glitchtipEnabled": "Activer le suivi des erreurs GlitchTip.",
+ "glitchtipEnabled": "Activer le suivi des erreurs GlitchTip",
"glitchtipDsn": "GlitchTip DSN",
"glitchtipDsnPlaceholder": "https://@glitchtip.example.com/",
"glitchtipEnvironment": "Environnement GlitchTip",
@@ -7085,7 +7343,7 @@
"webPushVapidGenerating": "Génération…",
"webPushVapidGenerated": "Clés VAPID générées. Enregistrez le sujet pour terminer.",
"webPushVapidGenerateFailed": "Impossible de générer les clés VAPID.",
- "webPushVapidGenerateConfirm": "Les clés VAPID sont déjà configurées. Générer une nouvelle paire invalide tous les abonnements push du navigateur — chaque appareil doit se réabonner. Continuer ?",
+ "webPushVapidGenerateConfirm": "Les clés VAPID sont déjà configurées. Générer une nouvelle paire invalide tous les abonnements push du navigateur : chaque appareil doit se réabonner. Continuer ?",
"webPushVapidGenerateConfirmTitle": "Régénérer les clés VAPID ?",
"webPushVapidRegenerate": "Régénérer",
"configured": "Configuré",
@@ -7113,18 +7371,23 @@
"documentAi": {
"title": "Lecture de documents",
"description": "Lecture par IA des documents enregistrés, scan des comptes rendus de laboratoire et extraction de médicaments à partir de texte. La plus grande part des données qui quittent le serveur."
+ },
+ "reasoning": {
+ "enabled": "Autoriser la réflexion approfondie",
+ "max": "Profondeur de réflexion maximale",
+ "description": "Les modèles réfléchissent avant de répondre : le Coach, le briefing quotidien et les synthèses de période. Plus précis, plus de jetons."
}
},
"modules": {
"title": "Disponibilité des modules",
"description": "Désactivez un module pour tout le serveur.",
"saveError": "Impossible de mettre à jour la disponibilité des modules.",
- "detail": "Un module désactivé ici est coupé pour chaque compte, quelle que soit la préférence personnelle. Les domaines essentiels (poids, tension, pouls, médicaments) restent toujours actifs et ne peuvent pas être désactivés.",
- "coachFollowsSwitch": "Suit l'interrupteur Coach dans Assistant."
+ "detail": "Le poids, la tension et le pouls sont des mesures essentielles et restent toujours actifs.",
+ "coachFollowsSwitch": "Suit l'interrupteur Coach de la section Coach."
},
"invites": {
- "title": "Invitations",
- "description": "Liens d'invitation pour l'inscription — ils fonctionnent même lorsque les inscriptions sont fermées.",
+ "title": "Liens d'invitation",
+ "description": "Chaque lien permet une inscription même lorsque les inscriptions sont fermées.",
"create": "Créer une invitation",
"createTitle": "Créer une invitation",
"createDescription": "Le lien permet une inscription même lorsque les inscriptions sont fermées.",
@@ -7136,7 +7399,7 @@
"createdToast": "Invitation créée",
"createError": "Impossible de créer l'invitation.",
"tokenTitle": "Lien d'invitation",
- "tokenOnceHint": "Copiable uniquement maintenant — copiez le lien ou faites scanner le code QR.",
+ "tokenOnceHint": "Copiable uniquement maintenant : copiez le lien ou faites scanner le code QR.",
"hashExplainer": "Seul un hachage du lien est stocké : il ne pourra plus être affiché. En cas de perte, révoquez l'invitation et créez-en une nouvelle.",
"copyLink": "Copier le lien",
"copiedToast": "Lien copié",
@@ -7170,8 +7433,8 @@
"revokedToast": "Invitation révoquée.",
"revokeError": "Impossible de révoquer l'invitation."
},
- "userManagement": "Gestion des utilisateurs",
- "userManagementDescription": "Gérez les comptes : rôles, réinitialisations de mot de passe et déconnexions forcées.",
+ "userManagement": "Comptes",
+ "userManagementDescription": "Rôles, réinitialisations de mot de passe et déconnexions forcées pour chaque compte.",
"userName": "Nom",
"userEmail": "E-mail",
"userRole": "Rôle",
@@ -7198,14 +7461,13 @@
"tokenName": "Nom",
"tokenPermissions": "Autorisations",
"tokenPermissionAll": "Tout",
- "tokenPermissionAllTooltip": "Autorisation joker — ce jeton peut appeler toutes les routes de l'API.",
+ "tokenPermissionAllTooltip": "Autorisation joker : ce jeton peut appeler toutes les routes de l'API.",
"tokenStatus": "Statut",
"tokenLastUsed": "Dernière utilisation",
"tokenCreated": "Créé",
"tokenNeverUsed": "Jamais",
"tokensEmptyTitle": "Aucun jeton d'API pour l'instant",
"tokensEmptyDescription": "Les jetons sont émis lorsqu'un client natif se connecte ou via Paramètres → Compte.",
- "loginOverview": "Aperçu des connexions",
"loginOverviewDescription": "Événements de connexion et d’authentification de tous les comptes, filtrables et exportables en CSV.",
"allAuthEvents": "Tous les événements d'authentification",
"failedOnly": "Échecs uniquement",
@@ -7238,6 +7500,7 @@
"finalDelete": "Supprimer définitivement",
"deletedResult": "{rows} lignes supprimées dans {tables} tables. Les comptes utilisateurs et les identifiants de connexion ont été conservés.",
"authRegister": "Inscription",
+ "authClaim": "Profil repris",
"authLogin": "Connexion",
"authLoginPasskey": "Connexion par passkey",
"authLoginPassword": "Connexion par mot de passe",
@@ -7312,7 +7575,7 @@
},
"services": {
"title": "Services",
- "subtitle": "Activez ou désactivez les canaux de notification et les intégrations à l'échelle du système."
+ "subtitle": "Activez ou désactivez les canaux de notification et l'API pour tout le serveur."
},
"integrations": {
"title": "Intégrations",
@@ -7376,7 +7639,7 @@
"app-logs": {
"title": "Journaux de l'app",
"subtitle": "Affichez les wide-events structurés les plus récents émis par ce processus.",
- "processNote": "Seuls les journaux du processus de l'app sont affichés — ceux du processus worker vont vers Loki lorsque cette intégration est configurée.",
+ "processNote": "Seuls les journaux du processus de l'app sont affichés ; ceux du processus worker vont vers Loki lorsque cette intégration est configurée.",
"filterTraceId": "ID de trace",
"filterAction": "L'action contient…",
"filterLevel": "Niveau",
@@ -7398,7 +7661,8 @@
"refresh": "Actualiser",
"eventDetails": "Détails de l'événement",
"closeDetails": "Fermer les détails",
- "processDetail": "Le tampon est volatil et limité à 500 entrées."
+ "processDetail": "Le tampon est volatil et limité à 500 entrées.",
+ "cardTitle": "Événements récents"
},
"backups": {
"title": "Sauvegardes",
@@ -7410,7 +7674,7 @@
"loading": "Chargement des sauvegardes...",
"loadError": "Échec du chargement des sauvegardes.",
"emptyTitle": "Aucune sauvegarde pour l'instant",
- "emptyDescription": "Les sauvegardes s'exécutent automatiquement chaque dimanche à 03:00 — ou lancez-en une maintenant pour créer le premier instantané.",
+ "emptyDescription": "Les sauvegardes s'exécutent automatiquement chaque dimanche à 03:00, ou lancez-en une maintenant pour créer le premier instantané.",
"colUser": "Utilisateur",
"colType": "Type",
"typeWeeklyAuto": "Hebdomadaire (auto)",
@@ -7425,6 +7689,12 @@
"scheduleFailedDescription": "{when} : {reason}",
"scheduleFailedNoReason": "aucune raison enregistrée",
"scheduleRemedy": "Lancez une sauvegarde maintenant et vérifiez qu'une nouvelle ligne apparaît. Si elle échoue à nouveau, le journal du worker indique le compte sur lequel elle s'est arrêtée.",
+ "activity": {
+ "running": "En cours depuis {when}.",
+ "interruptedTitle": "Une exécution s'est arrêtée en cours de route sans reprendre",
+ "interruptedAccount": "{username} : commencée {when}, jamais terminée",
+ "interruptedRemedy": "L'application a très probablement redémarré ou manqué de mémoire pendant l'écriture de cette copie. La prochaine exécution traite ces comptes en dernier, afin que tous les autres reçoivent leur copie d'abord. Si cela se reproduit, donnez plus de mémoire au conteneur."
+ },
"download": "Télécharger",
"downloadAria": "Télécharger la sauvegarde de {username}",
"downloadFailed": "Échec du téléchargement",
@@ -7440,11 +7710,11 @@
"restoreTitle": "Restaurer à partir d'une sauvegarde",
"restoreDescription": "Cette action remplacera TOUTES les données actuelles de {username} par l'instantané du {when}. Les mesures, médicaments, événements de prise, entrées d'humeur, canaux de notification, abonnements push et suppressions Telegram en file d'attente existants seront supprimés.",
"restoreInstanceSettingsLabel": "Restaurer aussi les paramètres de l'instance",
- "restoreInstanceSettingsHint": "Inscription, exigence MFA, langue par défaut, disponibilité des modules, configuration des notifications et de l'IA — repris de l'instantané et appliqués à tout le monde sur cette installation. Laissez décoché pour ne restaurer que ce compte.",
+ "restoreInstanceSettingsHint": "Inscription, exigence MFA, langue par défaut, disponibilité des modules, configuration des notifications et de l'IA : repris de l'instantané et appliqués à tout le monde sur cette installation. Laissez décoché pour ne restaurer que ce compte.",
"restorePromptLabel": "Tapez RESTORE pour continuer",
"previewTitle": "Contenu de la sauvegarde",
"previewLoading": "Lecture du contenu…",
- "previewUnavailable": "Le contenu n'a pas pu être lu — la restauration reste possible.",
+ "previewUnavailable": "Le contenu n'a pas pu être lu, mais la restauration reste possible.",
"previewStillReading": "Cette sauvegarde a été créée par une version antérieure, son contenu est donc lu en entier, ce qui prend un moment pour un gros dossier. Rouvrez cette fenêtre dans quelques minutes pour le voir. La restauration n'en dépend pas.",
"previewEmpty": "Le fichier ne contient aucun enregistrement.",
"previewOther": "Autres enregistrements",
@@ -7487,7 +7757,7 @@
"restoreSkippedMedicationTarget": "Analyte cible d'un médicament",
"restoreSkippedScheduleRevisionLink": "Succession des périodes de schéma",
"restoreSkippedAccountSetting": "Réglage du compte",
- "restoreMissingTitle": "Restauration refusée — le fichier est incomplet",
+ "restoreMissingTitle": "Restauration refusée : le fichier est incomplet",
"restoreMissingDescription": "Le manifeste du fichier indique qu'il contient la section ci-dessous, et il ne la contient pas. Rien n'a été modifié : une restauration remplace toute une classe d'enregistrements, une section absente l'aurait donc vidée.",
"restoreMissingRemedy": "Utilise une sauvegarde de reprise après sinistre, ou réexporte le compte et téléverse le nouveau fichier. Une section que le manifeste déclare omise est autre chose et se restaure normalement.",
"restoreMissingDocuments": "Documents",
@@ -7555,7 +7825,12 @@
"restoreFailureKeyMissing": "La sauvegarde a besoin de la clé de chiffrement {keys}, avec laquelle ce serveur ne peut pas l'ouvrir. Remettez la clé dans ENCRYPTION_KEYS, redémarrez, puis relancez la restauration.",
"restoreFailureForeignReference": "Le fichier renvoie à des éléments qui appartiennent à un autre compte. Rien n'a été écrit.",
"previewKeyMissing": "Cette sauvegarde ne peut pas être restaurée sur ce serveur : elle a besoin de la clé de chiffrement {keys}. Remettez d'abord la clé dans ENCRYPTION_KEYS et redémarrez.",
- "restoreSkippedSymptomEpisodeReference": "Entrée de symptôme liée à une maladie absente du fichier (conservée sans le lien)"
+ "restoreSkippedSymptomEpisodeReference": "Entrée de symptôme liée à une maladie absente du fichier (conservée sans le lien)",
+ "restoreSkippedCycleCiphertext": "Note ou libellé du cycle chiffré avec une clé que ce serveur ne possède pas (jour ou symptôme conservé sans)",
+ "restoreSkippedMoodLabelCiphertext": "Nom d’étiquette ou de groupe d’humeur chiffré avec une clé que ce serveur ne possède pas (conservé sans son nom)",
+ "restoreSkippedVisitCiphertext": "Texte de consultation ou de praticien chiffré avec une clé que ce serveur ne possède pas (conservé sans)",
+ "restoreSkippedVaccinationCiphertext": "Note de vaccination chiffrée avec une clé que ce serveur ne possède pas (dose conservée sans)",
+ "restoreSkippedEnvironmentLocationCiphertext": "Lieu d'une période ou d'un jour environnemental chiffré avec une clé que ce serveur ne possède pas (conservé chiffré, ces jours ne sont pas actualisés)"
},
"danger-zone": {
"title": "Zone de danger",
@@ -7572,7 +7847,6 @@
"encryption": {
"title": "Chiffrement",
"subtitle": "Couverture du chiffrement et état de la rotation des clés pour les données au repos.",
- "loading": "Chargement de l'état du chiffrement...",
"loadError": "Échec du chargement de l'état du chiffrement.",
"coverageTitle": "Couverture",
"coverageDescription": "Combien de lignes chiffrées relèvent de chaque id de clé.",
@@ -7617,12 +7891,45 @@
"backupsNone": "Aucune sauvegarde n'a encore enregistré les clés dont elle a besoin.",
"backupsStillNeed": "Toutes les lignes sont renouvelées, mais des sauvegardes ont encore besoin de {keys}",
"backupsUnrecordedBadge": "Toutes les lignes sont renouvelées, mais d'anciennes sauvegardes peuvent encore avoir besoin d'une clé retirée",
- "backupsDetail": "La rotation rechiffre chaque ligne, mais une sauvegarde garde la clé sous laquelle son contenu a été écrit, et sa restauration en a besoin. Gardez une clé retirée dans ENCRYPTION_KEYS tant qu'elle figure ci-dessous."
+ "backupsDetail": "La rotation rechiffre chaque ligne, mais une sauvegarde garde la clé sous laquelle son contenu a été écrit, et sa restauration en a besoin. Gardez une clé retirée dans ENCRYPTION_KEYS tant qu'elle figure ci-dessous.",
+ "columnsShow": "Afficher le tableau ({count} avec des lignes)",
+ "columnsHide": "Masquer le tableau",
+ "columnsShowAll": "Afficher les {count} colonnes",
+ "columnsShowWithRows": "Seulement les colonnes avec des lignes",
+ "columnsNoneWithRows": "Aucune colonne chiffrée ne contient encore de lignes."
+ },
+ "maintenance": {
+ "title": "Maintenance de la table des mesures",
+ "description": "Libère la place qu'occupaient les anciens restes de synchronisation, une fois le nettoyage terminé.",
+ "body": "Elle exécute un vacuum de la table des mesures puis reconstruit ses index, un par un. La lecture et l'enregistrement continuent de fonctionner pendant ce temps. Choisissez une heure calme et prévoyez de l'espace disque libre pour une copie supplémentaire du plus grand index.",
+ "tableSize": "Table avec index",
+ "indexSize": "Index",
+ "loading": "Vérification de l'état de la maintenance…",
+ "loadError": "Impossible de lire l'état de la maintenance.",
+ "unavailable": "Aucun processus d'arrière-plan n'est connecté, la maintenance ne peut donc pas s'exécuter ici.",
+ "purgePending": "Le nettoyage des anciens restes de synchronisation est toujours en cours. La maintenance pourra démarrer une fois qu'il sera terminé.",
+ "state": {
+ "idle": "Pas encore exécutée.",
+ "queued": "En attente du processus d'arrière-plan.",
+ "running": "En cours depuis {since}. Sur une grande instance, cela peut prendre un moment.",
+ "completed": "Terminée le {at}.",
+ "refusedPurge": "Pas démarrée, car le nettoyage était encore en cours. Relancez-la une fois le nettoyage terminé.",
+ "alreadyRunning": "Pas démarrée, car une autre exécution était déjà en cours.",
+ "failed": "Arrêtée le {at}. Le journal de l'application indique l'étape et le runbook explique quoi vérifier."
+ },
+ "start": "Lancer la maintenance",
+ "confirmTitle": "Lancer la maintenance maintenant ?",
+ "confirmBody": "Elle s'exécute en arrière-plan, étape par étape, et peut prendre un moment. Si vous n'avez pas encore fait de sauvegarde aujourd'hui, faites-en une d'abord.",
+ "confirm": "Lancer",
+ "cancel": "Annuler",
+ "started": "Maintenance lancée.",
+ "alreadyQueued": "Une maintenance est déjà en file d'attente ou en cours.",
+ "startFailed": "Impossible de lancer la maintenance."
}
},
"carrier": "Opérateur",
"mfaRequired": "Exiger un second facteur",
- "mfaRequiredDescription": "Chaque compte sans deuxième facteur actif est dirigé vers la configuration après la connexion. Cela invite les utilisateurs web à l'activer, mais ne bloque pas l'accès direct par API ou natif (Bearer) pour les comptes pas encore configurés.",
+ "mfaRequiredDescription": "Les comptes sans deuxième facteur sont dirigés vers sa configuration après une connexion web ; l'accès par API ou natif (Bearer) reste ouvert.",
"mfaEnforcedOn": "MFA activé",
"mfaEnforcedOff": "MFA désactivé",
"mfaEnforcedOnHint": "Un second facteur est requis pour cet utilisateur. Cliquez pour ne plus l'exiger.",
@@ -7640,7 +7947,7 @@
"centralCodex": {
"title": "Connexion IA partagée",
"description": "Reliez un compte ChatGPT connecté que chaque utilisateur de ce serveur peut activer pour les fonctions IA.",
- "honesty": "Il s'agit d'un unique compte IA partagé et connecté, pas d'une clé d'API. Chaque personne qui l'active envoie ses requêtes par ce biais : les limites horaires et hebdomadaires du compte sont donc partagées entre elles, et ces mêmes limites alimentent déjà le Coach. Les fournisseurs par abonnement peuvent utiliser le contenu pour améliorer leurs modèles et aucun accord de traitement des données ne le couvre — ce n'est donc jamais l'option par défaut et cela reste après les fournisseurs sans entraînement pour la lecture de documents. Ne reliez qu'un compte que vous êtes prêt à partager ainsi.",
+ "honesty": "Il s'agit d'un unique compte IA partagé et connecté, pas d'une clé d'API. Chaque personne qui l'active envoie ses requêtes par ce biais : les limites horaires et hebdomadaires du compte sont donc partagées entre elles, et ces mêmes limites alimentent déjà le Coach. Les fournisseurs par abonnement peuvent utiliser le contenu pour améliorer leurs modèles et aucun accord de traitement des données ne le couvre. Ce n'est donc jamais l'option par défaut et cela reste après les fournisseurs sans entraînement pour la lecture de documents. Ne reliez qu'un compte que vous êtes prêt à partager ainsi.",
"statusConnected": "Connecté",
"statusDisconnected": "Non connecté",
"statusExpired": "Expiré",
@@ -7652,7 +7959,7 @@
"connectedSince": "Connecté depuis le {when}",
"connected": "Connexion IA partagée reliée",
"disconnected": "Connexion IA partagée supprimée",
- "error": "Une erreur s'est produite — veuillez réessayer"
+ "error": "Une erreur s'est produite. Veuillez réessayer"
},
"failingJobs": {
"title": "Tâches en arrière-plan",
@@ -7720,7 +8027,9 @@
"restartNote": "Redémarre HealthLog après la modification.",
"lastResort": "Dernier recours, seulement si tu es sûr que la clé est la bonne et qu'aucune des trois solutions ne convient : définis {setting} et redémarre. HealthLog sert alors quand même et se contente de journaliser le constat. Cela désactive une vérification de sécurité : avec une mauvaise clé, chaque valeur chiffrée échoue et les nouvelles données sont écrites sous cette clé.",
"codes": "Pour l'administrateur : les requêtes API répondent 503 avec le code d'erreur {code}, et /api/health indique la raison {reason}."
- }
+ },
+ "loginOverviewCardTitle": "Événements de connexion",
+ "userColumn": "Utilisateur"
},
"achievements": {
"title": "Réussites",
@@ -7736,7 +8045,9 @@
"noneUnlockedYet": "Aucune réussite débloquée pour l’instant.",
"pointsValue": "{points} points",
"metricCount": "{count}",
- "metricDays": "{count} jours",
+ "metricDaysOne": "{count} jour",
+ "metricDaysFew": "{count} jours",
+ "metricDaysOther": "{count} jours",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -7759,14 +8070,6 @@
"title": "Légende des prises",
"description": "Enregistrez 300 prises de médicaments."
},
- "overIntake1": {
- "title": "Double dose",
- "description": "Prenez votre médicament au moins une fois de plus que prévu."
- },
- "skippedIntake1": {
- "title": "Pause prise",
- "description": "Sautez au moins une prise prévue."
- },
"passkeyCreated1": {
"title": "Clé d’accès configurée",
"description": "Créez votre première clé d’accès."
@@ -7792,19 +8095,19 @@
"description": "Prenez tous vos médicaments à l’heure pendant 1 jour."
},
"compliance801": {
- "title": "Observance 80% · 1j",
+ "title": "Observance de 80% pendant 1 jour",
"description": "Atteignez au moins 80% d’observance sur 30 jours pendant 1 jour."
},
"bmiGreen1": {
- "title": "IMC au vert · 1j",
+ "title": "IMC au vert pendant 1 jour",
"description": "Maintenez votre IMC dans la plage normale pendant 1 jour."
},
"bpGreen1": {
- "title": "Tension au vert · 1j",
+ "title": "Tension au vert pendant 1 jour",
"description": "Maintenez votre tension dans la plage normale pendant 1 jour."
},
"pulseGreen1": {
- "title": "Pouls au vert · 1j",
+ "title": "Pouls au vert pendant 1 jour",
"description": "Maintenez votre pouls au repos dans la plage normale pendant 1 jour."
},
"onTimePerfect7": {
@@ -7812,19 +8115,19 @@
"description": "Prenez tous vos médicaments à l’heure pendant 7 jours d’affilée."
},
"compliance807": {
- "title": "Observance 80% · 7j",
+ "title": "Observance de 80% pendant 7 jours",
"description": "Atteignez au moins 80% d’observance sur 30 jours pendant 7 jours d’affilée."
},
"bmiGreen7": {
- "title": "IMC au vert · 7j",
+ "title": "IMC au vert pendant 7 jours",
"description": "Maintenez votre IMC dans la plage normale pendant 7 jours d’affilée."
},
"bpGreen7": {
- "title": "Tension au vert · 7j",
+ "title": "Tension au vert pendant 7 jours",
"description": "Maintenez votre tension dans la plage normale pendant 7 jours d’affilée."
},
"pulseGreen7": {
- "title": "Pouls au vert · 7j",
+ "title": "Pouls au vert pendant 7 jours",
"description": "Maintenez votre pouls au repos dans la plage normale pendant 7 jours d’affilée."
},
"onTimePerfect30": {
@@ -7832,19 +8135,19 @@
"description": "Prenez tous vos médicaments à l’heure pendant 30 jours d’affilée."
},
"compliance8030": {
- "title": "Observance 80% · 30j",
+ "title": "Observance de 80% pendant 30 jours",
"description": "Atteignez au moins 80% d’observance sur 30 jours pendant 30 jours d’affilée."
},
"bmiGreen30": {
- "title": "IMC au vert · 30j",
+ "title": "IMC au vert pendant 30 jours",
"description": "Maintenez votre IMC dans la plage normale pendant 30 jours d’affilée."
},
"bpGreen30": {
- "title": "Tension au vert · 30j",
+ "title": "Tension au vert pendant 30 jours",
"description": "Maintenez votre tension dans la plage normale pendant 30 jours d’affilée."
},
"pulseGreen30": {
- "title": "Pouls au vert · 30j",
+ "title": "Pouls au vert pendant 30 jours",
"description": "Maintenez votre pouls au repos dans la plage normale pendant 30 jours d’affilée."
},
"onTimePerfect180": {
@@ -7852,19 +8155,19 @@
"description": "Prenez tous vos médicaments à l’heure pendant 180 jours d’affilée."
},
"compliance80180": {
- "title": "Observance 80% · 180j",
+ "title": "Observance de 80% pendant 180 jours",
"description": "Atteignez au moins 80% d’observance sur 30 jours pendant 180 jours d’affilée."
},
"bmiGreen180": {
- "title": "IMC au vert · 180j",
+ "title": "IMC au vert pendant 180 jours",
"description": "Maintenez votre IMC dans la plage normale pendant 180 jours d’affilée."
},
"bpGreen180": {
- "title": "Tension au vert · 180j",
+ "title": "Tension au vert pendant 180 jours",
"description": "Maintenez votre tension dans la plage normale pendant 180 jours d’affilée."
},
"pulseGreen180": {
- "title": "Pouls au vert · 180j",
+ "title": "Pouls au vert pendant 180 jours",
"description": "Maintenez votre pouls au repos dans la plage normale pendant 180 jours d’affilée."
},
"onTimePerfect360": {
@@ -7872,19 +8175,19 @@
"description": "Prenez tous vos médicaments à l’heure pendant 360 jours d’affilée."
},
"compliance80360": {
- "title": "Observance 80% · 360j",
+ "title": "Observance de 80% pendant 360 jours",
"description": "Atteignez au moins 80% d’observance sur 30 jours pendant 360 jours d’affilée."
},
"bmiGreen360": {
- "title": "IMC au vert · 360j",
+ "title": "IMC au vert pendant 360 jours",
"description": "Maintenez votre IMC dans la plage normale pendant 360 jours d’affilée."
},
"bpGreen360": {
- "title": "Tension au vert · 360j",
+ "title": "Tension au vert pendant 360 jours",
"description": "Maintenez votre tension dans la plage normale pendant 360 jours d’affilée."
},
"pulseGreen360": {
- "title": "Pouls au vert · 360j",
+ "title": "Pouls au vert pendant 360 jours",
"description": "Maintenez votre pouls au repos dans la plage normale pendant 360 jours d’affilée."
},
"moodFirst": {
@@ -7968,15 +8271,15 @@
"description": "Vous avez changé la langue de l’application au moins une fois."
},
"missFree7": {
- "title": "Aucune dose oubliée · 7j",
+ "title": "Aucune dose oubliée en 7 jours",
"description": "7 jours consécutifs sans dose oubliée."
},
"missFree30": {
- "title": "Aucune dose oubliée · 30j",
+ "title": "Aucune dose oubliée en 30 jours",
"description": "30 jours consécutifs sans dose oubliée."
},
"missFree90": {
- "title": "Aucune dose oubliée · 90j",
+ "title": "Aucune dose oubliée en 90 jours",
"description": "90 jours consécutifs sans dose oubliée."
},
"measurementWeeks4": {
@@ -7985,10 +8288,10 @@
},
"selfContextComplete": {
"title": "Auto-déclaration à jour",
- "description": "Les quatre champs de l’auto-déclaration sont remplis — le Coach connaît votre contexte."
+ "description": "Les quatre champs de l’auto-déclaration sont remplis : le Coach connaît votre contexte."
},
"sleepLog7": {
- "title": "Journal de sommeil · 7j",
+ "title": "Journal de sommeil pendant 7 jours",
"description": "Données de sommeil enregistrées pendant 7 jours consécutifs."
}
},
@@ -8007,7 +8310,7 @@
},
"hiddenCard": {
"title": "Réussite cachée",
- "description": "Continuez à suivre — vous pourriez bien tomber dessus.",
+ "description": "Continuez à suivre, vous pourriez bien tomber dessus.",
"ariaLabel": "Réussite cachée verrouillée"
},
"hiddenUnlockToast": {
@@ -8016,7 +8319,7 @@
"dashboardCard": {
"title": "Débloqués récemment",
"viewAll": "Tout voir",
- "empty": "Pas encore de réussites — continuez à suivre pour débloquer la première."
+ "empty": "Pas encore de réussites. Continuez à suivre pour débloquer la première."
}
},
"notifications": {
@@ -8078,9 +8381,6 @@
"enabledToast": "Rappels proactifs du Coach activés.",
"disabledToast": "Rappels proactifs du Coach désactivés.",
"saveError": "Impossible d'enregistrer. Veuillez réessayer plus tard.",
- "suggestionsLabel": "Suggestions d'exemples",
- "suggestionsDesc": "Affiche de temps en temps une question d'exemple prête à l'emploi sur l'écran du Coach.",
- "suggestionsAria": "Activer ou désactiver les suggestions d'exemples du Coach",
"aiComposedLabel": "Laisser le Coach rédiger le message",
"aiComposedDesc": "Quand un fournisseur est disponible, le message proactif est rédigé par le modèle plutôt que par le modèle de texte fixe. Désactivé par défaut.",
"aiComposedAria": "Activer ou désactiver le message proactif rédigé par l'IA"
@@ -8151,7 +8451,46 @@
"loginRequired": "Connecte-toi pour gérer les notifications.",
"someChannelsGloballyDisabled": "Un ou plusieurs canaux sont désactivés dans les paramètres et ne recevront pas de notifications.",
"reminderSettingsLink": "Réglages des rappels",
- "channelsLink": "Canaux"
+ "channelsLink": "Canaux",
+ "handover": {
+ "claimedTitle": "{name} a maintenant son propre compte",
+ "claimedEnd": "{name} a repris son dossier de santé. Votre accès a pris fin.",
+ "claimedRead": "{name} a repris son dossier de santé. Vous pouvez encore le consulter, mais plus le modifier.",
+ "claimedManage": "{name} a repris son dossier de santé. Vous pouvez encore le gérer jusqu'à nouvelle décision.",
+ "changedTitle": "{name} a modifié votre accès",
+ "changedEnd": "Votre accès au dossier de santé de {name} a pris fin.",
+ "changedRead": "Vous pouvez désormais consulter le dossier de santé de {name}, sans le modifier.",
+ "changedManage": "Vous pouvez gérer le dossier de santé de {name}.",
+ "unnamed": {
+ "claimedTitle": "La personne dont vous vous occupiez a maintenant son propre compte",
+ "claimedEnd": "La personne dont vous vous occupiez a repris son dossier de santé. Votre accès a pris fin.",
+ "claimedRead": "La personne dont vous vous occupiez a repris son dossier de santé. Vous pouvez toujours le consulter, mais plus le modifier.",
+ "claimedManage": "La personne dont vous vous occupiez a repris son dossier de santé. Vous pouvez toujours le gérer jusqu'à ce qu'elle en décide autrement.",
+ "changedTitle": "Votre accès à un dossier de santé a changé",
+ "changedEnd": "Votre accès au dossier de santé de la personne dont vous vous occupiez a pris fin.",
+ "changedRead": "Vous pouvez désormais consulter le dossier de santé de la personne dont vous vous occupiez, mais pas le modifier.",
+ "changedManage": "Vous pouvez gérer le dossier de santé de la personne dont vous vous occupiez."
+ }
+ },
+ "links": {
+ "open": "Ouvrir dans HealthLog"
+ },
+ "quickActions": {
+ "moodGood": "Bien",
+ "moodOkay": "Moyen",
+ "moodBad": "Mal"
+ },
+ "content": {
+ "title": "Valeurs de santé dans les notifications",
+ "description": "Si une notification peut afficher des valeurs comme ton score du matin.",
+ "detail": "Désactivé, une notification dit seulement de quoi il s'agit, l'écran verrouillé n'affiche donc aucune valeur de santé. Activé, elle ajoute les valeurs, par exemple le score du matin dans le récapitulatif. Toucher une notification ouvre le bon endroit de l'app dans les deux cas.",
+ "toggleAria": "Afficher les valeurs de santé dans les notifications",
+ "statusDetailed": "Avec valeurs",
+ "statusGeneric": "Sans valeurs",
+ "detailedToast": "Les notifications incluent désormais des valeurs de santé.",
+ "genericToast": "Les notifications n'incluent plus de valeurs de santé.",
+ "saveError": "Enregistrement impossible. Réessaie plus tard."
+ }
},
"i18n": {
"maintainershipBanner": {
@@ -8182,7 +8521,7 @@
}
},
"edit": {
- "title": "Modifier la plage cible — {metric}",
+ "title": "Modifier la plage cible : {metric}",
"description": "Ajuste la plage cible utilisée dans les graphiques, les insights et le rapport médical. Valeurs en {unit}.",
"derivedHint": "Cette carte est dérivée d’autres métriques. Modifie plutôt les objectifs sous-jacents (p. ex. tension ou poids).",
"minLabel": "Min",
@@ -8206,15 +8545,19 @@
"currentValue": "Actuel : {value} {unit}",
"targetRangeValue": "Plage cible : {min}–{max} {unit}",
"sourceLabel": "Source : {source}",
+ "sourceNames": {
+ "moodEntries": "Entrées d’humeur",
+ "sevenDays": "7 jours"
+ },
"inTarget": "Dans la plage cible",
"aboveTarget": "{delta} {unit} au-dessus de la plage",
"belowTarget": "{delta} {unit} en dessous de la plage",
"diastolic": "Diastolique",
- "glucoseFasting": "Glycémie — à jeun",
- "glucosePostprandial": "Glycémie — postprandiale",
- "glucoseRandom": "Glycémie — indifférente",
- "glucoseBedtime": "Glycémie — avant le coucher",
- "glucoseUnspecified": "Glycémie — non précisée",
+ "glucoseFasting": "Glycémie (à jeun)",
+ "glucosePostprandial": "Glycémie (postprandiale)",
+ "glucoseRandom": "Glycémie (indifférente)",
+ "glucoseBedtime": "Glycémie (avant le coucher)",
+ "glucoseUnspecified": "Glycémie (non précisée)",
"status": {
"underweight": "Insuffisance pondérale",
"normal": "Normal",
@@ -8300,7 +8643,7 @@
"askMedicationName": "Indique le médicament, p. ex. pris Ramipril.",
"medicationNotFoundExact": "Médicament introuvable. Envoie le nom exactement tel qu’il apparaît dans HealthLog.",
"helpHeader": "Commandes disponibles :",
- "helpBody": "/help — afficher cette aide\n/add — enregistrer une prise\n/start — démarrer le bot\n\nCommandes texte :\ntaken <nom> — confirmer une prise\n\nBoutons dans les rappels :\n• Pris — confirmer la prise\n• 🕐 1h / 🕐 3h — reporter le rappel\n• ⏭ Ignorer — ignorer la prise\n• ✓ Confirmer — confirmer une prise manquée",
+ "helpBody": "/help : afficher cette aide\n/add : enregistrer une prise\n/start : démarrer le bot\n\nCommandes texte :\ntaken <nom> : confirmer une prise\n\nBoutons dans les rappels :\n• Pris : confirmer la prise\n• 🕐 1h / 🕐 3h : reporter le rappel\n• ⏭ Ignorer : ignorer la prise\n• ✓ Confirmer : confirmer une prise manquée",
"measureValuePrompt": "Répondez avec la valeur pour l’enregistrer (facultatif).",
"buttonMeasureValue": "Valeur…",
"measureValueSaved": "Mesure enregistrée.",
@@ -8325,9 +8668,9 @@
},
"lowStockReminders": {
"title": "Stock faible : {medName}",
- "bodyToday": "{medName} : {units} unités restantes — moins d’un jour de réserve.",
- "bodyReorder": "{medName} : {units} unités restantes. Le stock sera épuisé vers le {runsOutOn} — recommandez avant le {reorderBy}.",
- "bodyLastDose": "{medName} : {units} unités restantes — environ votre dernière dose. Le stock sera épuisé vers le {runsOutOn}.",
+ "bodyToday": "{medName} : {units} unités restantes, moins d’un jour de réserve.",
+ "bodyReorder": "{medName} : {units} unités restantes. Le stock sera épuisé vers le {runsOutOn}, recommandez avant le {reorderBy}.",
+ "bodyLastDose": "{medName} : {units} unités restantes, environ votre dernière dose. Le stock sera épuisé vers le {runsOutOn}.",
"bodyExpired": "{medName} : plus de stock utilisable. {units} unités sont marquées périmées.",
"bodyAsNeeded": "{medName} : plus que {units} unités. Recommandez avant d’avoir besoin de la prochaine dose."
},
@@ -8354,11 +8697,11 @@
"questionConditions": "Avez-vous des maladies chroniques que le Coach devrait connaître ?",
"questionFocus": "Y a-t-il quelque chose de précis auquel le Coach devrait veiller ?",
"questionAllergies": "Avez-vous des allergies ou des intolérances ?",
- "questionAboutMe": "Envie de parler un peu de vous au Coach — habitudes, objectifs, contexte ?"
+ "questionAboutMe": "Envie de parler un peu de vous au Coach (habitudes, objectifs, contexte) ?"
},
"clinicianView": {
"title": "Dossier de santé partagé",
- "period": "Période couverte : {start} – {end}",
+ "period": "Période couverte : {start}–{end}",
"expires": "Ce lien expire le {date}.",
"provenance": "Il s'agit d'un résumé en lecture seule partagé par la personne concernée, exporté de ses données HealthLog auto-enregistrées. Les valeurs sont auto-déclarées et peuvent inclure des mesures synchronisées depuis des appareils ; à considérer comme rapportées par le patient, non comme un examen clinique.",
"labs": "Valeurs de laboratoire",
@@ -8382,7 +8725,7 @@
"strain": "Score de charge",
"score": "Score de bien-être"
},
- "footer": "Partagé depuis HealthLog — un suivi de santé auto-hébergé.",
+ "footer": "Partagé depuis HealthLog, un suivi de santé auto-hébergé.",
"unlock": {
"title": "Saisir la phrase secrète",
"description": "Ce dossier de santé partagé est protégé. Saisissez la phrase secrète qui vous a été communiquée pour le consulter.",
@@ -8417,7 +8760,11 @@
},
"course": "Traitement {n}",
"courseRange": "du {start} au {end}",
- "courseSince": "depuis le {start}"
+ "courseSince": "depuis le {start}",
+ "unavailable": {
+ "title": "Lien expiré ou invalide",
+ "description": "Ce lien de partage a expiré, a été révoqué ou est incomplet. Demandez un nouveau lien à la personne qui l’a partagé."
+ }
},
"cycle": {
"symptomCategory": {
@@ -8470,7 +8817,7 @@
},
"crosstab": {
"description": "Comment chaque mesure se compare entre tes phases lutéale et folliculaire, sur tes propres données.",
- "pairLabel": "{metric} · lutéale vs folliculaire",
+ "pairLabel": "{metric} : phase lutéale vs folliculaire",
"detail": "Lutéale {lutealAvg} {unit} ({lutealDays} jours) vs folliculaire {follicularAvg} {unit} ({follicularDays} jours).",
"metricRestingHeartRate": "Fréquence cardiaque au repos",
"metricHrv": "Variabilité de la fréquence cardiaque",
@@ -8593,8 +8940,8 @@
"ariaUnknown": "Pas encore de cycle actif",
"caption": "de ton cycle",
"firstPeriodCta": "Enregistrer mes premières règles",
- "periodOverdue": "Tes règles arrivent plus tard que d’habitude — le compteur de jours est en pause jusqu’à ce que les prochaines règles soient enregistrées.",
- "periodOverdueDays": "Tes règles arrivent {days} jours plus tard que d’habitude — le compteur de jours est en pause jusqu’à ce que les prochaines règles soient enregistrées.",
+ "periodOverdue": "Tes règles arrivent plus tard que d’habitude. Le compteur de jours est en pause jusqu’à ce que les prochaines règles soient enregistrées.",
+ "periodOverdueDays": "Tes règles arrivent {days} jours plus tard que d’habitude. Le compteur de jours est en pause jusqu’à ce que les prochaines règles soient enregistrées.",
"logPeriodCta": "Enregistrer les règles"
},
"calendar": {
@@ -8606,7 +8953,7 @@
"prevMonth": "Mois précédent",
"nextMonth": "Mois suivant",
"loading": "Chargement de ton calendrier…",
- "learning": "Nous apprenons ton cycle — enregistre au moins trois cycles complets et la fenêtre fertile, l'ovulation et les phases se compléteront. D'ici là, le calendrier n'affiche que ce que tu as enregistré.",
+ "learning": "Nous apprenons ton cycle : enregistre au moins trois cycles complets et la fenêtre fertile, l'ovulation et les phases se compléteront. D'ici là, le calendrier n'affiche que ce que tu as enregistré.",
"legendSymptom": "Symptômes",
"legendOvulationConfirmed": "Ovulation confirmée",
"flowSpotting": "spotting",
@@ -8619,7 +8966,7 @@
"predictions": {
"title": "Ce qui arrive ensuite",
"nextPeriod": "Prochaines règles",
- "nextPeriodWindow": "{low} – {high}",
+ "nextPeriodWindow": "{low}–{high}",
"fertileWindow": "Fenêtre fertile",
"ovulation": "Ovulation estimée",
"confidenceHigh": "Élevée",
@@ -8628,7 +8975,7 @@
"stillLearning": "Encore en apprentissage",
"stillLearningBody": "Enregistre au moins trois cycles complets et tes prévisions s'affineront. D'ici là, ceci reste une estimation approximative.",
"rawMode": "Prévision désactivée",
- "rawModeBody": "Tu as désactivé les prévisions — seules tes données enregistrées sont affichées, sans prévision.",
+ "rawModeBody": "Tu as désactivé les prévisions : seules tes données enregistrées sont affichées, sans prévision.",
"none": "Pas encore de prévision. Enregistre des règles pour commencer.",
"ovulationConfirmed": "Ovulation (confirmée)"
},
@@ -8661,7 +9008,7 @@
"symptoms": "Symptômes",
"temperature": "Température basale",
"temperatureDisturbed": "Marquer la mesure comme perturbée",
- "temperatureDisturbedHint": "Fièvre, maladie, alcool ou mesure tardive — exclue de l'évaluation de la température.",
+ "temperatureDisturbedHint": "Fièvre, maladie, alcool ou mesure tardive : exclue de l'évaluation de la température.",
"ovulationTest": "Test d'ovulation",
"mucus": "Glaire cervicale",
"cervix": "Col de l'utérus",
@@ -8690,7 +9037,7 @@
"temperatureSection": "Température et ovulation",
"intimacySection": "Intimité et contraception",
"testsSection": "Tests",
- "phaseDayHeader": "Jour {day} · {phase}",
+ "phaseDayHeader": "Jour {day}, {phase}",
"bbtHint": "Prends-la dès le matin, avant de te lever.",
"titleForDate": "Saisie du {date}",
"descriptionToday": "Aujourd'hui. Flux, symptômes et notes.",
@@ -8757,15 +9104,15 @@
"fieldInfo": {
"bbt": "Ta température au repos, prise dès le matin avant de te lever. Elle augmente légèrement (environ 0,2–0,3 °C) après l'ovulation.",
"bbtLabel": "Qu'est-ce que la température basale ?",
- "mucus": "Les pertes vaginales évoluent au fil du cycle — de sèches ou collantes à crémeuses, puis aqueuses et comme du blanc d'œuf autour des jours les plus fertiles.",
+ "mucus": "Les pertes vaginales évoluent au fil du cycle, de sèches ou collantes à crémeuses, puis aqueuses et comme du blanc d'œuf autour des jours les plus fertiles.",
"mucusLabel": "Qu'est-ce que la glaire cervicale ?",
- "ovulationTest": "Un test urinaire à domicile qui détecte le pic de LH — la montée hormonale environ 24–36 h avant l'ovulation. Un test positif signale la période la plus fertile.",
+ "ovulationTest": "Un test urinaire à domicile qui détecte le pic de LH, la montée hormonale environ 24–36 h avant l'ovulation. Un test positif signale la période la plus fertile.",
"ovulationTestLabel": "Qu'est-ce qu'un test d'ovulation ?",
"spotting": "Léger saignement en dehors des règles.",
"spottingLabel": "Qu'est-ce que le spotting entre les règles ?",
"pregnancyTest": "Résultat d'un test de grossesse (hCG) à domicile.",
"pregnancyTestLabel": "Qu'est-ce qu'un test de grossesse ?",
- "progesteroneTest": "Un test confirmant que l'ovulation a eu lieu — la progestérone augmente ensuite.",
+ "progesteroneTest": "Un test confirmant que l'ovulation a eu lieu : la progestérone augmente ensuite.",
"progesteroneTestLabel": "Qu'est-ce qu'un test de progestérone ?",
"intercourse": "Si un rapport sexuel a eu lieu ce jour-là.",
"intercourseLabel": "Que suit le rapport ?",
@@ -8818,14 +9165,14 @@
"postpone": {
"menuItem": "Reporter…",
"title": "Reporter l'échéance",
- "description": "Ne déplace que l'échéance actuelle — le rythme régulier reste inchangé.",
+ "description": "Ne déplace que l'échéance actuelle ; le rythme régulier reste inchangé.",
"chip": "+{days} jours",
"dateLabel": "Nouvelle date",
"confirm": "Reporter"
},
"skip": {
"heading": "Passer cette échéance",
- "explainer": "Enregistré honnêtement comme passé — pas comme fait. L'intervalle redémarre à partir d'aujourd'hui.",
+ "explainer": "Enregistré honnêtement comme passé, pas comme fait. L'intervalle redémarre à partir d'aujourd'hui.",
"action": "Passer",
"confirmTitle": "Passer cette échéance ?"
},
@@ -8867,10 +9214,10 @@
"VISCERAL_FAT": "Graisse viscérale",
"BODY_MASS_INDEX": "Indice de masse corporelle (IMC)",
"WAIST_CIRCUMFERENCE": "Tour de taille",
- "PHQ9_SCORE": "PHQ-9 · humeur",
- "GAD7_SCORE": "GAD-7 · inquiétude",
- "WHO5_SCORE": "WHO-5 · bien-être",
- "SCI_SCORE": "SCI · sommeil"
+ "PHQ9_SCORE": "PHQ-9 (humeur)",
+ "GAD7_SCORE": "GAD-7 (inquiétude)",
+ "WHO5_SCORE": "WHO-5 (bien-être)",
+ "SCI_SCORE": "SCI (sommeil)"
},
"typeGroups": {
"vitals": "Constantes vitales",
@@ -8966,7 +9313,7 @@
"localOcr": {
"heading": "Lire les scans sur cet appareil",
"description": "Lit la photo sur votre appareil et n'envoie que le texte extrait à votre fournisseur.",
- "detail": "Utile quand votre fournisseur d'IA ne lit pas les images. Moins précis — vérifiez chaque valeur avant d'enregistrer."
+ "detail": "Utile quand votre fournisseur d'IA ne lit pas les images. Moins précis : vérifiez chaque valeur avant d'enregistrer."
},
"sort": {
"heading": "Tri",
@@ -9028,7 +9375,7 @@
"emptyDescription": "Ajoutez une valeur pour suivre ce biomarqueur dans le temps.",
"title": "Biomarqueur",
"genericDescription": "Un biomarqueur biologique que vous suivez. Ajoutez des mesures pour observer son évolution par rapport à la plage de référence.",
- "valuesTitle": "{marker} — toutes les valeurs",
+ "valuesTitle": "{marker} : toutes les valeurs",
"valuesDescription": "Chaque mesure de ce biomarqueur. Modifiez ou supprimez une entrée directement.",
"backToMarker": "Retour à {marker}",
"latestResult": "Dernier résultat"
@@ -9048,7 +9395,7 @@
},
"biomarker": {
"manageTitle": "Biomarqueurs",
- "managerDescription": "Définissez un marqueur une fois — son unité et sa plage sont réutilisées pour chaque mesure.",
+ "managerDescription": "Définissez un marqueur une fois : son unité et sa plage sont réutilisées pour chaque mesure.",
"define": "Définir",
"defineFirst": "Définir votre premier biomarqueur",
"defineTitle": "Définir un biomarqueur",
@@ -9067,7 +9414,7 @@
"form": {
"seedLabel": "Partir d'un marqueur courant",
"seedPlaceholder": "Choisir un marqueur (facultatif)",
- "seedHint": "En choisir un remplit les champs — ajustez la plage à votre laboratoire.",
+ "seedHint": "En choisir un remplit les champs. Ajustez la plage à votre laboratoire.",
"name": "Nom",
"namePlaceholder": "ex. Cholestérol LDL",
"unit": "Unité",
@@ -9081,7 +9428,7 @@
"upperPlaceholder": "Borne supérieure",
"context": "Contexte",
"contextPlaceholder": "Note facultative sur ce marqueur",
- "rangeHint": "Les deux bornes sont facultatives — laissez-en une vide pour une plage ouverte (ex. LDL < 116).",
+ "rangeHint": "Les deux bornes sont facultatives. Laissez-en une vide pour une plage ouverte (ex. LDL < 116).",
"requiredError": "Le nom et l'unité sont requis.",
"rangeOrderError": "La borne inférieure ne doit pas dépasser la borne supérieure.",
"save": "Enregistrer le biomarqueur",
@@ -9146,17 +9493,17 @@
"tsh": "La TSH (hormone thyréostimulante) est libérée par l'hypophyse pour signaler à la thyroïde de produire ses hormones, et elle s'ajuste à la hausse ou à la baisse pour les maintenir en équilibre. Grâce à cette boucle de rétroaction, la TSH est le dépistage de première intention le plus sensible de la fonction thyroïdienne : elle bouge généralement avant que les hormones thyroïdiennes elles-mêmes ne dévient. Une valeur élevée ou abaissée oriente vers une thyroïde hypo ou hyperactive et conduit habituellement à doser directement les hormones thyroïdiennes. La grossesse et l'âge décalent la plage attendue.",
"ft3": "La T3 libre (triiodothyronine) est la forme non liée et biologiquement active de l'hormone thyroïdienne la plus puissante, la fraction disponible pour agir sur les tissus. La majeure partie de la T3 circulante est liée à des protéines de transport et inactive ; la part libre reflète donc la disponibilité réelle de l'hormone. Elle est mesurée pour caractériser la fonction thyroïdienne, surtout avec la TSH et la T4 libre lorsque les résultats sont incertains. Les taux dépendent fortement de la technique de dosage et se lisent dans leur contexte clinique.",
"ft4": "La T4 libre (thyroxine) est la forme non liée de la principale hormone que la thyroïde libère dans le sang ; elle sert de réservoir que les tissus convertissent en T3 plus active selon les besoins. La fraction libre reflète ce qui est réellement disponible pour le corps, indépendamment du taux de protéines de transport. On la mesure généralement avec la TSH pour distinguer un problème de la thyroïde d'un trouble de la signalisation hypophysaire. La grossesse et certains médicaments peuvent la décaler.",
- "ferritin": "La ferritine est la principale protéine de stockage du fer de l'organisme, et son taux sanguin reflète globalement les réserves totales en fer. Une valeur basse oriente vers des réserves épuisées, tandis qu'une valeur élevée peut traduire une surcharge en fer — ou simplement une inflammation, car la ferritine monte aussi comme protéine de la phase aiguë. Pour cette raison, elle s'interprète souvent avec d'autres marqueurs du fer et des signes d'inflammation. C'est l'une des mesures centrales pour évaluer le statut en fer.",
- "transferrin-saturation": "Le coefficient de saturation de la transferrine est le pourcentage de transferrine — la protéine qui transporte le fer dans le sang — actuellement chargé en fer. Il relie la quantité de fer en circulation à la capacité de transport de l'organisme, offrant un instantané de la disponibilité du fer qui complète la vue des réserves donnée par la ferritine. Une saturation basse suggère un fer en quantité insuffisante, tandis qu'une valeur élevée peut indiquer une surcharge. Elle se lit avec la ferritine et le bilan martial complet.",
+ "ferritin": "La ferritine est la principale protéine de stockage du fer de l'organisme, et son taux sanguin reflète globalement les réserves totales en fer. Une valeur basse oriente vers des réserves épuisées, tandis qu'une valeur élevée peut traduire une surcharge en fer, ou simplement une inflammation, car la ferritine monte aussi comme protéine de la phase aiguë. Pour cette raison, elle s'interprète souvent avec d'autres marqueurs du fer et des signes d'inflammation. C'est l'une des mesures centrales pour évaluer le statut en fer.",
+ "transferrin-saturation": "Le coefficient de saturation de la transferrine est le pourcentage de transferrine (la protéine qui transporte le fer dans le sang) actuellement chargé en fer. Il relie la quantité de fer en circulation à la capacité de transport de l'organisme, offrant un instantané de la disponibilité du fer qui complète la vue des réserves donnée par la ferritine. Une saturation basse suggère un fer en quantité insuffisante, tandis qu'une valeur élevée peut indiquer une surcharge. Elle se lit avec la ferritine et le bilan martial complet.",
"vitamin-d": "La vitamine D se mesure sous forme de 25-hydroxyvitamine D, la forme de stockage circulante et le marqueur standard du statut global en vitamine D. Le corps la fabrique dans la peau exposée à la lumière du soleil et en absorbe une partie par l'alimentation ; elle soutient l'équilibre du calcium, la santé osseuse et la fonction immunitaire. Les taux varient selon la saison, la latitude, le teint de peau et la supplémentation. Elle est suivie pour estimer dans le temps si les réserves sont suffisantes.",
- "vitamin-b12": "La vitamine B12 (cobalamine) est une vitamine essentielle, obtenue surtout à partir d'aliments d'origine animale et stockée en grande partie dans le foie. Elle est nécessaire à la formation des globules rouges, à la synthèse de l'ADN et à la santé du système nerveux. Une carence prolongée — par faible apport ou absorption altérée — peut entraîner une anémie et des symptômes neurologiques. Elle est mesurée pour évaluer le statut nutritionnel, en particulier lorsque l'apport ou l'absorption peuvent être limités.",
+ "vitamin-b12": "La vitamine B12 (cobalamine) est une vitamine essentielle, obtenue surtout à partir d'aliments d'origine animale et stockée en grande partie dans le foie. Elle est nécessaire à la formation des globules rouges, à la synthèse de l'ADN et à la santé du système nerveux. Une carence prolongée, par faible apport ou absorption altérée, peut entraîner une anémie et des symptômes neurologiques. Elle est mesurée pour évaluer le statut nutritionnel, en particulier lorsque l'apport ou l'absorption peuvent être limités.",
"folate": "Le folate (vitamine B9) est une vitamine du groupe B présente dans les légumes à feuilles vertes, les légumineuses et les aliments enrichis, centrale pour la formation des globules rouges et la synthèse de l'ADN. Il travaille en étroite collaboration avec la vitamine B12, et une carence de l'un ou de l'autre peut produire un type d'anémie similaire. Un apport suffisant en folate est particulièrement important autour du début de la grossesse pour un développement sain du fœtus. Il est mesuré pour évaluer le statut nutritionnel et la cause de certaines anémies.",
"hs-crp": "La CRP ultrasensible mesure la protéine C-réactive, un marqueur que le foie libère en réponse à l'inflammation, avec une technique affinée pour détecter les faibles concentrations pertinentes pour le risque cardiovasculaire. Contrairement à la CRP standard utilisée pour les infections aiguës, la version ultrasensible capte l'inflammation discrète et chronique liée à la maladie vasculaire. Comme toute infection ou blessure récente peut l'élever, elle s'interprète à distance d'une affection aiguë et se confirme souvent par un prélèvement répété. Elle se lit comme un facteur parmi d'autres du risque cardiovasculaire global.",
"creatinine": "La créatinine est un déchet du métabolisme musculaire normal que les reins filtrent du sang à un rythme assez constant. Comme la production est relativement stable, son taux sanguin sert de jauge pratique de la capacité des reins à éliminer les déchets. Les valeurs dépendent aussi de la masse musculaire, de l'hydratation et de l'alimentation ; on les interprète donc généralement avec le débit de filtration estimé. C'est un marqueur central de la fonction rénale.",
"egfr": "Le DFG estimé (débit de filtration glomérulaire) estime la quantité de sang que les reins filtrent chaque minute, calculée à partir de la créatinine, de l'âge et du sexe. Il traduit une valeur brute de créatinine en une mesure plus directement interprétable de la fonction rénale. Un DFG qui baisse au fil du temps est le principal moyen de classer et de suivre les atteintes rénales chroniques. En tant qu'estimation, il se lit comme une tendance plutôt que comme un chiffre exact unique.",
"alt": "L'ALAT (alanine aminotransférase) est une enzyme concentrée dans les cellules du foie, libérée dans le sang lorsque ces cellules sont stressées ou endommagées. Comme elle est relativement spécifique du foie, c'est l'un des principaux marqueurs pour détecter une atteinte ou une inflammation hépatique. Les valeurs peuvent monter avec la stéatose hépatique, une hépatite virale, l'alcool ou certains médicaments. On la lit généralement avec l'ASAT et les autres marqueurs du bilan hépatique.",
"ast": "L'ASAT (aspartate aminotransférase) est une enzyme présente dans le foie, mais aussi dans le muscle, le cœur et les globules rouges, libérée lorsque l'un de ces tissus est lésé. Comme elle est moins spécifique du foie que l'ALAT, les deux s'interprètent ensemble, et leur rapport peut orienter sur l'origine et la nature du problème. Un exercice intense ou une lésion musculaire peuvent l'élever indépendamment du foie. C'est l'une des enzymes standard du bilan hépatique.",
- "ggt": "La GGT (gamma-glutamyl transférase) est une enzyme du foie et des voies biliaires qui monte en particulier lorsque l'écoulement de la bile est entravé ou en cas d'exposition à l'alcool. Elle sert souvent à clarifier pourquoi un autre marqueur hépatique est élevé — par exemple, pour distinguer une origine hépatique ou biliaire. Elle est sensible mais peu spécifique ; on la lit donc dans le contexte du reste du bilan hépatique. C'est l'un des marqueurs utilisés pour évaluer la santé du foie et des voies biliaires.",
+ "ggt": "La GGT (gamma-glutamyl transférase) est une enzyme du foie et des voies biliaires qui monte en particulier lorsque l'écoulement de la bile est entravé ou en cas d'exposition à l'alcool. Elle sert souvent à clarifier pourquoi un autre marqueur hépatique est élevé, par exemple pour distinguer une origine hépatique ou biliaire. Elle est sensible mais peu spécifique ; on la lit donc dans le contexte du reste du bilan hépatique. C'est l'un des marqueurs utilisés pour évaluer la santé du foie et des voies biliaires.",
"sodium": "Le sodium est le principal électrolyte du liquide situé à l'extérieur des cellules et le moteur essentiel de l'équilibre hydrique et du volume sanguin. Le corps le régule étroitement par la soif, les hormones et les reins, de sorte que son taux sanguin reflète davantage l'équilibre entre l'eau et le sel que le seul apport alimentaire. Des valeurs anormales signalent généralement un trouble de l'équilibre de l'eau plutôt que du sodium lui-même. C'est un élément central du bilan métabolique de base et des électrolytes.",
"potassium": "Le potassium est le principal électrolyte à l'intérieur des cellules et il est essentiel à la transmission nerveuse et au rythme régulier du muscle cardiaque. Son taux sanguin est maintenu dans une plage étroite par les reins et les signaux hormonaux, car des valeurs hautes comme basses peuvent perturber l'activité électrique du cœur. Les taux peuvent être influencés par la fonction rénale, l'hydratation et plusieurs médicaments courants. C'est une mesure centrale du bilan des électrolytes.",
"hemoglobin": "L'hémoglobine est la protéine contenant du fer des globules rouges, qui fixe l'oxygène dans les poumons et le transporte vers les tissus. Sa concentration est la principale mesure de la capacité du sang à transporter l'oxygène et le marqueur central pour détecter l'anémie. Des valeurs basses peuvent refléter une perte de sang, un manque de fer ou de vitamines, ou une production réduite, tandis que des valeurs élevées peuvent suivre une déshydratation ou des conditions pauvres en oxygène. C'est une pierre angulaire de la numération formule sanguine.",
@@ -9165,8 +9512,8 @@
"platelets": "Les plaquettes sont de petits fragments cellulaires du sang qui s'agrègent et forment le bouchon initial arrêtant le saignement d'un vaisseau lésé. Leur nombre reflète l'équilibre entre la production dans la moelle osseuse et la consommation ou la destruction. Un nombre bas peut accroître la tendance au saignement, tandis qu'un nombre élevé peut suivre une inflammation ou d'autres stimuli. Elles font partie de la numération formule sanguine et constituent une jauge de base de la capacité de coagulation.",
"apob": "L'apolipoprotéine B est la protéine structurale portée par chacune des particules de lipoprotéines athérogènes ; la doser revient donc à compter directement ces particules plutôt que le cholestérol qu'elles transportent. Comme il y a une molécule d'ApoB sur chaque particule de LDL, de VLDL et apparentée, elle peut capter un risque cardiovasculaire qu'une valeur de cholestérol LDL manque, en particulier lorsque le nombre de particules et leur teneur en cholestérol divergent. C'est pourquoi elle est de plus en plus utilisée pour affiner l'évaluation du risque fondée sur les lipides. Elle s'interprète avec le bilan lipidique habituel.",
"lp-a": "La lipoprotéine(a) est une particule semblable au LDL avec une protéine supplémentaire attachée, dont le taux est en grande partie déterminé par la génétique et reste assez stable toute la vie. Une Lp(a) élevée est un facteur de risque indépendant et héréditaire de maladie cardiovasculaire et de rétrécissement de la valve aortique. Comme elle varie peu, on la mesure généralement une seule fois pour établir le risque à vie d'une personne, plutôt que de la suivre de façon répétée. Elle s'exprime en unités molaires et se lit dans le cadre du risque cardiovasculaire global.",
- "fasting-insulin": "L'insuline à jeun est le taux d'insuline — l'hormone qui fait passer le glucose du sang vers les cellules — mesuré après un jeûne nocturne. Associée à la glycémie à jeun, elle aide à estimer la résistance à l'insuline, l'état dans lequel le corps a besoin de plus d'insuline pour maintenir le glucose dans sa plage. Une insuline à jeun élevée avec une glycémie normale peut être un signe précoce que cette compensation est en cours. Elle se lit dans le cadre d'un bilan de santé métabolique et non isolément.",
- "omega-3-index": "L'indice oméga-3 est la part d'EPA et de DHA — les deux acides gras oméga-3 marins à longue chaîne — parmi les acides gras des membranes des globules rouges. Comme les globules rouges se renouvellent lentement, il reflète l'apport en oméga-3 sur le long terme plutôt qu'un repas récent. Un indice plus élevé a été associé, dans des études d'observation, à un risque cardiovasculaire moindre. Il est suivi comme un marqueur stable du statut en oméga-3, qui répond à l'alimentation sur plusieurs mois."
+ "fasting-insulin": "L'insuline à jeun est le taux d'insuline (l'hormone qui fait passer le glucose du sang vers les cellules) mesuré après un jeûne nocturne. Associée à la glycémie à jeun, elle aide à estimer la résistance à l'insuline, l'état dans lequel le corps a besoin de plus d'insuline pour maintenir le glucose dans sa plage. Une insuline à jeun élevée avec une glycémie normale peut être un signe précoce que cette compensation est en cours. Elle se lit dans le cadre d'un bilan de santé métabolique et non isolément.",
+ "omega-3-index": "L'indice oméga-3 est la part d'EPA et de DHA (les deux acides gras oméga-3 marins à longue chaîne) parmi les acides gras des membranes des globules rouges. Comme les globules rouges se renouvellent lentement, il reflète l'apport en oméga-3 sur le long terme plutôt qu'un repas récent. Un indice plus élevé a été associé, dans des études d'observation, à un risque cardiovasculaire moindre. Il est suivi comme un marqueur stable du statut en oméga-3, qui répond à l'alimentation sur plusieurs mois."
},
"apob": "Apolipoprotéine B (ApoB)",
"lp-a": "Lipoprotéine(a)",
@@ -9184,7 +9531,7 @@
"acceptImageOnly": "JPEG, PNG ou WebP",
"extracting": "Lecture de votre compte rendu…",
"readingOnDevice": "Lecture sur votre appareil…",
- "localModeHint": "Lu sur votre appareil, puis envoyé sous forme de texte. Moins précis — vérifiez chaque valeur.",
+ "localModeHint": "Lu sur votre appareil, puis envoyé sous forme de texte. Moins précis : vérifiez chaque valeur.",
"rangeFromScan": "Valeurs de référence issues du scan",
"rangeVerify": "Vérifiez les valeurs de référence",
"reviewTitle": "Vérifier avant d'enregistrer",
@@ -9199,12 +9546,12 @@
"dateLabel": "Date",
"refLowLabel": "Référence basse",
"refHighLabel": "Référence haute",
- "newBiomarker": "Nouveau biomarqueur — sera créé",
+ "newBiomarker": "Nouveau biomarqueur, sera créé",
"linksExisting": "Lié au {name} existant",
"duplicateWarning": "Une valeur correspondante existe déjà",
"lowConfidence": "Veuillez vérifier cette valeur",
"lowConfidenceBadge": "Confiance faible",
- "valueUnreadable": "Valeur illisible — saisissez-la manuellement",
+ "valueUnreadable": "Valeur illisible : saisissez-la manuellement",
"saveSelected": "Enregistrer {count} sélectionnées",
"discardAll": "Tout abandonner",
"savedToast": "{count} résultats de laboratoire enregistrés",
@@ -9320,6 +9667,10 @@
"vaccinations": {
"label": "Vaccinations",
"description": "Le carnet de vaccination structuré : dose, série, prochain rappel."
+ },
+ "timeline": {
+ "label": "Chronologie",
+ "description": "Des années d’un coup d’œil : maladies, médicaments et consultations à côté de vos valeurs, ainsi que vos propres événements de vie."
}
},
"moduleOff": {
@@ -9351,8 +9702,8 @@
},
"form": {
"date": "Date",
- "catalogLabel": "Vaccin (catalogue)",
- "identityHint": "Choisissez dans le catalogue ou saisissez ci-dessous : l'un suffit.",
+ "catalogLabel": "Vaccin",
+ "identityHint": "Choisissez un vaccin dans la liste ou saisissez-le ci-dessous. L'un suffit.",
"catalogNone": "Choisir un vaccin",
"catalogSearch": "Rechercher des vaccins",
"catalogNoMatch": "Aucune entrée du catalogue trouvée",
@@ -9472,10 +9823,31 @@
"overdue": "Rappel en retard depuis le {date}",
"dueSoon": "Rappel prévu le {date}",
"current": "Prochain rappel le {date}"
+ },
+ "custom": {
+ "title": "Vaccins personnels",
+ "add": "Ajouter un vaccin personnel",
+ "name": "Nom",
+ "components": "Protège contre",
+ "seriesDoses": "Doses de la primovaccination",
+ "boosterMonths": "Rappel tous les (mois)",
+ "saved": "Vaccin enregistré",
+ "deleted": "Vaccin supprimé",
+ "deleteConfirm": "Supprimer ce vaccin ? Les doses déjà saisies gardent leur nom.",
+ "saveFailed": "Le vaccin n’a pas pu être enregistré.",
+ "nameTaken": "Tu as déjà un vaccin portant ce nom.",
+ "editTitle": "Modifier le vaccin personnel",
+ "description": "Un vaccin absent du catalogue, compté comme n'importe quelle entrée du catalogue.",
+ "componentsHint": "Choisissez chaque maladie contre laquelle il protège.",
+ "boosterHint": "Laissez vide s'il n'y a pas de rappel.",
+ "protectsAgainst": "Protège contre {list}.",
+ "deleteTitle": "Retirer ce vaccin personnel ?",
+ "sectionDescription": "Les vaccins que vous avez ajoutés parce que le catalogue ne les connaît pas.",
+ "catalogGroup": "Catalogue"
}
},
"illness": {
- "title": "Journal de maladie",
+ "title": "Maladie",
"subtitle": "Un suivi rétrospectif de vos épisodes de maladie et d'affections.",
"settings": {
"viewDescription": "Choisis si les épisodes s'affichent en cartes ou en liste compacte.",
@@ -9562,7 +9934,7 @@
},
"correlation": {
"title": "Comment cela s'est déroulé",
- "learning": "Encore en apprentissage — enregistre quelques jours de plus pour voir des tendances ici.",
+ "learning": "Encore en apprentissage. Enregistre quelques jours de plus pour voir des tendances ici.",
"preOnsetTitle": "Comment cela s'est annoncé",
"preOnsetBody": "Signaux qui se sont écartés de ta base avant le début que tu as noté.",
"nadirTitle": "Ce qui a changé",
@@ -9589,7 +9961,7 @@
"countOne": "Tu as enregistré 1 épisode sur cette période.",
"countOther": "Tu as enregistré {count} épisodes sur cette période.",
"typicalGap": "Écart de récupération typique : {days} jours entre le mieux-être et la stabilisation de tes valeurs.",
- "typicalGapWithDriver": "Écart de récupération typique : {days} jours – tu te sens généralement mieux avant que {vital} ne se stabilise.",
+ "typicalGapWithDriver": "Écart de récupération typique : {days} jours. Tu te sens généralement mieux avant que {vital} ne se stabilise.",
"typicalGapLearning": "Encore quelques épisodes résolus et un écart de récupération typique apparaîtra ici.",
"none": "Aucun épisode enregistré sur cette période.",
"analyze": "Analyser l'écart de récupération"
@@ -9663,7 +10035,7 @@
"dismiss": "Pas maintenant",
"applied": "Ajouté",
"dismissed": "Ignoré",
- "failed": "Impossible d'ajouter — réessaie",
+ "failed": "Impossible d'ajouter. Réessaie",
"checkup": {
"title": "Ajouter un rappel d'examen ?"
},
@@ -9691,9 +10063,9 @@
"reviewPrefix": "Point d'étape : {when}",
"accept": "Adopter le plan",
"decline": "Refuser",
- "accepted": "Plan adopté — le Coach le gardera en tête.",
+ "accepted": "Plan adopté : le Coach le gardera en tête.",
"declined": "Proposition refusée.",
- "failed": "Échec de l'action — veuillez réessayer.",
+ "failed": "Échec de l'action. Veuillez réessayer.",
"markMet": "Objectif atteint",
"abandon": "Terminer le plan",
"remove": "Supprimer",
@@ -9715,7 +10087,7 @@
},
"refusal": {
"outOfScope": "Je peux uniquement t'aider avec les données de santé de ton journal : mesures, médicaments, humeur, tendances. Je ne réponds pas aux questions en dehors de ce cadre.",
- "promptInjection": "J'ai repéré des instructions qui cherchent à contourner mes consignes. Je suis le Coach HealthLog et je résume uniquement tes propres données de santé — reformule ta question dans ce cadre, s'il te plaît.",
+ "promptInjection": "J'ai repéré des instructions qui cherchent à contourner mes consignes. Je suis le Coach HealthLog et je résume uniquement tes propres données de santé. Reformule ta question dans ce cadre, s'il te plaît.",
"conversationPoisoned": "Un message précédent de cette conversation contient des instructions qui cherchent à contourner mes consignes. Démarre une nouvelle conversation, s'il te plaît."
},
"noRecordedData": "Aucune mesure n'est enregistrée pour ce que tu demandes, je ne peux donc te donner aucun chiffre. Dès que tu en auras saisi, repose-moi la question et je partirai de tes propres valeurs.",
@@ -9726,9 +10098,6 @@
"showResult": "Ouverture d'un tableau précédent",
"reuse": "Reprise du tableau précédent",
"headerActive": "{label}…",
- "headerDoneOne": "1 source consultée",
- "headerDoneFew": "{count} sources consultées",
- "headerDoneOther": "{count} sources consultées",
"listLabel": "Ce que le Coach a consulté",
"toggleShow": "Afficher ce que le Coach a consulté",
"toggleHide": "Masquer ce que le Coach a consulté",
@@ -9754,7 +10123,9 @@
"illness": "Maladie et récupération",
"cycle": "Cycle",
"correlations": "Liens entre indicateurs",
- "snapshot": "Résumé des données"
+ "snapshot": "Résumé des données",
+ "environment": "Météo et air",
+ "day": "Une journée"
},
"window": {
"last7days": "7 derniers jours",
@@ -9878,7 +10249,8 @@
"relatedMetric": "Voir aussi : {metric}",
"continue": "Continuer à chercher",
"settingLabel": "Suggérer des questions de suivi",
- "settingHint": "Affiche jusqu'à trois suggestions sous la dernière réponse du Coach."
+ "settingHint": "Affiche jusqu'à trois suggestions sous la dernière réponse du Coach.",
+ "changeAssumption": "À la place : {value}"
},
"clarify": {
"cardLabel": "Le Coach a une question",
@@ -9891,6 +10263,12 @@
"last90days": "90 derniers jours",
"lastYear": "12 derniers mois",
"allTime": "Toute la période"
+ },
+ "comparison": {
+ "baseline90d": "Comparer avec mes 90 jours habituels"
+ },
+ "anchor": {
+ "illness": "Depuis la maladie du {date}"
}
},
"reuse": {
@@ -9936,10 +10314,10 @@
"saved": "Enregistré",
"error": "Une erreur est survenue. Réessaie.",
"instrument": {
- "phq9": "PHQ-9 · humeur",
- "gad7": "GAD-7 · inquiétude",
- "who5": "WHO-5 · bien-être",
- "sci": "SCI · sommeil"
+ "phq9": "PHQ-9 (humeur)",
+ "gad7": "GAD-7 (inquiétude)",
+ "who5": "WHO-5 (bien-être)",
+ "sci": "SCI (sommeil)"
},
"instrumentSub": {
"phq9": "Dépistage de la dépression, 9 questions",
@@ -9960,7 +10338,7 @@
"2": "Très gênant",
"3": "Extrêmement gênant"
},
- "functionalOptionalHint": "Facultatif — tu peux envoyer sans répondre à cette question.",
+ "functionalOptionalHint": "Facultatif : tu peux envoyer sans répondre à cette question.",
"result": {
"title": "Ton résultat",
"totalLabel": "Score total",
@@ -10380,12 +10758,34 @@
"daylight": "Durée du jour",
"precip": "Précipitations",
"pressureMean": "Pression atmosphérique",
- "pressureDelta": "Variation de pression"
+ "pressureDelta": "Variation de pression",
+ "pm25": "Particules fines (PM2,5)",
+ "pm10": "Particules (PM10)",
+ "no2": "Dioxyde d’azote",
+ "ozone8h": "Ozone (maximum sur 8 heures)",
+ "europeanAqi": "Indice européen de qualité de l’air",
+ "uvIndexMax": "Indice UV (maximum)",
+ "dustMax": "Poussières du Sahara",
+ "pollenMax": "Pollen (valeur la plus élevée)"
+ },
+ "chip": {
+ "day": "Météo du {date}",
+ "pollenHigh": "Beaucoup de pollen : {kinds}",
+ "hotNight": "Nuit chaude",
+ "veryPoorAir": "Air très mauvais"
+ },
+ "pollen": {
+ "alder": "aulne",
+ "birch": "bouleau",
+ "grass": "graminées",
+ "mugwort": "armoise",
+ "olive": "olivier",
+ "ragweed": "ambroisie"
}
},
"documents": {
"title": "Documents",
- "subtitle": "Chaque courrier, compte rendu et scan au même endroit, consultable — chiffré au repos.",
+ "subtitle": "Chaque courrier, compte rendu et scan au même endroit, consultable et chiffré au repos.",
"pageUpload": "Téléverser",
"ownerOnlyUpload": "Seul le titulaire du dossier peut ajouter des documents.",
"kind": {
@@ -10405,11 +10805,13 @@
"searchPlaceholder": "Rechercher un titre ou un nom de fichier…",
"typeGroup": "Type de document",
"conditionGroup": "Affection",
+ "procedureGroup": "Interventions et chirurgies",
"yearGroup": "Année",
"clear": "Effacer les filtres",
"typeAll": "Tous les types",
"typeCount": "{count} types",
"conditionAll": "Toutes les affections",
+ "procedureAll": "Toutes les interventions",
"yearAll": "Toutes les années"
},
"card": {
@@ -10425,15 +10827,15 @@
},
"error": {
"fileTooLarge": "Ce fichier dépasse la limite de {maxSize}.",
- "quotaExceeded": "Le stockage est plein — {used} sur {quota} utilisés.",
+ "quotaExceeded": "Le stockage est plein : {used} sur {quota} utilisés.",
"unsupportedType": "Ce type de fichier n'est pas pris en charge.",
"purged": "Ce document a été définitivement supprimé et ne peut pas être restauré.",
"duplicateExists": "Un document identique est déjà enregistré.",
- "rateLimited": "Trop de téléversements — réessayez dans quelques minutes.",
+ "rateLimited": "Trop de téléversements. Réessayez dans quelques minutes.",
"generic": "Le téléversement a échoué. Veuillez réessayer."
},
"toast": {
- "duplicate": "Déjà enregistré — le document existant est mis en évidence.",
+ "duplicate": "Déjà enregistré. Le document existant est mis en évidence.",
"deleted": "Document supprimé",
"restoreFailed": "Impossible de restaurer le document."
},
@@ -10456,7 +10858,7 @@
},
"empty": {
"title": "Votre coffre à documents est vide",
- "description": "Conservez courriers médicaux, résultats de laboratoire, imagerie, ordonnances et carnets de vaccination — chiffrés et consultables par type, affection et année.",
+ "description": "Conservez courriers médicaux, résultats de laboratoire, imagerie, ordonnances et carnets de vaccination. Ils sont chiffrés et consultables par type, affection et année.",
"action": "Téléverser un premier document",
"noMatchesTitle": "Aucun document correspondant",
"noMatchesDescription": "Rien ne correspond à la recherche et aux filtres actuels."
@@ -10487,6 +10889,7 @@
"saveError": "Impossible d'enregistrer la modification.",
"deleteError": "Impossible de supprimer le document.",
"download": "Télécharger",
+ "openInNewTab": "Ouvrir dans un nouvel onglet",
"delete": "Supprimer",
"titleLabel": "Titre",
"titlePlaceholder": "Ajouter un titre",
@@ -10496,9 +10899,9 @@
"linkCondition": "Lier",
"noConditions": "Lié à aucune affection",
"previewUnavailable": "Pas d'aperçu intégré pour ce type de fichier",
- "uploadedMeta": "Ajouté le {date} · {size}",
+ "uploadedMeta": "Ajouté le {date}, {size}",
"importedFrom": "Importé depuis {system}",
- "importedFromWithId": "Importé depuis {system} · ID {id}",
+ "importedFromWithId": "Importé depuis {system}, ID {id}",
"sourceOther": "un autre système",
"share": "Partager",
"labValuesLink": "Voir {count} valeurs de laboratoire",
@@ -10598,7 +11001,7 @@
"CONDITION": "Affection",
"MEDICATION_STATEMENT": "Médicament"
},
- "statedRange": "plage indiquée {range}",
+ "statedRange": "Plage indiquée {range}",
"needsCheck": "Vérifiez cette valeur par rapport au document, puis enregistrez-la.",
"verify": "Vérifier",
"verifySave": "Enregistrer la valeur",
@@ -10714,6 +11117,21 @@
"finishedHint": "Cherchez à nouveau ou changez un filtre pour choisir d'autres documents.",
"maxReached": "Vous pouvez importer au plus {max} documents à la fois.",
"noMatchHint": "Essayez une partie du nom, ou effacez l'étiquette et les dates."
+ },
+ "customize": "Personnaliser l'affichage",
+ "layout": {
+ "viewDescription": "Des cartes avec aperçu ou une liste compacte.",
+ "arrangementHeading": "Disposition",
+ "arrangementDescription": "Chaque mois dans son propre bloc, ou tout à la suite sur toute la largeur.",
+ "stacked": "Mois empilés",
+ "flow": "En continu",
+ "saveFailed": "Impossible d'enregistrer l'affichage."
+ },
+ "deleteConfirm": {
+ "titleOne": "Supprimer ce document ?",
+ "titleMany": "Supprimer {count} documents ?",
+ "body": "Vous pouvez annuler juste après. Au bout de 30 jours, la suppression est définitive.",
+ "action": "Supprimer"
}
},
"selfDescription": {
@@ -10800,7 +11218,7 @@
"genericDescription": "Une mesure que vous suivez. Ajoutez des valeurs pour la suivre dans le temps.",
"emptyTitle": "Aucune valeur pour l'instant",
"emptyDescription": "Ajoutez votre première valeur pour commencer à représenter cette mesure.",
- "valuesTitle": "{metric} — toutes les valeurs",
+ "valuesTitle": "{metric} : toutes les valeurs",
"valuesDescription": "Toutes les valeurs enregistrées pour cette mesure.",
"backToMetric": "Retour à {metric}"
}
@@ -10847,35 +11265,41 @@
"lastAttemptReason": {
"unitMismatch": "Les valeurs reçues ne correspondaient pas aux unités attendues par l'appli pour ces nutriments, elles n'ont donc pas pu être enregistrées.",
"valueOutOfRange": "Les quantités reçues étaient supérieures à ce que l'appli peut raisonnablement enregistrer, elles n'ont donc pas pu être sauvegardées.",
- "dayInvalid": "Les entrées portaient une date que le serveur n'a pas pu accepter. Cela peut arriver quand l'horloge ou le fuseau horaire du côté qui envoie les données est incorrect — cela vaut la peine de vérifier.",
+ "dayInvalid": "Les entrées portaient une date que le serveur n'a pas pu accepter. Cela peut arriver quand l'horloge ou le fuseau horaire du côté qui envoie les données est incorrect ; cela vaut la peine de vérifier.",
"upsertFailed": "Ces entrées sont arrivées jusqu'au serveur, mais un problème de notre côté a empêché leur enregistrement. Tu n'as rien à faire.",
"unknown": "Ces entrées sont arrivées mais n'ont pas pu être enregistrées."
}
},
"hydration": {
- "referenceMeta": "Apport de référence {value} {unit}/jour · EFSA"
+ "referenceMeta": "Apport de référence {value} {unit}/jour (EFSA)"
},
"caffeine": {
- "ceilingMeta": "Plafond de sécurité {value} {unit}/jour · EFSA"
+ "ceilingMeta": "Plafond de sécurité {value} {unit}/jour (EFSA)"
},
"micronutrients": {
"title": "Vitamines et minéraux",
- "rowMeta": "{days} sur {window} jours · réf. {value} {unit} (EFSA)",
+ "rowMeta": "{days} sur {window} jours, réf. {value} {unit} (EFSA)",
"rowMetaNoRef": "{days} sur {window} jours",
"footer": "{tracked} des {total} micronutriments suivis ont des données sur cette période.",
"emptyTitle": "Aucune donnée de micronutriments synchronisée pour l'instant",
"loadError": "Tes données de nutriments n'ont pas pu être chargées"
},
- "sparseNote": "Une valeur basse ici signifie généralement un suivi partiel, pas une carence — les totaux journaliers Apple Health reflètent ce que vos applis ont enregistré, pas l'apport alimentaire total."
+ "sparseNote": "Une valeur basse ici signifie généralement un suivi partiel, pas une carence : les totaux journaliers Apple Health reflètent ce que vos applis ont enregistré, pas l'apport alimentaire total."
},
"daily": {
"line": {
- "score": "Ton score de santé aujourd'hui est de {score}.",
- "allClear": "Rien ne requiert ton attention aujourd'hui."
+ "allClear": "Rien ne requiert ton attention aujourd'hui.",
+ "scoreReady": "Ton score de santé du jour est prêt."
},
"push": {
"title": "Votre résumé du matin",
- "bodyProvisional": "{line} · Le sommeil de la nuit dernière n'est pas encore disponible."
+ "bodyProvisional": "{line}\nLe sommeil de la nuit dernière n'est pas encore disponible.",
+ "titleScore": "Score du matin {score}",
+ "band": {
+ "green": "Tout semble stable aujourd'hui.",
+ "yellow": "Quelques points méritent un coup d'œil aujourd'hui.",
+ "red": "Certains domaines demandent de l'attention aujourd'hui."
+ }
},
"item": {
"doseWindow": {
@@ -10893,7 +11317,7 @@
},
"syncIssue": {
"title": "La synchronisation demande votre attention",
- "body": "{integration} ne se synchronise pas — reconnecte-le pour garder tes données à jour."
+ "body": "{integration} ne se synchronise pas. Reconnecte-le pour garder tes données à jour."
},
"coachCheckin": {
"title": "Il est temps de revoir un plan",
@@ -10905,10 +11329,10 @@
"tensionWindow": {
"title": "Peut-être un peu de tension",
"body": {
- "morning": "Votre fréquence cardiaque est restée au-dessus de votre valeur de repos un bon moment ce matin alors que vous étiez au calme — peut-être un peu de tension.",
- "afternoon": "Votre fréquence cardiaque est restée au-dessus de votre valeur de repos un bon moment cet après-midi alors que vous étiez au calme — peut-être un peu de tension.",
- "evening": "Votre fréquence cardiaque est restée au-dessus de votre valeur de repos un bon moment ce soir alors que vous étiez au calme — peut-être un peu de tension.",
- "night": "Votre fréquence cardiaque est restée au-dessus de votre valeur de repos un bon moment cette nuit alors que vous étiez au calme — peut-être un peu de tension."
+ "morning": "Votre fréquence cardiaque est restée au-dessus de votre valeur de repos un bon moment ce matin alors que vous étiez au calme, peut-être un peu de tension.",
+ "afternoon": "Votre fréquence cardiaque est restée au-dessus de votre valeur de repos un bon moment cet après-midi alors que vous étiez au calme, peut-être un peu de tension.",
+ "evening": "Votre fréquence cardiaque est restée au-dessus de votre valeur de repos un bon moment ce soir alors que vous étiez au calme, peut-être un peu de tension.",
+ "night": "Votre fréquence cardiaque est restée au-dessus de votre valeur de repos un bon moment cette nuit alors que vous étiez au calme, peut-être un peu de tension."
}
},
"ecgNewRecording": {
@@ -10933,9 +11357,9 @@
},
"upcomingVisit": {
"title": "Rendez-vous à venir",
- "bodyToday": "{what} — aujourd'hui",
- "bodyTomorrow": "{what} — demain",
- "bodyDayAfterTomorrow": "{what} — après-demain"
+ "bodyToday": "{what}, aujourd'hui",
+ "bodyTomorrow": "{what}, demain",
+ "bodyDayAfterTomorrow": "{what}, après-demain"
}
},
"action": {
@@ -10954,10 +11378,34 @@
"scoreLabel": "Score de santé",
"deltaVsBaseline": "{delta} par rapport à ta référence",
"deltaFlat": "Conforme à ta référence",
- "sleepPending": "Le sommeil de la nuit dernière n'est pas encore arrivé — la lecture du jour est provisoire.",
+ "sleepPending": "Le sommeil de la nuit dernière n'est pas encore arrivé : la lecture du jour est provisoire.",
"worthALook": "À regarder",
"allClear": "Rien ne requiert ton attention aujourd'hui.",
- "ringLink": "Ouvrir les détails de {metric}"
+ "ringLink": "Ouvrir les détails de {metric}",
+ "heading": "Aujourd'hui",
+ "steadyWeeksOne": "Stable depuis {count} semaine",
+ "steadyWeeksFew": "Stable depuis {count} semaines",
+ "steadyWeeksOther": "Stable depuis {count} semaines",
+ "valueWithUnit": "{value} {unit}",
+ "rangeWithUnit": "{low} à {high} {unit}",
+ "rangeAtThisTime": "{range} pour ce moment de la journée",
+ "vital": {
+ "RESTING_HEART_RATE": "Fréquence cardiaque au repos",
+ "HEART_RATE_VARIABILITY": "Variabilité de la fréquence cardiaque",
+ "RESPIRATORY_RATE": "Fréquence respiratoire",
+ "OXYGEN_SATURATION": "Saturation en oxygène",
+ "BODY_TEMPERATURE": "Température corporelle",
+ "SKIN_TEMPERATURE": "Température cutanée",
+ "BLOOD_PRESSURE_SYS": "Tension systolique",
+ "BLOOD_PRESSURE_DIA": "Tension diastolique",
+ "BLOOD_GLUCOSE": "Glycémie",
+ "PULSE": "Pouls",
+ "WEIGHT": "Poids",
+ "generic": "Un signe vital"
+ },
+ "steadyAtLeastWeeksOne": "Stable depuis au moins {count} semaine",
+ "steadyAtLeastWeeksFew": "Stable depuis au moins {count} semaines",
+ "steadyAtLeastWeeksOther": "Stable depuis au moins {count} semaines"
},
"milestone": {
"metric": {
@@ -10993,6 +11441,68 @@
"body": "Un mois entier avec votre {metric} dans votre plage habituelle. Merveilleusement stable."
}
}
+ },
+ "lead": {
+ "vitalOutside": {
+ "above": "{metric} est à {value}, au-dessus de ta plage habituelle de {range}.",
+ "below": "{metric} est à {value}, en dessous de ta plage habituelle de {range}."
+ },
+ "vitalsOutside": {
+ "aboveOne": "{metric} est à {value}, au-dessus de ta plage habituelle de {range}, et {count} autre signe vital sort aussi de sa plage.",
+ "aboveFew": "{metric} est à {value}, au-dessus de ta plage habituelle de {range}, et {count} autres signes vitaux sortent aussi de leur plage.",
+ "aboveOther": "{metric} est à {value}, au-dessus de ta plage habituelle de {range}, et {count} autres signes vitaux sortent aussi de leur plage.",
+ "belowOne": "{metric} est à {value}, en dessous de ta plage habituelle de {range}, et {count} autre signe vital sort aussi de sa plage.",
+ "belowFew": "{metric} est à {value}, en dessous de ta plage habituelle de {range}, et {count} autres signes vitaux sortent aussi de leur plage.",
+ "belowOther": "{metric} est à {value}, en dessous de ta plage habituelle de {range}, et {count} autres signes vitaux sortent aussi de leur plage."
+ },
+ "sleep": {
+ "more": "La nuit dernière, tu as dormi {duration}, {diff} de plus que d'habitude.",
+ "less": "La nuit dernière, tu as dormi {duration}, {diff} de moins que d'habitude.",
+ "usual": "La nuit dernière, tu as dormi {duration}, proche de ton habitude.",
+ "plain": "La nuit dernière, tu as dormi {duration}."
+ },
+ "vitalsInRangeOne": "Ton dernier signe vital est dans sa plage habituelle.",
+ "vitalsInRangeFew": "Tes {count} derniers signes vitaux sont tous dans leur plage habituelle.",
+ "vitalsInRangeOther": "Tes {count} derniers signes vitaux sont tous dans leur plage habituelle.",
+ "vitalInRange": "{metric} est à {value}, dans ta plage habituelle de {range}."
+ },
+ "todayFact": {
+ "restMode": {
+ "label": "Mode repos",
+ "value": "Jour {day}"
+ },
+ "medications": {
+ "label": "Médicaments",
+ "value": "{taken} sur {scheduled} pris"
+ },
+ "appointment": {
+ "label": "Rendez-vous",
+ "today": "Aujourd'hui {time}, {what}",
+ "tomorrow": "Demain {time}, {what}"
+ },
+ "sleep": {
+ "label": "La nuit dernière",
+ "plain": "{duration}",
+ "usual": "{duration}, proche de ton habitude",
+ "more": "{duration}, {diff} de plus que d'habitude",
+ "less": "{duration}, {diff} de moins que d'habitude"
+ },
+ "vitals": {
+ "label": "Signes vitaux",
+ "allInOne": "{count} vérifié, dans ta plage",
+ "allInFew": "{count} vérifiés, tous dans ta plage",
+ "allInOther": "{count} vérifiés, tous dans ta plage",
+ "oneOut": {
+ "above": "{metric} au-dessus de ta plage",
+ "below": "{metric} en dessous de ta plage"
+ },
+ "someOut": "{count} sur {total} hors de ta plage"
+ },
+ "cycle": {
+ "label": "Cycle",
+ "value": "{phase}, jour {day}",
+ "dayOnly": "Jour {day}"
+ }
}
},
"reportSelection": {
@@ -11122,7 +11632,7 @@
"errorScope": "Ces rubriques ne peuvent pas être partagées ainsi. Choisissez au moins une rubrique, ou partagez le dossier complet.",
"errorManageBrowserOnly": "Un accès de gestion ne peut être proposé que depuis un navigateur.",
"errorStepUp": "Confirmez votre deuxième facteur, puis réessayez.",
- "errorOffline": "Cela n'a pas atteint le serveur. Votre invitation est toujours là — vérifiez votre connexion et réessayez."
+ "errorOffline": "Cela n'a pas atteint le serveur. Votre invitation est toujours là : vérifiez votre connexion et réessayez."
},
"given": {
"title": "Qui peut ouvrir mon dossier",
@@ -11177,7 +11687,7 @@
"managed": {
"title": "Profils dont vous vous occupez",
"description": "Dossiers de santé sans identifiants propres.",
- "explainer": "Un profil géré est un dossier de santé pour quelqu'un qui ne se connecte pas lui-même — un enfant, ou une personne dont vous vous occupez. Il n'a ni identifiants ni adresse e-mail, et vous en devenez la première personne responsable. Vous pourrez en ajouter d'autres plus tard, et chacune pourra entrer dans le dossier pour le tenir à jour.",
+ "explainer": "Un profil géré est un dossier de santé pour quelqu'un qui ne se connecte pas lui-même, comme un enfant ou une personne dont vous vous occupez. Il n'a ni identifiants ni adresse e-mail, et vous en devenez la première personne responsable. Vous pourrez en ajouter d'autres plus tard, et chacune pourra entrer dans le dossier pour le tenir à jour.",
"nameLabel": "Nom",
"namePlaceholder": "Le nom sous lequel vous reconnaîtrez ce dossier",
"nameHint": "Jusqu'à 80 caractères. Affiché partout où ce dossier apparaît.",
@@ -11199,7 +11709,7 @@
"creating": "Création…",
"created": "{name} est en place. Vous pouvez entrer dans ce dossier depuis le menu du compte.",
"errorStepUp": "Confirmez votre deuxième facteur, puis réessayez.",
- "errorOffline": "Cela n'a pas atteint le serveur. Rien n'a changé — vérifiez votre connexion et réessayez.",
+ "errorOffline": "Cela n'a pas atteint le serveur. Rien n'a changé. Vérifiez votre connexion et réessayez.",
"errorInvalid": "Le serveur a refusé ces informations. Vérifiez le nom, la date et le fuseau horaire.",
"errorFailed": "Le profil n'a pas pu être créé.",
"edit": "Modifier",
@@ -11210,7 +11720,7 @@
"editErrorFailed": "Les modifications n'ont pas pu être enregistrées.",
"delete": "Supprimer le profil",
"deleteTitle": "Supprimer {name} ?",
- "deleteBody": "Tout ce qui a été enregistré pour {name} disparaît avec le profil — mesures, médicaments, documents et notes. Cette action est irréversible.",
+ "deleteBody": "Tout ce qui a été enregistré pour {name} disparaît avec le profil : mesures, médicaments, documents et notes. Cette action est irréversible.",
"deleteGuardiansOne": "{count} personne s'occupe aujourd'hui de ce dossier et le perdra.",
"deleteGuardiansFew": "{count} personnes s'occupent aujourd'hui de ce dossier et le perdront.",
"deleteGuardiansOther": "{count} personnes s'occupent aujourd'hui de ce dossier et le perdront.",
@@ -11239,7 +11749,47 @@
"guardianErrorFailed": "L'invitation n'a pas pu être envoyée.",
"errorMfaRequired": "Cela demande un deuxième facteur, et votre compte n'en a pas encore. Configurez-en un dans Réglages → Sécurité, puis réessayez.",
"rosterLoadError": "Impossible de charger qui s'occupe de ce dossier.",
- "errorRateLimit": "Trop de profils créés dans la dernière heure. Patientez un peu et réessayez."
+ "errorRateLimit": "Trop de profils créés dans la dernière heure. Patientez un peu et réessayez.",
+ "handover": {
+ "action": "Transmettre",
+ "title": "Transmettre à {name}",
+ "description": "Donnez à {name} sa propre connexion. Rien n'est copié ni déplacé : le dossier devient son compte.",
+ "guardiansLabel": "Accès après la transmission",
+ "guardiansHint": "Votre proposition. {name} la voit et décide après la connexion.",
+ "access": {
+ "end": "Aucun accès",
+ "read": "Lecture seule",
+ "manage": "Gérer"
+ },
+ "expiryLabel": "Lien valable pendant",
+ "expiry1": "1 jour",
+ "expiry7": "7 jours",
+ "expiry14": "14 jours",
+ "minorHint": "{name} a moins de 16 ans. Garder l'accès en lecture et parler de cette étape ensemble vaut la peine.",
+ "irreversible": "Dès que {name} utilise le lien, la transmission ne peut plus être annulée. Le dossier lui appartient et c'est à elle ou lui de décider qui garde l'accès.",
+ "afterwards": "{name} confirme personnellement l'avertissement et tout consentement à l'IA. Les rappels de ce dossier ne vous parviennent plus.",
+ "replaceNote": "Créer un nouveau lien retire celui qui est ouvert.",
+ "create": "Créer le lien",
+ "creating": "Création…",
+ "linkOnce": "Ce lien n'est affiché que maintenant. Donnez-le à {name} en personne ou par un canal de confiance, ou laissez scanner le code.",
+ "qrAlt": "Code QR du lien de transmission pour {name}",
+ "urlLabel": "Lien de transmission",
+ "copy": "Copier",
+ "copied": "Lien copié",
+ "copyError": "Le lien n'a pas pu être copié. Sélectionnez-le et copiez-le à la main.",
+ "linkExpires": "Valable jusqu'au {date}.",
+ "done": "Terminé",
+ "pending": "Un lien de transmission est ouvert jusqu'au {date}.",
+ "replace": "Nouveau lien",
+ "revoke": "Retirer le lien",
+ "revoked": "Lien retiré",
+ "revokeFailed": "Le lien n'a pas pu être retiré. Réessayez.",
+ "errorFailed": "Le lien de transmission n'a pas pu être créé.",
+ "errorRosterChanged": "Entre-temps, les personnes qui s'occupent du dossier ont changé. Fermez ceci et rouvrez-le.",
+ "oidcOnly": "Transmettre un profil n'est pas possible tant que ce serveur n'autorise que l'authentification unique.",
+ "loadError": "L'état de la transmission n'a pas pu être chargé.",
+ "entryHint": "Quand {name} souhaite tenir ce dossier de façon autonome."
+ }
},
"toast": {
"savedTo": "Enregistré dans le dossier de {name}"
@@ -11358,7 +11908,13 @@
"symptomEventDelete": "{name} a supprimé une entrée de symptôme",
"symptomDefinitionCreate": "{name} a ajouté un symptôme",
"symptomDefinitionUpdate": "{name} a modifié un symptôme",
- "symptomDefinitionDelete": "{name} a retiré un symptôme"
+ "symptomDefinitionDelete": "{name} a retiré un symptôme",
+ "customVaccineCreate": "{name} a ajouté un vaccin personnel",
+ "customVaccineUpdate": "{name} a modifié un vaccin personnel",
+ "customVaccineDelete": "{name} a retiré un vaccin personnel",
+ "lifeEventCreate": "{name} a ajouté un événement de vie",
+ "lifeEventUpdate": "{name} a modifié un événement de vie",
+ "lifeEventDelete": "{name} a supprimé un événement de vie"
},
"section": {
"measurements": {
@@ -11375,7 +11931,7 @@
},
"profile": {
"label": "Antécédents de santé",
- "detail": "Allergies, antécédents familiaux, les informations de votre profil de santé, vos consultations et cabinets, ainsi que votre carnet de vaccination."
+ "detail": "Allergies, antécédents familiaux, les informations de votre profil de santé, vos consultations et cabinets ainsi que votre carnet de vaccination."
},
"illness": {
"label": "Maladies",
@@ -11396,7 +11952,7 @@
},
"actionError": {
"retry": "Réessayer",
- "offline": "Cela n'a pas atteint le serveur. Rien n'a changé — vérifiez votre connexion et réessayez.",
+ "offline": "Cela n'a pas atteint le serveur. Rien n'a changé : vérifiez votre connexion et réessayez.",
"rateLimited": "Trop de tentatives d'affilée. Attendez une minute et réessayez.",
"acceptExpired": "Cette invitation a expiré. Demandez-en une nouvelle.",
"acceptWithdrawn": "Cette invitation a été retirée.",
@@ -11408,12 +11964,28 @@
"notFound": "Cet accès n'existe plus.",
"revokeFailed": "L'accès n'a pas pu être terminé.",
"renounceFailed": "L'accès n'a pas pu être rendu.",
- "profileNotFound": "Ce profil n'existe plus — il a peut-être été supprimé, ou votre accès a pris fin.",
+ "profileNotFound": "Ce profil n'existe plus. Il a peut-être été supprimé, ou votre accès a pris fin.",
"deleteProfileFailed": "Le profil n'a pas pu être supprimé.",
"removeGuardianFailed": "Cet accès n'a pas pu prendre fin."
},
"anamnesis": {
"relativeNote": "Un antécédent familial décrit les proches de cette personne. Ne l'ajoutez que si elle voudrait le voir consigné ici."
+ },
+ "handoverDecision": {
+ "title": "Qui garde l'accès à votre dossier",
+ "description": "Décidez ce que les personnes qui ont tenu votre dossier peuvent faire désormais.",
+ "proposed": "Proposé : {access}",
+ "settled": "Déjà modifié dans Accès partagé",
+ "empty": "Personne d'autre n'avait accès à votre dossier.",
+ "later": "Vous pouvez le modifier à tout moment dans Paramètres, Accès partagé.",
+ "save": "Confirmer",
+ "saving": "Enregistrement…",
+ "decideLater": "Plus tard",
+ "errorFailed": "Votre décision n'a pas pu être enregistrée. Réessayez.",
+ "loadError": "Les personnes qui tenaient votre dossier n'ont pas pu être chargées.",
+ "notificationsTitle": "Configurez vos propres rappels",
+ "notificationsBody": "Jusqu'ici, les rappels de ce dossier parvenaient aux personnes qui le tenaient. Désormais, ils vous parviennent dès que vous ajoutez un canal de notification.",
+ "notificationsAction": "Ouvrir les notifications"
}
},
"encounters": {
@@ -11434,7 +12006,7 @@
"both": "Des deux côtés",
"none": "Non précisé"
},
- "bodySiteWithSide": "{site} · {side}",
+ "bodySiteWithSide": "{site} ({side})",
"sectionTitle": "Rendez-vous",
"sectionDescription": "Consultations et rendez-vous pris, au même endroit.",
"add": "Rendez-vous",
@@ -11526,7 +12098,6 @@
"title": "Cabinets et médecins",
"description": "Votre carnet d'adresses : saisissez un cabinet une fois, choisissez-le partout.",
"linkLabel": "Carnet d'adresses",
- "add": "Entrée",
"searchPlaceholder": "Rechercher un nom, un cabinet ou une spécialité",
"loadError": "Impossible de charger le carnet d'adresses.",
"emptyTitle": "Aucune entrée",
@@ -11623,7 +12194,6 @@
"unreadableLabel": "Symptôme illisible",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Noter un symptôme",
"sheetDescription": "L'un de tes propres symptômes, son intensité et quand.",
"symptom": "Symptôme",
"symptomRequired": "Choisis un symptôme.",
@@ -11672,5 +12242,426 @@
"title": "Symptômes notés",
"filed": "noté avec cette maladie"
}
+ },
+ "day": {
+ "scoreOfMax": "sur {max}",
+ "cycleDay": "Jour {n} du cycle",
+ "cyclePhaseLine": {
+ "MENSTRUAL": "Jour {n} du cycle, phase menstruelle",
+ "FOLLICULAR": "Jour {n} du cycle, phase folliculaire",
+ "OVULATORY": "Jour {n} du cycle, phase ovulatoire",
+ "LUTEAL": "Jour {n} du cycle, phase lutéale"
+ },
+ "previousDay": "Jour précédent",
+ "nextDay": "Jour suivant",
+ "close": "Fermer le jour",
+ "hidePanel": "Masquer le jour",
+ "showPanel": "Afficher {date}",
+ "openDay": "Ouvrir le jour",
+ "viewWholeDay": "Voir toute la journée",
+ "captureForDay": "Saisir pour ce jour",
+ "empty": "Aucune entrée pour ce jour.",
+ "notShared": "Certaines rubriques ne sont pas partagées avec vous.",
+ "inTimeline": "Dans la chronologie",
+ "askCoach": "Interroger le Coach sur ce jour",
+ "loadFailed": "Impossible de charger ce jour.",
+ "dayOf": "Jour {n} sur {m}",
+ "dayN": "Jour {n}",
+ "usualRange": "Habituellement de {lo} à {hi}",
+ "groups": {
+ "scores": "Scores",
+ "running": "En cours ce jour-là",
+ "values": "Valeurs",
+ "happened": "Ce qui s’est passé"
+ },
+ "notable": {
+ "extremeHigh": "Valeur journalière la plus haute depuis {since}",
+ "extremeLow": "Valeur journalière la plus basse depuis {since}",
+ "firstValue": "Première mesure de ce type",
+ "gap": "{days} jours sans mesure"
+ },
+ "previousDayShortcut": "Jour précédent (Alt + flèche gauche)",
+ "nextDayShortcut": "Jour suivant (Alt + flèche droite)",
+ "captureShort": "Enregistrer",
+ "inTimelineShort": "Chronologie",
+ "askCoachPrompt": "Que s’est-il passé le {date} ?",
+ "coachChips": "Jours dans cette réponse",
+ "description": "Tout ce que vos entrées contiennent pour ce jour.",
+ "openDocument": "Ouvrir le document",
+ "previousReading": "Avant, {date}",
+ "sheetExpand": "Agrandir en pleine hauteur",
+ "sheetShrink": "Réduire",
+ "valuesLegend": "Point pour ce jour, barre pour les 30 jours précédents",
+ "valuesAllOne": "Toutes les valeurs",
+ "valuesAllFew": "Les {count} valeurs",
+ "valuesAllOther": "Les {count} valeurs",
+ "valuesFewer": "Moins de valeurs",
+ "countEntriesOne": "1 entrée",
+ "countEntriesFew": "{count} entrées",
+ "countEntriesOther": "{count} entrées",
+ "meanOfReadingsOne": "Moyenne de 1 mesure",
+ "meanOfReadingsFew": "Moyenne de {count} mesures",
+ "meanOfReadingsOther": "Moyenne de {count} mesures",
+ "sinceLastVisit": {
+ "title": "Depuis la dernière visite le {date}",
+ "countOne": "1 entrée",
+ "countFew": "{count} entrées",
+ "countOther": "{count} entrées",
+ "allOne": "Tout afficher",
+ "allFew": "Afficher les {count}",
+ "allOther": "Afficher les {count}",
+ "fewer": "Afficher moins",
+ "note": "Établi à partir de vos entrées, sans évaluation. Chaque date ouvre la journée entière.",
+ "report": "Rapport médical du {date} à aujourd’hui"
+ },
+ "travel": "En déplacement",
+ "event": {
+ "started": "Début : {label}",
+ "ended": "Fin : {label}",
+ "paused": "Pause : {label}",
+ "resumed": "Reprise : {label}",
+ "illnessBegan": "Début : {label}",
+ "illnessResolved": "Guérison : {label}"
+ },
+ "pickDate": "Choisir une date",
+ "pickerTitle": "Aller à un jour",
+ "today": "Aujourd’hui",
+ "hasEntries": "avec des entrées",
+ "dayNSince": "Jour {n}, depuis le {date}",
+ "sinceDate": "Depuis le {date}"
+ },
+ "timeline": {
+ "title": "Chronologie",
+ "subtitle": "Ce qui s’est passé et quand, à côté de vos valeurs.",
+ "standing": "Permanent",
+ "startUnknown": "début inconnu",
+ "zoom": {
+ "all": "Tout",
+ "year": "12 mois",
+ "quarter": "3 mois",
+ "range": "Période"
+ },
+ "lanes": {
+ "life": "Vie",
+ "illness": "Maladies",
+ "allergies": "Allergies",
+ "medications": "Médicaments",
+ "vaccinations": "Vaccinations",
+ "visits": "Médecin et interventions",
+ "labs": "Analyses",
+ "documents": "Documents",
+ "cycle": "Cycle"
+ },
+ "readiness": {
+ "title": "Ce que votre chronologie contient déjà",
+ "subtitle": "Un état des lieux honnête avant de l’ouvrir.",
+ "menuEntry": "Couverture des données",
+ "hint": "Les pistes vides restent masquées tant qu’elles ne contiennent rien. Les modules désactivés n’apparaissent jamais. Vous retrouverez cet aperçu plus tard sous Couverture des données, dans la chronologie.",
+ "later": "Plus tard",
+ "open": "Ouvrir la chronologie",
+ "verdict": {
+ "carries": "Elle est déjà bien remplie.",
+ "thin": "Encore maigre. Pour l’instant, la chronologie montre surtout vos valeurs."
+ },
+ "status": {
+ "carries": "remplie",
+ "thin": "maigre",
+ "empty": "vide"
+ },
+ "lanes": {
+ "values": "Valeurs",
+ "mood": "Humeur",
+ "environment": "Environnement",
+ "life": "Événements de vie"
+ },
+ "meter": "{carrying} pistes sur {total} sont remplies",
+ "carriesDetail": "{carrying} pistes sur {total} donnent une vue d’ensemble depuis {since}.",
+ "loadFailed": "L’inventaire n’a pas pu être chargé.",
+ "card": {
+ "title": "Ce que la chronologie contient déjà",
+ "summary": "Elle remonte à {since} ; {carrying} pistes sur {total} sont déjà remplies.",
+ "dismiss": "Masquer"
+ },
+ "detail": {
+ "valuesOne": "{count} mesure depuis {since}",
+ "valuesFew": "{count} mesures depuis {since}",
+ "valuesOther": "{count} mesures depuis {since}",
+ "illnessOne": "{count} épisode ; chroniques : {chronic}",
+ "illnessFew": "{count} épisodes ; chroniques : {chronic}",
+ "illnessOther": "{count} épisodes ; chroniques : {chronic}",
+ "allergiesOne": "{count} allergie",
+ "allergiesFew": "{count} allergies",
+ "allergiesOther": "{count} allergies",
+ "medicationsOne": "{count} médicament ; sans date de début : {missingStart}",
+ "medicationsFew": "{count} médicaments ; sans date de début : {missingStart}",
+ "medicationsOther": "{count} médicaments ; sans date de début : {missingStart}",
+ "medicationsDatedOne": "{count} médicament avec date de début",
+ "medicationsDatedFew": "{count} médicaments avec date de début",
+ "medicationsDatedOther": "{count} médicaments avec date de début",
+ "vaccinationsOne": "{count} vaccin",
+ "vaccinationsFew": "{count} vaccins",
+ "vaccinationsOther": "{count} vaccins",
+ "visitsOne": "{count} consultation, dont {procedures} avec intervention",
+ "visitsFew": "{count} consultations, dont {procedures} avec intervention",
+ "visitsOther": "{count} consultations, dont {procedures} avec intervention",
+ "visitsPlannedOne": "{count} consultation, seulement prévue",
+ "visitsPlannedFew": "{count} consultations, seulement prévues",
+ "visitsPlannedOther": "{count} consultations, seulement prévues",
+ "labsOne": "{count} jour avec résultats",
+ "labsFew": "{count} jours avec résultats",
+ "labsOther": "{count} jours avec résultats",
+ "documentsOne": "{count} document daté",
+ "documentsFew": "{count} documents datés",
+ "documentsOther": "{count} documents datés",
+ "lifeOne": "{count} événement de vie",
+ "lifeFew": "{count} événements de vie",
+ "lifeOther": "{count} événements de vie",
+ "cycleOne": "{count} cycle",
+ "cycleFew": "{count} cycles",
+ "cycleOther": "{count} cycles",
+ "moodOne": "{count} entrée depuis {since}",
+ "moodFew": "{count} entrées depuis {since}",
+ "moodOther": "{count} entrées depuis {since}",
+ "environmentOne": "{count} jour avec données d’environnement",
+ "environmentFew": "{count} jours avec données d’environnement",
+ "environmentOther": "{count} jours avec données d’environnement",
+ "empty": "Rien pour l’instant"
+ },
+ "gap": {
+ "valuesEmpty": "Connecter des sources",
+ "illnessEmpty": "Ajouter un épisode",
+ "medicationsWithoutStart": "Compléter",
+ "visitsEmpty": "Ajouter une visite",
+ "documentsUndated": "Vérifier",
+ "labsEmpty": "Ajouter un résultat",
+ "vaccinationsEmpty": "Ajouter des vaccins",
+ "lifeEventsEmpty": "Ajouter"
+ },
+ "gapLine": {
+ "medicationsWithoutStartOne": "{count} médicament sans date de début",
+ "medicationsWithoutStartFew": "{count} médicaments sans date de début",
+ "medicationsWithoutStartOther": "{count} médicaments sans date de début",
+ "documentsUndatedOne": "{count} document sans date",
+ "documentsUndatedFew": "{count} documents sans date",
+ "documentsUndatedOther": "{count} documents sans date",
+ "valuesEmpty": "Pas encore de valeurs sur une longue période",
+ "illnessEmpty": "Pas encore de maladies avec une date de début",
+ "visitsEmpty": "Pas encore de consultations effectuées",
+ "labsEmpty": "Pas encore de résultats d’analyses",
+ "vaccinationsEmpty": "Pas encore de vaccins",
+ "lifeEventsEmpty": "Aucun événement de vie pour l’instant"
+ }
+ },
+ "legendSpan": "Période",
+ "legendEvent": "Événement",
+ "legendMeanQuarter": "Moyenne trimestrielle",
+ "legendMeanMonth": "Moyenne mensuelle",
+ "legendMeanWeek": "Moyenne hebdomadaire",
+ "legendThin": "Moins de 3 mesures",
+ "legendGap": "Courte période sans mesures",
+ "today": "Aujourd’hui",
+ "noDataYet": "Pas encore de données",
+ "layers": "Calques",
+ "layersLanes": "Pistes",
+ "zoomLabel": "Période affichée",
+ "chartLabel": "Chronologie de {from} à {to}",
+ "keyboardHint": "Les flèches déplacent la sélection, Début et Fin vont aux extrémités, Entrée ouvre le jour.",
+ "loadFailed": "La chronologie n’a pas pu être chargée.",
+ "empty": {
+ "title": "Rien dans la chronologie pour l’instant",
+ "description": "Les entrées datées apparaîtront ici dès qu’il y en aura."
+ },
+ "values": {
+ "menuTitle": "Valeurs sous les lignes",
+ "trigger": "Valeurs ({count})",
+ "noneInWindow": "Aucune valeur sur cette période",
+ "series": {
+ "BLOOD_PRESSURE_SYS": "Tension systolique",
+ "BLOOD_PRESSURE_DIA": "Tension diastolique"
+ },
+ "noValue": "aucune valeur",
+ "bloodPressure": "Tension artérielle",
+ "readingsOne": "1 mesure",
+ "readingsFew": "moyenne de {count} mesures",
+ "readingsOther": "moyenne de {count} mesures"
+ },
+ "table": {
+ "caption": "Entrées de la chronologie",
+ "lane": "Piste",
+ "entry": "Entrée",
+ "start": "Début",
+ "end": "Fin",
+ "seriesCaption": "Moyennes par période",
+ "period": "Période"
+ },
+ "selection": {
+ "empty": "Aucune entrée sur cette période",
+ "since": "depuis le {date}",
+ "range": "{from} au {to}",
+ "mean": "Ø {values}",
+ "week": "Semaine du {date}",
+ "hintClick": "Cliquez sur un moment pour le voir ici",
+ "hintTap": "Touchez un moment pour le voir ici"
+ },
+ "chronicle": {
+ "months": "Mois",
+ "years": "Années",
+ "groupingLabel": "Regroupement",
+ "empty": "Aucune entrée de {from} à {to}",
+ "emptySingle": "Aucune entrée en {period}",
+ "ended": "Fin : {label}",
+ "afterDaysOne": "après 1 jour",
+ "afterDaysFew": "après {count} jours",
+ "afterDaysOther": "après {count} jours",
+ "notable": {
+ "extremeHigh": "Valeur la plus haute depuis longtemps",
+ "extremeLow": "Valeur la plus basse depuis longtemps",
+ "firstValue": "Première valeur d’une mesure",
+ "gap": "Une longue interruption des valeurs"
+ },
+ "notableMeta": "Ouvrez le jour pour les détails"
+ },
+ "standingAllergiesOne": "{count} allergie",
+ "standingAllergiesFew": "{count} allergies",
+ "standingAllergiesOther": "{count} allergies",
+ "legendMeanDay": "Moyenne journalière",
+ "range": {
+ "from": "Du",
+ "to": "Au",
+ "label": "Période choisie",
+ "open": "Choisir une période",
+ "clear": "Tout afficher",
+ "apply": "Appliquer"
+ },
+ "item": {
+ "labValuesOne": "1 valeur de laboratoire",
+ "labValuesFew": "{count} valeurs de laboratoire",
+ "labValuesOther": "{count} valeurs de laboratoire"
+ }
+ },
+ "lifeEvents": {
+ "title": "Événement de vie",
+ "add": "Événement de vie",
+ "privacy": "Le titre et la note sont chiffrés. Ils ne sont envoyés à aucune IA.",
+ "emptyHint": "Un déménagement, une naissance ou un nouvel emploi donnent un repère aux années.",
+ "save": "Enregistrer",
+ "cancel": "Annuler",
+ "delete": "Supprimer l’événement de vie",
+ "saved": "Événement de vie enregistré",
+ "deleted": "Événement de vie supprimé",
+ "fields": {
+ "title": "Que s’est-il passé ?",
+ "category": "Type",
+ "when": "Quand",
+ "precision": "Précision",
+ "addEnd": "Date de fin, s’il s’agissait d’une période",
+ "endDate": "Jusqu’au",
+ "note": "Note",
+ "optionalTag": "facultatif",
+ "notePlaceholder": "Quelque chose que vous voudrez savoir plus tard",
+ "removeEnd": "Retirer la date de fin"
+ },
+ "category": {
+ "FAMILY": "Famille",
+ "HOME": "Logement",
+ "WORK": "Travail",
+ "LOSS": "Deuil",
+ "OTHER": "Autre"
+ },
+ "precision": {
+ "DAY": "Jour",
+ "MONTH": "Mois",
+ "YEAR": "Année"
+ },
+ "edit": "Modifier l’événement de vie",
+ "errors": {
+ "titleRequired": "Donnez un nom à l’événement.",
+ "titleTooLong": "120 caractères au maximum.",
+ "categoryRequired": "Choisissez un type.",
+ "dateRequired": "Indiquez une date valide.",
+ "endBeforeStart": "La fin précède le début.",
+ "noteTooLong": "2000 caractères au maximum."
+ },
+ "saveFailed": "L’enregistrement a échoué.",
+ "deleteFailed": "La suppression a échoué.",
+ "deleteConfirm": {
+ "title": "Supprimer cet événement de vie ?",
+ "body": "Son titre et sa note disparaîtront."
+ }
+ },
+ "palette": {
+ "title": "Recherche",
+ "description": "Trouver une page, un réglage, un module, une action ou une entrée de votre dossier.",
+ "placeholder": "Rechercher des pages, réglages, votre dossier…",
+ "results": "Résultats",
+ "noResults": "Aucun résultat pour « {query} ».",
+ "moduleOn": "Activé",
+ "moduleOff": "Désactivé",
+ "hintMove": "Naviguer",
+ "hintOpen": "Ouvrir",
+ "hintClose": "Fermer",
+ "groups": {
+ "recent": "Récents",
+ "pages": "Pages",
+ "insights": "Insights",
+ "settings": "Paramètres",
+ "modules": "Modules",
+ "actions": "Actions",
+ "records": "Dans votre dossier"
+ },
+ "actions": {
+ "capture": "Nouvelle saisie",
+ "coach": "Demander au Coach",
+ "backup": "Créer une sauvegarde",
+ "today": "Ouvrir aujourd’hui",
+ "shortcuts": "Afficher les raccourcis clavier"
+ },
+ "records": {
+ "loading": "Recherche dans votre dossier…",
+ "empty": "Rien dans votre dossier pour « {query} ».",
+ "error": "Votre dossier n’a pas pu être parcouru. Réessayez dans un instant.",
+ "truncated": "Seules les entrées les plus récentes ont été parcourues.",
+ "kind": {
+ "visit": "Consultation",
+ "condition": "Maladie",
+ "document": "Document",
+ "vaccination": "Vaccination",
+ "lab": "Résultat d’analyse",
+ "medication": "Médicament",
+ "symptom": "Symptôme",
+ "workout": "Entraînement",
+ "note": "Note"
+ },
+ "source": {
+ "mood": "Note d’humeur",
+ "measurement": "Note sur une mesure",
+ "illness": "Journal de maladie"
+ }
+ }
+ },
+ "shortcuts": {
+ "title": "Raccourcis clavier",
+ "description": "Des touches qui fonctionnent partout dans l’application, sauf pendant la saisie dans un champ.",
+ "menuEntry": "Raccourcis clavier",
+ "then": "puis",
+ "search": "Rechercher",
+ "help": "Afficher cette liste",
+ "close": "Fermer une boîte de dialogue ou un panneau",
+ "previousDay": "Jour précédent",
+ "nextDay": "Jour suivant",
+ "toggleDay": "Ouvrir ou fermer le jour",
+ "dayHint": "Jour précédent et suivant fonctionnent quand un jour est ouvert.",
+ "groups": {
+ "navigation": "Navigation",
+ "actions": "Actions",
+ "day": "Jour"
+ }
+ },
+ "aboutPage": {
+ "eyebrow": "À propos",
+ "title": "À propos de HealthLog",
+ "lastUpdated": "Dernière mise à jour : {date}",
+ "project": "Projet",
+ "credits": "Crédits"
}
}
diff --git a/messages/it.json b/messages/it.json
index e0e489863..102b09984 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -49,7 +49,9 @@
"dismiss": "Ignora",
"moreInfo": "Maggiori informazioni",
"durationHoursMinutes": "{hours} h {minutes} min",
- "durationMinutes": "{minutes} min"
+ "durationMinutes": "{minutes} min",
+ "durationClockMinutes": "{time} min",
+ "durationClockHours": "{time} h"
},
"apiErrors": {
"measurement": {
@@ -165,7 +167,7 @@
"anamnesisShiftSchedule": "Orario di lavoro",
"anamnesisColFact": "Voce del profilo",
"anamnesisColValue": "Valore registrato",
- "subtitle": "HealthLog — Rapporto personale di salute",
+ "subtitle": "HealthLog: rapporto personale di salute",
"patient": "Paziente",
"dateOfBirth": "Data di nascita",
"gender": "Sesso",
@@ -175,18 +177,18 @@
"height": "Altezza",
"period": "Periodo del rapporto",
"createdOn": "Creato il",
- "vitalsTitle": "Parametri vitali — Riepilogo",
+ "vitalsTitle": "Parametri vitali: riepilogo",
"colParameter": "Parametro",
"colCurrent": "Attuale",
"colAvgPeriod": "Media periodo",
"colMin": "Min",
"colMax": "Max",
"colN": "n",
- "bpClassificationTitle": "Pressione arteriosa — Classificazione ESH (2023)",
+ "bpClassificationTitle": "Pressione arteriosa: classificazione ESH (2023)",
"avgBp": "Pressione arteriosa media",
"classification": "Classificazione",
"bmiTitle": "Indice di massa corporea (BMI)",
- "bmiRow": "IMC:{bmi} kg/m² — Classification: {class} (WHO)",
+ "bmiRow": "IMC:{bmi} kg/m², Classification: {class} (WHO)",
"complianceTitle": "Aderenza ai farmaci",
"glp1Title": "Terapia GLP-1",
"glp1WeightSummary": "Peso nel periodo del referto: {start} kg → {end} kg ({delta} kg)",
@@ -204,7 +206,7 @@
"colExpected": "Attese",
"colComplianceRate": "Tasso di aderenza",
"moodTitle": "Andamento dell’umore",
- "moodSummary": "Media:{avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+ "moodSummary": "Media {avg} / 5.0, {count} voci, intervallo {min}–{max}",
"colMood": "Umore",
"colCount": "Quantità",
"colShare": "Quota",
@@ -234,8 +236,8 @@
"typeGlucoseRandom": "Glicemia (qualsiasi)",
"typeGlucoseBedtime": "Glicemia (prima di coricarsi)",
"typeGlucoseUnspecified": "Glicemia (non indicata)",
- "glucoseClassificationTitle": "Glicemia — Intervalli di riferimento (ADA 2024 / DDG)",
- "glucoseRow": "{label}: media {avg} {unit} (obiettivo {rangeMin}–{rangeMax}) — {class}",
+ "glucoseClassificationTitle": "Glicemia: intervalli di riferimento (ADA 2024 / DDG)",
+ "glucoseRow": "{label}: media {avg} {unit} (obiettivo {rangeMin}–{rangeMax}), {class}",
"glucoseInTarget": "in obiettivo",
"glucoseBelowTarget": "sotto obiettivo",
"glucoseAboveTarget": "sopra obiettivo",
@@ -287,7 +289,7 @@
"gmi": "Indicatore di gestione del glucosio (GMI)",
"eA1c": "A1C stimata",
"cv": "Variabilità (CV%)",
- "spotCaveat": "Sono stime da misurazioni occasionali, non un profilo da monitor continuo — vanno lette come una tendenza, non come un verdetto clinico."
+ "spotCaveat": "Sono stime da misurazioni occasionali, non un profilo da monitor continuo: vanno lette come una tendenza, non come un verdetto clinico."
},
"illnessTitle": "Condizioni e malattie",
"illnessOngoing": "In corso",
@@ -376,12 +378,12 @@
"backToDashboard": "Torna alla dashboard"
},
"offlineBanner": {
- "message": "Nessuna connessione — le modifiche non possono essere salvate finché non torni online.",
- "saveFailed": "Impossibile salvare — sei offline. Riprova quando torni online.",
+ "message": "Nessuna connessione: le modifiche non possono essere salvate finché non torni online.",
+ "saveFailed": "Impossibile salvare: sei offline. Riprova quando torni online.",
"showingCached": "Vengono mostrati i tuoi ultimi dati sincronizzati."
},
"demoBanner": {
- "message": "Modalità demo — le modifiche non vengono salvate."
+ "message": "Modalità demo: le modifiche non vengono salvate."
},
"nav": {
"dashboard": "Dashboard",
@@ -407,6 +409,7 @@
"mood": "Umore",
"coach": "Coach",
"skipToContent": "Vai al contenuto",
+ "breadcrumb": "Sei qui",
"sidebar": "Barra laterale",
"mainNavigation": "Navigazione principale",
"mobileNavigation": "Navigazione mobile",
@@ -426,7 +429,9 @@
"workout": "Allenamento",
"workoutDescription": "Una sessione che i tuoi dispositivi non hanno registrato",
"symptom": "Sintomo",
- "symptomDescription": "Uno dei tuoi sintomi, con la sua intensità"
+ "symptomDescription": "Uno dei tuoi sintomi, con la sua intensità",
+ "lifeEventDescription": "Un trasloco, una nascita, un nuovo lavoro",
+ "lifeEvent": "Evento di vita"
},
"cycle": "Ciclo",
"labs": "Esami",
@@ -435,7 +440,8 @@
"illness": "Malattia",
"documents": "Documenti",
"vaccinations": "Vaccinazioni",
- "mentalWellbeing": "Benessere mentale"
+ "mentalWellbeing": "Benessere mentale",
+ "timeline": "Cronologia"
},
"auth": {
"login": "Accedi",
@@ -452,14 +458,13 @@
"hasAccount": "Hai già un account?",
"loginFailed": "Accesso non riuscito",
"registerFailed": "Registrazione non riuscita",
- "invitedBanner": "Sei stato invitato — crea il tuo account anche se la registrazione è chiusa.",
+ "invitedBanner": "Sei stato invitato: crea il tuo account anche se la registrazione è chiusa.",
"alreadySignedIn": {
"title": "Hai già effettuato l'accesso",
"body": "Hai effettuato l'accesso come {username}. Un invito crea un nuovo account. Esci per accettarlo.",
"signOut": "Esci e accetta l'invito"
},
"createAccount": "Crea account",
- "newAccount": "Crea un nuovo account",
"backToPasskey": "Torna a passkey",
"passkeySetupLater": "Puoi configurare le passkey più tardi dalle impostazioni.",
"passwordMinLength": "Min. 12 caratteri",
@@ -512,6 +517,37 @@
"fixLan": "Server in una rete domestica senza TLS: imposta SESSION_COOKIE_SECURE=false nelle sue impostazioni e riavvialo.",
"fixHttps": "Server dietro un proxy HTTPS: apri invece l'indirizzo https:// e assicurati che il proxy inoltri X-Forwarded-Proto: https.",
"localhostNote": "Questa pagina è su http://localhost mentre il server imposta il cookie di accesso solo per https://. La maggior parte dei browser lo accetta su localhost; se dopo l'accesso torni qui, imposta SESSION_COOKIE_SECURE=false."
+ },
+ "claim": {
+ "title": "Prendi in mano la tua cartella sanitaria",
+ "titleNamed": "Ti diamo il benvenuto, {name}",
+ "description": "Finora la tua cartella sanitaria è stata tenuta per te. Scegli come accedere e diventerà il tuo account. Nulla viene copiato o spostato.",
+ "loading": "Controllo del link…",
+ "invalid": "Questo link non funziona più",
+ "invalidHint": "È sconosciuto, scaduto, ritirato o già usato. Chiedine uno nuovo a chi te l'ha inviato.",
+ "oidcOnly": "Questo server consente solo il single sign-on, quindi qui non è ancora possibile prendere in carico un profilo.",
+ "signedInTitle": "Prima esci",
+ "signedInBody": "Hai effettuato l'accesso come {username}. Questo link crea un accesso per un'altra persona, quindi esci prima di usarlo.",
+ "signOut": "Esci e continua",
+ "guardiansTitle": "Chi mantiene l'accesso per ora",
+ "guardiansHint": "Proposto dalle persone che hanno tenuto la tua cartella. Decidi subito dopo l'accesso.",
+ "guardianUnnamed": "Un tutore",
+ "usernameHint": "Da 3 a 30 lettere, numeri, _ o -.",
+ "usernameReserved": "I nomi utente che iniziano con managed- sono riservati.",
+ "emailHint": "Obbligatorio, così puoi rientrare se dimentichi la password.",
+ "consentNote": "L'avviso e qualsiasi uso dell'IA li confermi tu. Fino ad allora nulla della tua cartella viene inviato a un fornitore di IA.",
+ "expires": "Questo link è valido fino al {date}.",
+ "submit": "Prendi in carico",
+ "submitting": "Presa in carico…",
+ "errorTaken": "Questo nome utente o indirizzo email è già in uso.",
+ "errorRateLimit": "Troppi tentativi. Attendi qualche minuto e riprova.",
+ "errorFailed": "Non ha funzionato. Riprova tra un momento.",
+ "errorOffline": "Il server non è stato raggiunto. Controlla la connessione e riprova."
+ },
+ "inviteInvalid": {
+ "title": "Invito non valido",
+ "description": "Questo link di invito è incompleto o è stato copiato male. Chiedi un nuovo link alla persona che ti ha invitato.",
+ "toLogin": "Vai all’accesso"
}
},
"dashboard": {
@@ -543,17 +579,17 @@
"sleepShort": "Sonno",
"stepsShort": "Passi",
"vo2MaxShort": "VO₂ max",
- "bpInTargetWindow90": "PA-target · 90 gg",
- "bpInTargetWindowDynamic": "PA-target · {days} gg",
+ "bpInTargetWindow90": "PA-target (90 gg)",
+ "bpInTargetWindowDynamic": "PA-target ({days} gg)",
"bpInTargetCollecting": "raccolta dati in corso",
"medications": "Farmaci",
"quickAddMeasurement": "Registra misurazione",
"quickAddMood": "Registra umore",
"quickAddMedicationIntake": "Registra assunzione",
"quickAddWorkout": "Registra allenamento",
+ "quickAddLifeEvent": "Registra evento di vita",
"customizeDashboard": "Personalizza dashboard",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Registra assunzione del farmaco",
"sheetDescription": "Registra dose, orario e farmaco con pochi tap.",
"medicationLabel": "Farmaco",
"medicationPlaceholder": "Seleziona un farmaco",
@@ -666,7 +702,8 @@
"body": "Senza una copia della chiave, i tuoi dati non si possono recuperare dopo una reinstallazione. Ci vuole un minuto.",
"action": "Apri il passaggio"
},
- "quickAddSymptom": "Registra sintomo"
+ "quickAddSymptom": "Registra sintomo",
+ "todayCardVisible": "Mostra la scheda in alto"
},
"measurements": {
"title": "Misurazioni",
@@ -738,7 +775,7 @@
"formReset": "Reimposta modulo",
"saveError": "Errore durante il salvataggio",
"duplicateTimestamp": "Esiste già una misurazione di questo tipo a questa marca temporale.",
- "serverOwnedSource": "I valori provenienti da una fonte collegata non possono essere modificati — data e nota restano modificabili.",
+ "serverOwnedSource": "I valori provenienti da una fonte collegata non possono essere modificati, ma data e nota restano modificabili.",
"deleteError": "Errore durante l’eliminazione",
"deletedToast": "Misurazione eliminata",
"restoredToast": "Misurazione ripristinata",
@@ -833,11 +870,12 @@
"durationHoursOnly": "{hours} h",
"durationMinutesOnly": "{minutes} min",
"durationInvalid": "Inserisci una durata come 7,5, 7:30 o 7h 30m.",
- "durationTooLong": "Una voce non può superare le 24 ore."
+ "durationTooLong": "Una voce non può superare le 24 ore.",
+ "sourceHealthConnect": "Health Connect"
},
"metricCatalog": {
"title": "Cosa può monitorare HealthLog",
- "description": "Tutti gli ambiti di metriche supportati da HealthLog, che tu abbia già dei dati oppure no — e cosa li fornirebbe.",
+ "description": "Tutti gli ambiti di metriche supportati da HealthLog, che tu abbia già dei dati oppure no, e cosa li fornirebbe.",
"statusPresent": "Monitorato",
"statusAbsent": "Non ancora monitorato",
"statusModuleOff": "Disattivato",
@@ -845,11 +883,11 @@
"connectCta": "Collega una fonte",
"viewCta": "Visualizza",
"ecg": {
- "source": "Tracciati ECG a una derivazione da un orologio compatibile — oggi Withings ScanWatch — sincronizzati automaticamente."
+ "source": "Tracciati ECG a una derivazione da un orologio compatibile (oggi Withings ScanWatch), sincronizzati automaticamente."
},
"watchdog": {
"title": "Monitoraggio automatico",
- "description": "HealthLog osserva anche silenziosamente in background — notifiche di ritmo irregolare e disturbi respiratori da un orologio connesso, avvisi di scostamento dalla tua base di riferimento, e variazioni degne di nota tra i tuoi due ultimi pannelli di laboratorio — mostrando una scheda solo quando qualcosa emerge. Se almeno una di queste fonti è collegata, nessuna scheda visibile è un buon segno, non una funzione mancante; se non ne è ancora collegata nessuna, semplicemente non c'è ancora nulla sotto controllo."
+ "description": "HealthLog osserva anche silenziosamente in background (notifiche di ritmo irregolare e disturbi respiratori da un orologio connesso, avvisi di scostamento dalla tua base di riferimento, e variazioni degne di nota tra i tuoi due ultimi pannelli di laboratorio), mostrando una scheda solo quando qualcosa emerge. Se almeno una di queste fonti è collegata, nessuna scheda visibile è un buon segno, non una funzione mancante; se non ne è ancora collegata nessuna, semplicemente non c'è ancora nulla sotto controllo."
}
},
"mood": {
@@ -1006,7 +1044,7 @@
"archive": "Archivia",
"restore": "Ripristina",
"archivedTitle": "Tag archiviati",
- "archivedDescription": "I tag archiviati restano nelle tue voci passate — scompaiono solo dal selettore.",
+ "archivedDescription": "I tag archiviati restano nelle tue voci passate: scompaiono solo dal selettore.",
"archivedEmpty": "Niente in archivio.",
"purgeTitle": "Eliminare il tag definitivamente?",
"purgeBody": "Il tag verrà rimosso da {count} voci passate. L’operazione non può essere annullata.",
@@ -1191,7 +1229,8 @@
"bodyLogged": "Registrato nel diario di malattia: {count} sintomi",
"bodyNotLogged": "Per questa giornata non c'è nulla nel diario di malattia",
"openIllness": "Apri"
- }
+ },
+ "sourceNotification": "Notifica"
},
"medications": {
"title": "Farmaci",
@@ -1209,8 +1248,8 @@
"dialogDescription": "Questi farmaci sono da assumere adesso. Conferma per registrarli tutti come assunti.",
"confirm": "Assumi tutti ({count})",
"successToast": "{count} assunzioni registrate",
- "partialToast": "{taken} registrate, {failed} non riuscite – riprova",
- "failedToast": "Nessuna assunzione registrata – riprova"
+ "partialToast": "{taken} registrate, {failed} non riuscite. Riprova",
+ "failedToast": "Nessuna assunzione registrata. Riprova"
},
"logIntake": {
"dialogTitle": "Registra un'assunzione",
@@ -1284,6 +1323,14 @@
"disabledToast": "Registrazione assunzioni disattivata",
"toggleFailed": "Impossibile modificare la registrazione delle assunzioni."
},
+ "autoMarkTaken": {
+ "label": "Registra le assunzioni automaticamente",
+ "helper": "HealthLog registra ogni dose programmata come assunta al suo orario. Puoi comunque modificare o saltare una dose a mano. Non vengono inviati promemoria.",
+ "cardHint": "Registrato automaticamente",
+ "enabledToast": "Le assunzioni ora vengono registrate automaticamente",
+ "disabledToast": "Le assunzioni non vengono più registrate automaticamente",
+ "toggleFailed": "Impossibile modificare la registrazione automatica."
+ },
"withoutNotification": "Senza notifica",
"pausedSince": "In pausa dal",
"takeNow": "Assumi ora",
@@ -1299,7 +1346,7 @@
"complianceInfoLabel": "Cosa significa aderenza?",
"lastIntake": "Ultima assunzione:",
"nextIntake": "Prossima assunzione:",
- "nextIntakeOverdue": "In ritardo · {time} — ancora recuperabile",
+ "nextIntakeOverdue": "In ritardo da {time}, ancora recuperabile",
"dayStreak": "giorni di serie",
"dayStreakOne": "giorno di serie",
"cycleDueToday": "In scadenza oggi",
@@ -1393,10 +1440,10 @@
"error": {
"empty": "Scrivi prima una descrizione.",
"tooLong": "Mantieni la descrizione entro 2000 caratteri.",
- "rateLimit": "Troppe richieste — attendi un momento e riprova.",
- "noProvider": "Nessun provider IA configurato — configurane uno in Impostazioni.",
- "budget": "Budget IA giornaliero raggiunto — riprova domani.",
- "network": "Impossibile contattare il provider IA — riprova.",
+ "rateLimit": "Troppe richieste. Attendi un momento e riprova.",
+ "noProvider": "Nessun provider IA configurato: configurane uno in Impostazioni.",
+ "budget": "Budget IA giornaliero raggiunto. Riprova domani.",
+ "network": "Impossibile contattare il provider IA. Riprova.",
"consent": "La lettura della tua descrizione con IA richiede prima il tuo consenso.",
"unavailable": "La lettura delle descrizioni con IA non è disponibile qui al momento."
}
@@ -1412,7 +1459,7 @@
"everyNMonths": "Ogni N mesi in un giorno specifico",
"yearly": "Una volta all’anno",
"rolling": "Ogni N giorni dall’ultima assunzione (flessibile)",
- "rollingExplainer": "Conta dall’ultima assunzione registrata — si mette in pausa se salti una dose.",
+ "rollingExplainer": "Conta dall’ultima assunzione registrata e si mette in pausa se salti una dose.",
"oneShot": "Dose singola"
},
"weekdays": {
@@ -1470,7 +1517,7 @@
"night": "Notte"
},
"max": {
- "reached": "Massimo raggiunto — rimuovi un orario prima di aggiungerne un altro."
+ "reached": "Massimo raggiunto: rimuovi un orario prima di aggiungerne un altro."
}
},
"courseWindow": {
@@ -1520,7 +1567,7 @@
},
"step3": {
"title": "Qual è la dose?",
- "subline": "Una compressa, uno spruzzo, una goccia — a seconda della forma.",
+ "subline": "Una compressa, uno spruzzo, una goccia, a seconda della forma.",
"amountLabel": "Quantità",
"amountPlaceholder": "ad es. 5",
"unitLabel": "Unità",
@@ -1546,7 +1593,7 @@
},
"dosesPerUnitLabel": "Dosi per unità",
"dosesPerUnitPlaceholder": "es. 4",
- "dosesPerUnitHint": "Dosi per penna / fiala — lascia vuoto per non tracciare le scorte.",
+ "dosesPerUnitHint": "Dosi per penna / fiala. Lascia vuoto per non tracciare le scorte.",
"short": "Dose",
"unitsPerDoseLabel": "Unità per dose",
"unitsPerDoseHint": "ad es. 2 compresse da 2 mg per una dose da 4 mg, oppure ½ compressa per una dose divisa"
@@ -1558,7 +1605,7 @@
},
"step5": {
"title": "Con quale frequenza lo assumi?",
- "subline": "Scegli la cadenza — i dettagli arrivano al passo successivo.",
+ "subline": "Scegli la cadenza: i dettagli arrivano al passo successivo.",
"short": "Frequenza"
},
"step6": {
@@ -1580,7 +1627,7 @@
},
"step7": {
"title": "Quando lo assumi?",
- "subline": "Uno o più orari del giorno — i promemoria seguono questo elenco.",
+ "subline": "Uno o più orari del giorno: i promemoria seguono questo elenco.",
"presetsLabel": "Preimpostazioni per orario del giorno",
"presets": {
"morning": "Mattina",
@@ -1596,11 +1643,11 @@
"short": "Orari"
},
"step8": {
- "title": "Fatto — vuoi che HealthLog te lo ricordi?",
+ "title": "Fatto. Vuoi che HealthLog te lo ricordi?",
"subline": "Controlla tutto qui sotto prima di salvare.",
"remindersLabel": "Promemoria attivi",
- "remindersDescription": "Push, Telegram o ntfy — in base alle tue impostazioni.",
- "noChannelHint": "I promemoria richiedono un canale di notifica — al momento nessuno è attivo.",
+ "remindersDescription": "Push, Telegram o ntfy, in base alle tue impostazioni.",
+ "noChannelHint": "I promemoria richiedono un canale di notifica, ma al momento nessuno è attivo.",
"noChannelHintLink": "Configura le notifiche",
"short": "Fatto"
}
@@ -1684,7 +1731,7 @@
}
},
"errors": {
- "submitFailed": "Impossibile salvare il farmaco — riprova."
+ "submitFailed": "Impossibile salvare il farmaco. Riprova."
}
},
"doseStrength": {
@@ -1700,11 +1747,11 @@
"empty": "Nessuna fase di aumento della dose registrata. Aggiungi variazioni di dose con data per tracciare il piano.",
"youAreHere": "Sei qui",
"doseStep": "{dose} {unit}",
- "plannedFor": "Previsto · {date}"
+ "plannedFor": "Previsto per {date}"
},
"injectionSiteBodyOutlineAriaLabel": "Sagoma del corpo",
"injectionSitePickerAriaLabel": "Selettore del sito di iniezione",
- "injectionSiteRecommendedAriaLabel": "{site} — prossimo sito consigliato",
+ "injectionSiteRecommendedAriaLabel": "{site}, prossimo sito consigliato",
"injectionSiteLegendAriaLabel": "Legenda dei segnaposto",
"injectionSiteLegendRecommended": "Prossimo sito consigliato",
"injectionSiteLegendLastUsed": "Ultimo sito utilizzato",
@@ -1740,6 +1787,7 @@
"sourceImport": "Importazione",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "tramite {origin}",
+ "intakeRecordedAutomatically": "Registrata automaticamente",
"intakeHistoryEmptyTitle": "Ancora nessuna assunzione",
"intakeHistoryEmptyDescription": "Appena registrerai un’assunzione, comparirà qui con orario e sorgente.",
"intakeHistoryEmptyAction": "Registra un’assunzione",
@@ -1751,7 +1799,7 @@
"intakeHistoryStatusSkipped": "Saltata",
"intakeHistorySortByTaken": "Ordina per ora di assunzione",
"intakeHistorySortByScheduled": "Ordina per orario programmato",
- "intakeHistoryPageInfo": "Pagina {page} di {total} · {count} voci",
+ "intakeHistoryPageInfo": "Pagina {page} di {total}, {count} voci",
"intakeHistoryPrev": "Precedente",
"intakeHistoryNext": "Successiva",
"glp1": {
@@ -1792,7 +1840,7 @@
"cadence": {
"oneShotOn": "Dose singola il {date}.",
"oneShotPending": "Dose singola in sospeso.",
- "asNeeded": "Al bisogno — nessun piano fisso."
+ "asNeeded": "Al bisogno, nessun piano fisso."
},
"intake": {
"title": "Storico delle assunzioni",
@@ -1819,7 +1867,7 @@
"dialogTitle": "Modifica assunzione",
"takenAtLabel": "Assunta alle",
"scheduledForHint": "Programmata: {dateTime}",
- "farFromScheduledWarning": "Molto distante dall’orario programmato — controlla la data.",
+ "farFromScheduledWarning": "Molto distante dall’orario programmato: controlla la data.",
"skippedLabel": "Saltata",
"save": "Salva",
"cancel": "Annulla",
@@ -1851,7 +1899,7 @@
"markAsTaken": "Segna come assunta",
"markAsSkipped": "Segna come saltata",
"adHocTag": "fuori programma",
- "takenDetail": "prevista {planned} · assunta {taken}",
+ "takenDetail": "prevista {planned}, assunta {taken}",
"doseTaken": "Dose: {dose}",
"status": {
"taken_on_time": "puntuale",
@@ -1915,7 +1963,7 @@
},
"codes": {
"label": "Codici clinici (ATC / RxNorm)",
- "note": "Facoltativo. Usati nell'esportazione FHIR della cartella sanitaria. I codici vengono presi esattamente come inseriti — mai indovinati.",
+ "note": "Facoltativo. Usati nell'esportazione FHIR della cartella sanitaria. I codici vengono presi esattamente come inseriti, mai indovinati.",
"atcLabel": "ATC",
"rxNormLabel": "RxNorm",
"saved": "Codici clinici salvati",
@@ -1935,7 +1983,7 @@
"title": "Termina il farmaco",
"helper": "Interrompe i promemoria e segna il farmaco come terminato. Lo storico resta visibile.",
"button": "Termina",
- "dialogTitle": "Termina {name} — interrompe i promemoria, lo storico resta visibile",
+ "dialogTitle": "Termina {name}: interrompe i promemoria, lo storico resta visibile",
"dialogBody": "Nessun altro promemoria. Le voci precedenti restano nella cronologia.",
"toast": "Farmaco terminato",
"failed": "Impossibile terminare il farmaco."
@@ -1973,7 +2021,7 @@
"copyFailed": "Impossibile copiare.",
"tokenCopied": "Token copiato",
"copyToken": "Copia token",
- "mintedHint": "Copia questo token ora — viene mostrato una sola volta."
+ "mintedHint": "Copia questo token ora: viene mostrato una sola volta."
},
"advanced": {
"dataPortability": {
@@ -2002,7 +2050,6 @@
"bestand": "Scorta",
"verlauf": "Cronologia",
"injektion": "Iniezione",
- "api": "API",
"erweitert": "Avanzate",
"wirkung": "Effetto"
},
@@ -2014,9 +2061,9 @@
"statusTitle": "Stato",
"nextDoseLabel": "Prossima dose",
"nextDoseNone": "Nessuna dose in programma.",
- "nextDoseAsNeeded": "Al bisogno — nessuna dose programmata.",
+ "nextDoseAsNeeded": "Al bisogno, nessuna dose programmata.",
"nextDoseNotTracked": "Assunzioni non registrate, solo annotato.",
- "pausedHint": "In pausa — nessun promemoria.",
+ "pausedHint": "In pausa, nessun promemoria.",
"reminderLabel": "Promemoria",
"reminderOn": "Attivo",
"reminderOff": "Disattivo",
@@ -2181,7 +2228,7 @@
}
},
"logInjectionSiteTitle": "Registra il sito di iniezione",
- "logInjectionSiteDescription": "Dove hai iniettato {name}? Facoltativo — puoi saltarlo.",
+ "logInjectionSiteDescription": "Dove hai iniettato {name}? Facoltativo, puoi saltarlo.",
"logInjectionSiteSkip": "Salta",
"logInjectionSiteConfirm": "Salva il sito",
"logInjectionSiteNoneAvailable": "Nessun sito di iniezione disponibile: tutti i siti sono esclusi nelle tue impostazioni.",
@@ -2209,9 +2256,8 @@
"tableStockDoses": "{count} dosi",
"tableStockRunway": "≈ {days} giorni rimanenti",
"cardLowStockRunway": "Scorta: ≈ {days} giorni",
- "takeActionFor": "Assumi – {name}",
- "skipActionFor": "Salta – {name}",
- "reorderTitle": "Modifica ordine",
+ "takeActionFor": "Assumi: {name}",
+ "skipActionFor": "Salta: {name}",
"reorderDescription": "Imposta l'ordine dei tuoi farmaci: vale per la vista a schede e per la tabella.",
"reorderSaved": "Ordine salvato",
"reorderSaveFailed": "Impossibile salvare l’ordine",
@@ -2231,7 +2277,7 @@
"levelShift": "È stato rilevato un cambio di livello intorno al {date}.",
"plateau": {
"title": "Plateau del peso alla dose attuale",
- "evidence": "Variazione di peso di {delta} {unit} negli ultimi {window} giorni ({readings} misurazioni) — {days} giorni con {dose}."
+ "evidence": "Variazione di peso di {delta} {unit} negli ultimi {window} giorni ({readings} misurazioni), {days} giorni con {dose}."
},
"retarget": {
"label": "Non è la misurazione giusta?",
@@ -2241,7 +2287,7 @@
"error": "Impossibile salvare questa modifica. Riprova."
},
"beforeAfter": {
- "summary": "Ø {before} prima → Ø {after} da allora — una variazione di {delta}.",
+ "summary": "Ø {before} prima → Ø {after} da allora, una variazione di {delta}.",
"counts": "In base a {beforeCount} misurazioni prima e {afterCount} da allora.",
"needBefore": "Ancora troppe poche misurazioni prima dell'inizio: servono circa {weeks} settimane prima e dopo per confrontare.",
"needAfter": "Ancora troppe poche misurazioni dall'inizio: servono circa {weeks} settimane prima e dopo per confrontare.",
@@ -2356,7 +2402,7 @@
},
"charts": {
"loadingLabel": "Caricamento del grafico…",
- "loadingSlowHint": "Calcolo delle analisi — può richiedere un momento.",
+ "loadingSlowHint": "Calcolo delle analisi. Può richiedere un momento.",
"weight": "Peso",
"bloodPressure": "Pressione arteriosa",
"pulse": "Pulsazioni",
@@ -2378,6 +2424,7 @@
"days7Label": "7 g",
"days7Title": "Ultimi 7 giorni",
"days30Label": "30 g",
+ "daysNLabel": "{days} g",
"days30Title": "Ultimi 30 giorni",
"days90Label": "90 g",
"days90Title": "Ultimi 90 giorni",
@@ -2437,13 +2484,15 @@
"loadErrorTitle": "Impossibile caricare il grafico",
"loadErrorReload": "Ricarica",
"sparseDataCaption": "Più giorni completeranno il trend.",
+ "tooltipHourlyMean": "{metric} (media delle ore)",
"dataTable": {
"toggle": "Punti dati ({count})",
"columnDate": "Data",
"noValue": "Nessun valore",
"captionDay": "Una riga per giorno di {metric}: gli stessi punti disegnati dal grafico.",
"captionWeek": "Una riga per settimana di {metric}, la media dei giorni di quella settimana: gli stessi punti disegnati dal grafico.",
- "captionMonth": "Una riga per mese di {metric}, la media dei giorni di quel mese: gli stessi punti disegnati dal grafico."
+ "captionMonth": "Una riga per mese di {metric}, la media dei giorni di quel mese: gli stessi punti disegnati dal grafico.",
+ "hourlyMeanNote": "{metric} è la media di ogni giorno, con ogni ora conteggiata una sola volta, così un allenamento non pesa più del resto della giornata."
},
"personalBaseline": "La tua media",
"deltaVsBaseline": "{delta} vs. la tua media",
@@ -2471,7 +2520,8 @@
"moodHeatmapEmpty": "Mappa di calore dell'umore, nessun giorno ancora registrato.",
"complianceHeatmap": "Mappa di calore dell'aderenza ai farmaci dal {start} al {end}, {days} giorni attivi.",
"complianceHeatmapEmpty": "Mappa di calore dell'aderenza ai farmaci, nessun giorno attivo."
- }
+ },
+ "medicationsEmptyAction": "Aggiungi un farmaco"
},
"chart": {
"overlay": {
@@ -2502,8 +2552,8 @@
},
"tooltipPrior": "Periodo precedente",
"unavailable": {
- "lastMonth": "Confronto non disponibile — nessun dato del mese precedente",
- "lastYear": "Confronto non disponibile — nessun dato dell’anno precedente"
+ "lastMonth": "Confronto non disponibile: nessun dato del mese precedente",
+ "lastYear": "Confronto non disponibile: nessun dato dell’anno precedente"
},
"notAvailableForCompliance": "Confronto non disponibile per l’aderenza"
},
@@ -2515,7 +2565,9 @@
"dismissError": "Impossibile ignorare questo schema.",
"restoreError": "Impossibile ripristinare questo schema.",
"loadError": "Impossibile caricare gli schemi.",
- "empty": "Nessuno schema attuale ha superato la soglia di evidenza."
+ "empty": "Nessuno schema attuale ha superato la soglia di evidenza.",
+ "seasonalNote": "Dalla versione 1.42 HealthLog toglie stagione e tendenza di lungo periodo prima di confrontare i valori. Vedrai meno relazioni, ma quelle rimaste reggono meglio. Molti degli schemi di prima erano solo il calendario.",
+ "seasonalNoteDismiss": "Chiudi l’avviso"
},
"correlation": {
"direction": {
@@ -2547,29 +2599,37 @@
"ENV_DAYLIGHT": "la luce del giorno",
"ENV_PRECIP": "le precipitazioni",
"ENV_PRESSURE_MEAN": "la pressione atmosferica",
- "ENV_PRESSURE_DELTA": "l'oscillazione di pressione nella giornata"
+ "ENV_PRESSURE_DELTA": "l'oscillazione di pressione nella giornata",
+ "ENV_PM25": "il particolato fine (PM2,5)",
+ "ENV_OZONE_8H": "l’ozono",
+ "ENV_POLLEN_MAX": "i pollini"
},
"daily": {
- "high": "Quando {behaviour} sale, {outcome} del giorno dopo tende a {direction} nei tuoi dati — un andamento da osservare, non una causa.",
- "moderate": "Quando {behaviour} sale, {outcome} del giorno dopo sembra tendere a {direction}, per quanto emerge finora — un andamento da osservare, non una causa.",
- "faint": "Quando {behaviour} sale, al più si intravede un debole indizio che {outcome} del giorno dopo tenda a {direction} nei tuoi dati — troppo lieve per farci affidamento, mai una causa."
+ "high": "Quando {behaviour} sale, {outcome} del giorno dopo tende a {direction} nei tuoi dati. È un andamento da osservare, non una causa.",
+ "moderate": "Quando {behaviour} sale, {outcome} del giorno dopo sembra tendere a {direction}, per quanto emerge finora. È un andamento da osservare, non una causa.",
+ "faint": "Quando {behaviour} sale, al più si intravede nei tuoi dati un debole indizio che {outcome} del giorno dopo tenda a {direction}. È troppo lieve per farci affidamento e mai una causa."
},
"lab": {
- "default": "Valori di {lab} più alti si accompagnano al fatto che {outcome} tende a {direction} negli stessi periodi dei tuoi dati — un'associazione da tenere d'occhio con il tuo medico, mai una causa.",
- "faint": "Valori di {lab} più alti lasciano al più intravedere che {outcome} tenda a {direction} negli stessi periodi dei tuoi dati — troppo lieve per farci affidamento, un'associazione da discutere con il tuo medico, mai una causa."
+ "default": "Valori di {lab} più alti si accompagnano al fatto che {outcome} tende a {direction} negli stessi periodi dei tuoi dati, un'associazione da tenere d'occhio con il tuo medico, mai una causa.",
+ "faint": "Valori di {lab} più alti lasciano al più intravedere che {outcome} tenda a {direction} negli stessi periodi dei tuoi dati. È troppo lieve per farci affidamento e mai una causa, ma da discutere con il tuo medico."
},
"hypothesis": {
"bpCompliance": {
- "support": "Una maggiore aderenza ai farmaci si accompagna a valori sistolici più bassi — un andamento da osservare.",
- "surprising": "L'aderenza e la sistolica si muovono insieme nei tuoi dati — sorprendente; parlane con il tuo medico prima di modificare qualcosa.",
+ "support": "Una maggiore aderenza ai farmaci si accompagna a valori sistolici più bassi, un andamento da osservare.",
+ "surprising": "L'aderenza e la sistolica si muovono insieme nei tuoi dati, il che è sorprendente. Parlane con il tuo medico prima di modificare qualcosa.",
"none": "L'aderenza e la sistolica non si muovono insieme in modo marcato in questo periodo."
},
"moodPulse": {
- "support": "I giorni di umore più basso si accompagnano a un polso a riposo più alto — un andamento da osservare.",
- "surprising": "I giorni di umore migliore si accompagnano a un polso a riposo più alto nei tuoi dati — sorprendente.",
+ "support": "I giorni di umore più basso si accompagnano a un polso a riposo più alto, un andamento da osservare.",
+ "surprising": "I giorni di umore migliore si accompagnano a un polso a riposo più alto nei tuoi dati, il che è sorprendente.",
"none": "L'umore e il polso a riposo non si muovono insieme in modo marcato in questo periodo."
},
- "weightWeekday": "Il peso del {weekday} è {delta} kg {direction} la tua media degli altri giorni — un andamento da osservare."
+ "weightWeekday": "Il peso del {weekday} è {delta} kg {direction} la tua media degli altri giorni, un andamento da osservare."
+ },
+ "dailySameDay": {
+ "high": "Più {behaviour} nello stesso giorno e in quello prima tende ad accompagnarsi a {direction} {outcome} nei tuoi dati: uno schema da osservare, non una causa.",
+ "moderate": "Più {behaviour} nello stesso giorno e in quello prima sembra accompagnarsi a {direction} {outcome} nei tuoi dati, per quanto visto finora: uno schema da osservare, non una causa.",
+ "faint": "Più {behaviour} nello stesso giorno e in quello prima mostra al massimo un debole indizio di {direction} {outcome} nei tuoi dati: troppo poco per contarci, mai una causa."
}
},
"noAnalysisYet": "Ancora nessuna valutazione.",
@@ -2616,7 +2676,7 @@
"narrativeWeek": "Questa settimana",
"narrativeMonth": "Questo mese",
"narrativeUpdating": "Aggiornamento…",
- "heroFallbackSubtitle": "Uno sguardo quotidiano ai tuoi trend — direttamente dai numeri che registri.",
+ "heroFallbackSubtitle": "Uno sguardo quotidiano ai tuoi trend, direttamente dai numeri che registri.",
"heroGreetingMorning": "Buongiorno",
"heroGreetingAfternoon": "Buon pomeriggio",
"heroGreetingEvening": "Buonasera",
@@ -2681,7 +2741,7 @@
"referenceSleep": "Almeno {low} minuti per notte; il sonno più lungo non viene penalizzato",
"referenceAdiposity": "Rapporto vita-altezza inferiore a {high}",
"referenceHba1c": "HbA1c da {low} a {high}%",
- "referenceDetail": "{label} · {source} · {kind} · da {low} a {high}",
+ "referenceDetail": "{label}: da {low} a {high} ({kind}, {source})",
"openBound": "aperto",
"laboratoryReferenceSource": "Intervalli di riferimento del laboratorio",
"personalTargetSource": "Obiettivo personale",
@@ -2716,7 +2776,8 @@
"coverage": {
"recommendedMet": "Copre i tre ambiti consigliati dal punteggio",
"areas": "{present} ambiti di salute su {required}"
- }
+ },
+ "historyLink": "Andamento"
},
"dailyBriefing": {
"title": "Briefing del giorno",
@@ -2775,7 +2836,7 @@
"experimentCta": "Avvia esperimento di 7 giorni",
"experimentTooltip": "In arrivo",
"emptyTitle": "Servono più dati per mostrare questa correlazione",
- "sourceChip": "su {n} misurazioni appaiate · {window}",
+ "sourceChip": "su {n} misurazioni appaiate ({window})",
"sourceWindowLast30": "Ultimi 30 giorni",
"confidenceHigh": "Confidenza alta",
"confidenceModerate": "Confidenza media",
@@ -2790,13 +2851,14 @@
}
},
"coach": {
- "refusalOutOfScope": "Posso aiutarti solo con i dati di salute del tuo log — misurazioni, farmaci, umore, trend. Domande fuori da questo ambito non le rispondo.",
- "refusalInjection": "Nel tuo messaggio ci sono istruzioni che cercano di sovrascrivere le mie indicazioni. Sono il coach di HealthLog e riassumo solo i tuoi dati di salute — riformula la domanda in questo perimetro.",
- "errorBudget": "Hai raggiunto il limite giornaliero del coach. Il budget si reimposta a mezzanotte (UTC).",
+ "refusalOutOfScope": "Posso aiutarti solo con i dati di salute del tuo log: misurazioni, farmaci, umore, trend. Domande fuori da questo ambito non le rispondo.",
+ "refusalInjection": "Nel tuo messaggio ci sono istruzioni che cercano di sovrascrivere le mie indicazioni. Sono il coach di HealthLog e riassumo solo i tuoi dati di salute: riformula la domanda in questo perimetro.",
+ "errorBudget": "Hai raggiunto il limite giornaliero del coach. Il budget si reimposta a mezzanotte nel tuo fuso orario.",
"errorProvider": "Il coach non è riuscito a contattare un provider di analisi ora. Riprova fra poco.",
+ "errorDemo": "Nella demo il Coach non risponde.",
"errorNoProvider": "Il coach legge i tuoi dati di salute registrati e risponde a domande su di essi (non è consulenza medica). HealthLog ha bisogno di un provider IA per questo: collega la tua chiave o un modello locale.",
"errorNoProviderAction": "Collega un provider IA",
- "errorNetwork": "Nessuna connessione Internet — riprova quando sei di nuovo online.",
+ "errorNetwork": "Nessuna connessione Internet. Riprova quando sei di nuovo online.",
"prefsSaveError": "Impossibile salvare le preferenze del Coach. Riprova.",
"errorCredentialExpired": "La connessione al provider di IA è scaduta. Riconnettila nelle Impostazioni per mantenere attivo il Coach.",
"tagline": "Coach personale di salute, basato sui tuoi dati.",
@@ -2815,7 +2877,7 @@
"thinking": "Sto pensando…",
"unverifiedFiguresNotice": "Alcuni numeri non è stato possibile verificarli con i tuoi dati e sono stati omessi.",
"composerPlaceholder": "Chiedimi pure …",
- "nudgeBubbleLabel": "Nuovo messaggio dal coach — apri la chat",
+ "nudgeBubbleLabel": "Nuovo messaggio dal coach: apri la chat",
"fabLabel": "Apri il coach",
"readAloud": "Leggi ad alta voce",
"readAloudStop": "Interrompi lettura",
@@ -2833,14 +2895,13 @@
},
"rememberMessage": {
"action": "Memorizza",
- "done": "Salvato nelle tue informazioni personali",
- "duplicate": "Già presente nelle tue informazioni personali",
- "failed": "Salvataggio non riuscito. Riprova."
+ "failed": "Salvataggio non riuscito. Riprova.",
+ "alreadyKept": "Il Coach lo ricorda già."
},
"guided": {
"offerTitle": "Il Coach ha {count} domande per te",
"offerTitleOne": "Il Coach ha una domanda per te",
- "offerBody": "Risposte brevi affinano le tue informazioni personali — una domanda alla volta, qui in chat.",
+ "offerBody": "Risposte brevi affinano le tue informazioni personali, una domanda alla volta, qui in chat.",
"offerStart": "Rispondi ora",
"offerLater": "Più tardi",
"offerDismiss": "Non chiedere più",
@@ -2850,7 +2911,7 @@
"later": "Più tardi",
"dismissRemaining": "Non chiedere più",
"answerPlaceholder": "La tua risposta…",
- "summaryTitle": "Fatto — grazie per le tue risposte!",
+ "summaryTitle": "Fatto, grazie per le tue risposte!",
"summaryAnswered": "{answered} di {total} risposte",
"summaryAdopted": "{adopted} aggiunte alle tue informazioni personali",
"summaryAdoptedNone": "nulla di nuovo aggiunto alle tue informazioni personali",
@@ -2862,6 +2923,7 @@
"historyEmpty": "Le tue conversazioni compaiono qui. Fai una domanda per iniziare.",
"historyDeleteAria": "Elimina conversazione",
"historyDeleted": "Conversazione eliminata",
+ "historyDeleteFailed": "Non è stato possibile eliminare la conversazione.",
"rename": {
"action": "Rinomina conversazione",
"input": "Titolo della conversazione",
@@ -2875,7 +2937,6 @@
"sourcesFresh": "Aggiornate",
"sourcesStale": "Datate",
"sourcesFooter": "Il coach legge solo i dati che hai collegato.",
- "evidenceLabel": "Su cosa si basa?",
"metric": {
"bp": "Pressione arteriosa",
"weight": "Peso",
@@ -2921,14 +2982,6 @@
"skin_temp": "Temperatura cutanea",
"workouts": "Allenamenti"
},
- "window": {
- "last7days": "Ultimi 7 giorni",
- "last30days": "Ultimi 30 giorni",
- "last90days": "Ultimi 90 giorni",
- "lastYear": "ultimi 12 mesi",
- "allTime": "tutto il tempo (12 mesi in dettaglio)"
- },
- "windowLabel": "Periodo",
"settingsAriaLabel": "Impostazioni del coach",
"settingsTitle": "Impostazioni del coach",
"settingsDescription": "Imposta come il coach ti parla.",
@@ -2941,8 +2994,9 @@
"settingsVerbosityDefault": "Standard",
"settingsVerbosityDetailed": "Dettagliata",
"settingsVerbosityConciseHint": "Il tono conciso mantiene già le risposte brevi: la verbosità resta fissata su «Breve» finché è attivo.",
- "settingsSourcesPointer": "I gruppi di dati che il coach può usare e la finestra di analisi predefinita si gestiscono direttamente nella chat del coach (pannello «Su cosa mi baso»).",
- "settingsSourcesPointerLink": "Apri il coach",
+ "settingsSourcesSummary": "Il coach vede {count} aree di dati su {total} e guarda indietro di {window}.",
+ "settingsSourcesSummaryAll": "Il coach vede {count} aree di dati su {total} e l’intero storico.",
+ "settingsSourcesPointerLink": "Apri le impostazioni del coach",
"settingsExcludeLabel": "Escludi metriche",
"settingsExcludeHint": "Il coach non vede i dati delle metriche selezionate.",
"settingsContextLabel": "Invia contesto",
@@ -2954,34 +3008,30 @@
"settingsSave": "Salva",
"settingsSaved": "Salvato",
"settingsCancel": "Annulla",
- "feedbackHelpful": "Utile",
- "feedbackUnhelpful": "Poco utile",
- "feedbackThanks": "Grazie per il segnale.",
- "feedbackError": "Impossibile salvare il feedback",
"dailyLimitTitle": "Limite giornaliero raggiunto",
- "dailyLimitBody": "Limite giornaliero raggiunto. Il budget si reimposta a mezzanotte (UTC).",
+ "dailyLimitBody": "Limite giornaliero raggiunto. Il budget si reimposta a mezzanotte nel tuo fuso orario.",
"providerRateLimitTitle": "Provider con rate-limit",
"providerRateLimitBody": "Il provider è temporaneamente sovraccarico; reset in ~5 min.",
"cluster": {
"cardio": {
"label": "Cardiovascolare",
- "hint": "Pressione · polso · HRV · FC riposo e cammino"
+ "hint": "Pressione, polso, HRV, FC a riposo e in cammino"
},
"body": {
"label": "Composizione corporea",
- "hint": "Peso · grasso · muscoli · IMC"
+ "hint": "Peso, grasso, muscoli e IMC"
},
"activity": {
"label": "Attività",
- "hint": "Passi · energia · distanza · VO₂ max"
+ "hint": "Passi, energia, distanza e VO₂ max"
},
"workouts": {
"label": "Allenamenti",
- "hint": "Corse · uscite · sessioni"
+ "hint": "Corse, uscite e sessioni"
},
"sleep": {
"label": "Sonno",
- "hint": "Durata · fasi"
+ "hint": "Durata e fasi"
},
"mood": {
"label": "Umore",
@@ -2993,56 +3043,68 @@
},
"medication": {
"label": "Farmaci",
- "hint": "Aderenza · contesto GLP-1"
+ "hint": "Aderenza e contesto GLP-1"
},
"mobility": {
"label": "Mobilità e andatura",
- "hint": "Stabilità · asimmetria · andatura"
+ "hint": "Stabilità, asimmetria e andatura"
},
"environment": {
"label": "Ambiente",
- "hint": "Rumore · luce · temp. cutanea"
+ "hint": "Rumore, luce del giorno e temperatura cutanea"
}
},
"settingsClustersLabel": "Dati che il Coach vede",
"settingsClustersHint": "Scegli quali gruppi di dati il Coach può leggere. Più dati significano risposte più ricche e un prompt più grande.",
"settingsClusterNoData": "Ancora nessun dato",
- "sourcesMemberCount": "{count} metriche",
+ "sourcesMemberCountOne": "1 metrica",
+ "sourcesMemberCountFew": "{count} metriche",
+ "sourcesMemberCountOther": "{count} metriche",
+ "lookback": {
+ "label": "Fin dove il Coach può guardare indietro",
+ "option": {
+ "last7days": "{days} giorni",
+ "last30days": "{days} giorni",
+ "last90days": "{days} giorni",
+ "lastYear": "{months} mesi",
+ "allTime": "Tutto"
+ },
+ "detailDays": "Il Coach non legge nulla di più vecchio di {days} giorni, in nessuna risposta.",
+ "detailMonths": "Il Coach non legge nulla di più vecchio di {months} mesi, in nessuna risposta.",
+ "detailAll": "Tutto ciò che hai registrato: {dailyMonths} mesi giorno per giorno, le misurazioni più vecchie come valori mensili fino a {years} anni fa, gli esami degli ultimi {labMonths} mesi.",
+ "note": "Il tuo profilo, i farmaci attuali, una malattia in corso e le prossime visite restano visibili; i riepiloghi che richiedono più storico vengono accorciati o omessi. Le risposte date in precedenza dal Coach nelle conversazioni più vecchie possono ancora citare valori più vecchi."
+ },
"showConversations": "Mostra conversazioni",
"hideConversations": "Nascondi conversazioni",
"heroGreeting": "Chiedimi qualcosa sui tuoi dati",
"heroSubline": "Chiedi dei tuoi andamenti, farmaci o misurazioni.",
- "thinkingElapsed": "{seconds}s",
"thinkingDuration": "Ha riflettuto per {seconds}s",
"thinkingExpandAria": "Mostra cosa ha considerato il Coach",
"thinkingDetail": "Risposta basata sul tuo riepilogo di salute.",
"tokensUsed": "{count} token",
- "tokensUsedWithModel": "{count} token · {model}",
"askAboutThis": "Chiedi al coach",
"readStrip": {
"label": "Intervallo abituale",
"baselineWithin": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} rientra al suo interno.",
"baselineAbove": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} si colloca al di sopra.",
"baselineBelow": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} si colloca al di sotto.",
- "insufficient": "Sto ancora imparando il tuo intervallo abituale — continua a registrare per qualche giorno in più.",
+ "sameHoursWithin": "A quest'ora del giorno di solito sei a {low}–{high} {unit}; oggi finora {value} rientra nell'intervallo.",
+ "sameHoursAbove": "A quest'ora del giorno di solito sei a {low}–{high} {unit}; oggi finora {value} si colloca al di sopra.",
+ "sameHoursBelow": "A quest'ora del giorno di solito sei a {low}–{high} {unit}; oggi finora {value} si colloca al di sotto.",
+ "steadyWithin": "I tuoi valori sono di solito {low} {unit}; oggi {value} corrisponde.",
+ "steadyAbove": "I tuoi valori sono di solito {low} {unit}; oggi {value} è al di sopra.",
+ "steadyBelow": "I tuoi valori sono di solito {low} {unit}; oggi {value} è al di sotto.",
+ "latestWithin": "Il tuo intervallo abituale è {low}–{high} {unit}; la tua ultima misurazione, del {date}, era {value}, al suo interno.",
+ "latestAbove": "Il tuo intervallo abituale è {low}–{high} {unit}; la tua ultima misurazione, del {date}, era {value}, al di sopra.",
+ "latestBelow": "Il tuo intervallo abituale è {low}–{high} {unit}; la tua ultima misurazione, del {date}, era {value}, al di sotto.",
+ "steadyLatestWithin": "I tuoi valori sono di solito {low} {unit}; la tua ultima misurazione, del {date}, era {value}, in linea.",
+ "steadyLatestAbove": "I tuoi valori sono di solito {low} {unit}; la tua ultima misurazione, del {date}, era {value}, al di sopra.",
+ "steadyLatestBelow": "I tuoi valori sono di solito {low} {unit}; la tua ultima misurazione, del {date}, era {value}, al di sotto.",
+ "insufficient": "Sto ancora imparando il tuo intervallo abituale. Continua a registrare per qualche giorno in più.",
"driver": "{note}"
},
"scope": {
- "prefix": "Il Coach è già su {metric}",
- "tap": "Tocca per chiedere",
- "question": "Spiegami questo parametro — c'è qualcosa da tenere d'occhio?"
- },
- "seeded": {
- "prefix": "Oggi vale uno sguardo: {signal}",
- "signal": {
- "readiness": "prontezza",
- "recovery": "recupero"
- },
- "question": {
- "readiness": "La mia prontezza è anomala oggi — da cosa dipende?",
- "recovery": "Il mio recupero è cambiato — cosa c'è dietro?"
- },
- "dismiss": "Ignora"
+ "prefix": "Il Coach è già su {metric}"
},
"settings": "Impostazioni",
"history": {
@@ -3052,31 +3114,31 @@
"groupEarlier": "Precedenti"
},
"seed": {
- "bloodPressure": "Esamina con me l'andamento della mia pressione arteriosa negli ultimi 30 giorni — c'è qualcosa che dovrei tenere d'occhio?",
+ "bloodPressure": "Esamina con me l'andamento della mia pressione arteriosa negli ultimi 30 giorni: c'è qualcosa che dovrei tenere d'occhio?",
"weight": "Come si è evoluto il mio peso ultimamente e la direzione è qualcosa su cui intervenire?",
"bmi": "Cosa mi dice l'andamento del mio BMI e come dovrei interpretarlo?",
- "pulse": "Esamina con me le mie letture del battito — c'è qualcosa fuori dal comune?",
+ "pulse": "Esamina con me le mie letture del battito: c'è qualcosa fuori dal comune?",
"restingHr": "Come si è evoluta la mia frequenza cardiaca a riposo e cosa dice sulla mia forma fisica?",
"hrv": "Cosa mi dice la mia variabilità della frequenza cardiaca su recupero e stress ultimamente?",
- "sleep": "Esamina con me il mio sonno recente — durata, regolarità e qualsiasi cosa valga la pena cambiare.",
+ "sleep": "Esamina con me il mio sonno recente: durata, regolarità e qualsiasi cosa valga la pena cambiare.",
"mood": "Quali schemi noti nel mio umore nelle ultime settimane?",
"medications": "Com'è la mia aderenza alla terapia e corrisponde a come mi sono sentito?",
"steps": "Quanto sono stato attivo ultimamente in base ai miei passi e come si confronta con un valore di riferimento sano?",
"activeEnergy": "Cosa dice il mio andamento dell'energia attiva sul mio livello di attività ultimamente?",
"cardioFitness": "Cosa significa l'andamento della mia forma cardiorespiratoria (VO₂ max) e come la indirizzo nella direzione giusta?",
- "bloodGlucose": "Esamina con me le mie ultime letture di glicemia — sono in un intervallo sano?",
+ "bloodGlucose": "Esamina con me le mie ultime letture di glicemia: sono in un intervallo sano?",
"oxygenSaturation": "Cosa mi dice l'andamento della mia saturazione di ossigeno (SpO₂)?",
"respiratoryRate": "Cosa dice il mio andamento della frequenza respiratoria sulla mia salute?",
- "workouts": "Esamina con me i miei allenamenti recenti — carico, frequenza e come recupero tra uno e l'altro.",
+ "workouts": "Esamina con me i miei allenamenti recenti: carico, frequenza e come recupero tra uno e l'altro.",
"recoveryPage": "Perché il mio recupero è dov'è in questo momento e cosa lo determina?",
- "briefing": "Esamina con me il riepilogo di oggi — su cosa dovrei concentrarmi e c'è qualcosa su cui vale la pena agire?",
+ "briefing": "Esamina con me il riepilogo di oggi: su cosa dovrei concentrarmi e c'è qualcosa su cui vale la pena agire?",
"healthScore": "Perché il mio punteggio di salute è {score} su 100 e cosa lo migliorerebbe di più?",
- "periodWeek": "Esamina con me com'è andata la mia ultima settimana sui miei parametri di salute — cosa spicca?",
- "periodMonth": "Esamina con me com'è andato il mio ultimo mese sui miei parametri di salute — cosa spicca?",
+ "periodWeek": "Esamina con me com'è andata la mia ultima settimana sui miei parametri di salute: cosa spicca?",
+ "periodMonth": "Esamina con me com'è andato il mio ultimo mese sui miei parametri di salute: cosa spicca?",
"correlationBpCompliance": "Che relazione c'è tra la mia aderenza alla terapia e la mia pressione arteriosa? Spiegami cosa significa per me questa correlazione.",
"correlationMoodPulse": "C'è un legame reale tra il mio umore e il mio battito? Aiutami a interpretare questa correlazione.",
"correlationWeightWeekday": "Il mio peso segue uno schema settimanale? Cosa mi dice questa correlazione per giorno della settimana?",
- "recommendation": "Dimmi di più su questa raccomandazione: \"{text}\" — perché si applica a me e cosa dovrei fare?"
+ "recommendation": "Dimmi di più su questa raccomandazione: \"{text}\". Perché si applica a me e cosa dovrei fare?"
},
"assessmentPrompt": "Spiegami la mia valutazione di {metric}: cosa significa e cosa dovrei fare?",
"attach": {
@@ -3111,7 +3173,102 @@
"errorNotPermitted": "Il Coach non è disponibile in una cartella che qualcuno condivide con te.",
"errorUnavailable": "Il Coach non è riuscito a controllare le sue impostazioni. Riprova tra un momento.",
"errorOperatorOff": "L'IA è disattivata su questo server, quindi il Coach non può rispondere.",
- "errorCoachOff": "Il Coach è disattivato. Puoi riattivarlo in Impostazioni, Moduli."
+ "errorCoachOff": "Il Coach è disattivato. Puoi riattivarlo in Impostazioni, Moduli.",
+ "frame": {
+ "showPanel": "Mostra conversazioni",
+ "hidePanel": "Nascondi conversazioni",
+ "panelDescription": "Le tue conversazioni con il coach, dalla più recente.",
+ "rowActions": "Azioni per {title}",
+ "rename": "Rinomina",
+ "delete": "Elimina",
+ "modelSection": "Modello",
+ "provider": "Risponde con",
+ "modelManaged": "Il modello di questo fornitore si imposta nelle impostazioni IA.",
+ "noProvider": "Nessun fornitore disponibile",
+ "saving": "Salvataggio…",
+ "saveFailed": "Impossibile salvare la modifica.",
+ "allSettings": "Tutte le impostazioni IA",
+ "providerNotActive": "{provider} non è subentrato: manca un accesso o una chiave funzionante. Configuralo nelle impostazioni IA.",
+ "operatorCodex": "ChatGPT (condiviso dall'operatore)",
+ "operatorManaged": "Questa connessione è gestita dall'operatore del tuo server. Quando configuri uno dei tuoi fornitori, risponde per primo."
+ },
+ "answer": {
+ "scrollToBottom": "Vai all'ultimo messaggio",
+ "rememberDone": "Memorizzato",
+ "timeYesterday": "ieri {time}",
+ "timeEarlier": "{date}, {time}",
+ "info": "Dettagli della risposta",
+ "infoModel": "Modello: {model}"
+ },
+ "activity": {
+ "thinking": "Sta pensando…",
+ "fetching": "Recupera {domain}, {window}…",
+ "digestOne": "Riassume {count} valore…",
+ "digestFew": "Riassume {count} valori…",
+ "digestOther": "Riassume {count} valori…",
+ "digestDoneOne": "{count} valore da {areas}",
+ "digestDoneFew": "{count} valori da {areas}",
+ "digestDoneOther": "{count} valori da {areas}",
+ "areasOne": "{count} area",
+ "areasFew": "{count} aree",
+ "areasOther": "{count} aree",
+ "memoryOne": "Ricorda {count} cosa che gli hai detto",
+ "memoryFew": "Ricorda {count} cose che gli hai detto",
+ "memoryOther": "Ricorda {count} cose che gli hai detto",
+ "remember": "Memorizza qualcosa…",
+ "plan": "Prepara un piano…",
+ "asking": "Ti fa una domanda…",
+ "answer": "Scrive la risposta…",
+ "stop": {
+ "budget": "Budget raggiunto, risponde con ciò che ha",
+ "time": "Tempo scaduto, risponde con ciò che ha",
+ "cap": "Molti passaggi, ora riassume",
+ "noProgress": "Nessun dato nuovo, riassume"
+ },
+ "thoughtProcessOne": "Ragionamento ({count} passaggio)",
+ "thoughtProcessFew": "Ragionamento ({count} passaggi)",
+ "thoughtProcessOther": "Ragionamento ({count} passaggi)",
+ "awaitingAnswer": "Serve una tua risposta"
+ },
+ "memory": {
+ "saved": "Memorizzato: {fact}",
+ "undo": "Annulla",
+ "accept": "Sì, ricordalo",
+ "decline": "No",
+ "confirmed": "Ricevuto, me lo ricorderò.",
+ "declined": "D'accordo, non lo faccio."
+ },
+ "plan": {
+ "accept": "Adotta questo piano",
+ "decline": "Non ora",
+ "confirmed": "Annotato. Ne riparliamo insieme tra {days} giorni.",
+ "declined": "D'accordo, non ora."
+ },
+ "assumption": {
+ "line": "Ipotesi: {value}",
+ "change": "Cambia"
+ },
+ "suggestedReplies": {
+ "groupLabel": "Risposte suggerite"
+ },
+ "chart": {
+ "compare": {
+ "periods": "{a} rispetto a {b}"
+ }
+ },
+ "reasoning": {
+ "label": "Profondità di ragionamento",
+ "hint": "Pensare più a fondo significa risposte più precise e un po' più di attesa.",
+ "off": "Disattivato",
+ "minimal": "Minima",
+ "low": "Bassa",
+ "medium": "Media",
+ "high": "Alta",
+ "capped": "(limitato dall'amministratore)",
+ "disabled": "L'amministratore ha disattivato il ragionamento approfondito."
+ },
+ "memoryLink": "Memoria ({count})",
+ "newConversation": "Nuova conversazione"
},
"relativeJustNow": "proprio ora",
"relativeMinutesAgoOne": "{count} minuto fa",
@@ -3174,7 +3331,7 @@
},
"hrv": {
"title": "HRV",
- "description": "Variabilità della frequenza cardiaca nel tempo — segnale di recupero del sistema nervoso, captato di notte da Apple Watch e altri wearable compatibili.",
+ "description": "Variabilità della frequenza cardiaca nel tempo: segnale di recupero del sistema nervoso, captato di notte da Apple Watch e altri wearable compatibili.",
"chartTitle": "Variabilità della frequenza cardiaca",
"emptyState": {
"title": "Ancora nessun valore HRV",
@@ -3183,7 +3340,7 @@
},
"restingHr": {
"title": "FC a riposo",
- "description": "Il livello cardiaco di base sulla giornata — distinto dalle frequenze puntuali della pagina Polso. Apple Salute fornisce un valore al giorno.",
+ "description": "Il livello cardiaco di base sulla giornata, distinto dalle frequenze puntuali della pagina Polso. Apple Salute fornisce un valore al giorno.",
"chartTitle": "Frequenza cardiaca a riposo",
"emptyState": {
"title": "Ancora nessuna FC a riposo",
@@ -3193,7 +3350,7 @@
},
"oxygenSaturation": {
"title": "Saturazione di ossigeno",
- "description": "Ossigeno nel sangue (SpO₂) — intervallo clinico normale 95–100 %, con zone obiettivo ombreggiate sul grafico.",
+ "description": "Ossigeno nel sangue (SpO₂): intervallo clinico normale 95–100 %, con zone obiettivo ombreggiate sul grafico.",
"chartTitle": "Saturazione di ossigeno",
"emptyState": {
"title": "Ancora nessun campione SpO₂",
@@ -3206,13 +3363,13 @@
"chartTitle": "Temperatura corporea",
"emptyState": {
"title": "Ancora nessuna temperatura corporea",
- "description": "Registra una misura o lascia che il tuo wearable ne sincronizzi una — l'andamento apparirà qui.",
+ "description": "Registra una misura o lascia che il tuo wearable ne sincronizzi una: l'andamento apparirà qui.",
"cta": "Registra temperatura corporea"
}
},
"steps": {
"title": "Passi",
- "description": "Conteggio giornaliero dei passi — una riga al giorno, da Apple Salute o da un dispositivo di attività collegato.",
+ "description": "Conteggio giornaliero dei passi: una riga al giorno, da Apple Salute o da un dispositivo di attività collegato.",
"chartTitle": "Passi al giorno",
"emptyState": {
"title": "Ancora nessun dato sui passi",
@@ -3221,7 +3378,7 @@
},
"activeEnergy": {
"title": "Energia attiva",
- "description": "Totale giornaliero di energia attiva in kilocalorie — una riga al giorno, da Apple Salute o dall'attività Withings.",
+ "description": "Totale giornaliero di energia attiva in kilocalorie: una riga al giorno, da Apple Salute o dall'attività Withings.",
"chartTitle": "Energia attiva",
"emptyState": {
"title": "Ancora nessun dato di energia attiva",
@@ -3245,7 +3402,7 @@
"weightBody": "Il peso corporeo è uno dei segnali più semplici dell'equilibrio energetico a lungo termine e della salute generale. L'andamento nelle settimane conta molto più di una singola misurazione, che varia con l'idratazione, il cibo e l'ora del giorno.",
"bmiBody": "L'IMC è il peso diviso per l'altezza al quadrato, un singolo numero che ti colloca in un'ampia categoria di peso. È uno screening di popolazione, non una diagnosi, e non distingue il muscolo dal grasso.",
"sleepBody": "La durata del sonno è quanto hai effettivamente dormito nella notte, suddivisa in fasi leggera, profonda e REM. La maggior parte degli adulti necessita di circa sette-nove ore; quantità e qualità costanti favoriscono recupero, umore e metabolismo.",
- "moodBody": "Il monitoraggio dell'umore registra come ti senti nel tempo su una scala semplice. Annotarlo con regolarità rivela schemi — come sonno, attività o farmaci si allineano a come ti senti — che un singolo giorno non mostra mai.",
+ "moodBody": "Il monitoraggio dell'umore registra come ti senti nel tempo su una scala semplice. Annotarlo con regolarità rivela schemi (come sonno, attività o farmaci si allineano a come ti senti) che un singolo giorno non mostra mai.",
"medicationsBody": "L'aderenza misura quanto regolarmente assumi i farmaci come prescritto, qui come quota delle dosi previste che hai registrato. Un'assunzione costante è spesso ciò che fa funzionare una terapia come previsto.",
"restingHrBody": "La frequenza cardiaca a riposo è il battito quando sei completamente a riposo, di solito misurato durante la notte. Una frequenza più bassa indica in genere una migliore forma cardiovascolare, mentre un andamento in aumento può essere un segnale precoce di stress o malattia.",
"hrvBody": "L'HRV è la piccola variazione del tempo tra battiti consecutivi. Una variabilità più alta riflette di solito un sistema nervoso ben recuperato e adattabile, mentre un calo prolungato può segnalare affaticamento, stress o recupero insufficiente.",
@@ -3257,7 +3414,7 @@
"bodyWaterBody": "L'acqua corporea totale è la quota del tuo peso costituita da liquido, dentro e intorno alle cellule. Varia con l'idratazione, l'alimentazione e l'attività, quindi l'andamento nelle settimane dice più di una singola misura.",
"boneMassBody": "La massa ossea è il peso stimato dei minerali del tuo scheletro. Le stime da bilancia sono approssimative, ma un andamento stabile o in aumento sostiene la robustezza ossea a lungo termine.",
"fatFreeMassBody": "La massa magra è tutto ciò che nel corpo non è grasso: muscoli, ossa, organi e acqua. Mantenerla o aumentarla mentre si perde peso indica di solito una composizione corporea più sana.",
- "fatMassBody": "La massa grassa è il peso totale del grasso nel corpo. Un po' di grasso è essenziale per gli ormoni e le riserve di energia, ma un eccesso — soprattutto sull'addome — aumenta il rischio per la salute a lungo termine (WHO 2024).",
+ "fatMassBody": "La massa grassa è il peso totale del grasso nel corpo. Un po' di grasso è essenziale per gli ormoni e le riserve di energia, ma un eccesso, soprattutto sull'addome, aumenta il rischio per la salute a lungo termine (WHO 2024).",
"bodyFatBody": "La percentuale di grasso corporeo è la parte del tuo peso costituita da grasso. Gli intervalli sani dipendono da sesso ed età, e le bilance la stimano con la bioimpedenza, quindi l'andamento su più settimane dice più di una singola misura.",
"muscleMassBody": "La massa muscolare è il peso stimato dei muscoli del corpo. Più muscolo sostiene forza, metabolismo e mobilità, e di solito richiede impegno attivo per essere mantenuto con l'età.",
"visceralFatBody": "Il grasso viscerale è il grasso depositato in profondità nell'addome, intorno agli organi. A differenza del grasso sottocutaneo, un livello elevato è strettamente legato a malattie cardiache e diabete di tipo 2, quindi più basso è in genere meglio.",
@@ -3288,11 +3445,11 @@
"breathingDisturbancesBody": "Un wearable può stimare quanto spesso la respirazione si è interrotta durante il sonno. Un indice più basso è migliore; un livello segnalato come elevato, soprattutto su molte notti, andrebbe segnalato a un medico. È un'approssimazione di screening, non una diagnosi di apnea del sonno.",
"stepsBody": "Il conteggio dei passi è il numero totale di passi che fai ogni giorno: la misura più semplice del movimento quotidiano. Un'abitudine costante conta più di un singolo giorno intenso, e l'obiettivo tondo di 10.000 passi è uno slogan pubblicitario, non un dato della ricerca.",
"readinessBody": "La prontezza è una lettura giornaliera di quanto il tuo corpo sia pronto ad affrontare la giornata, combinando frequenza cardiaca a riposo, variabilità della frequenza cardiaca e sonno. È un'approssimazione del dispositivo rispetto alla tua baseline, non una misura clinica: un punteggio più alto indica recupero, uno più basso è un invito gentile a rallentare.",
- "sleepScoreBody": "Il tuo punteggio del sonno riassume un'intera notte in un solo numero — durata, efficienza, regolarità degli orari ed equilibrio tra sonno leggero, profondo e REM. Osserva l'andamento nell'arco delle settimane invece di rincorrere un punteggio perfetto in una singola notte.",
+ "sleepScoreBody": "Il tuo punteggio del sonno riassume un'intera notte in un solo numero: durata, efficienza, regolarità degli orari ed equilibrio tra sonno leggero, profondo e REM. Osserva l'andamento nell'arco delle settimane invece di rincorrere un punteggio perfetto in una singola notte.",
"recoveryBody": "Il recupero riflette quanto bene il tuo corpo si è ripreso, basandosi su segnali notturni come la variabilità della frequenza cardiaca e la frequenza a riposo. Un punteggio alto è il via libera per spingere; uno più basso suggerisce che il tuo organismo si sta ancora ricostruendo.",
"strainBody": "Lo sforzo misura quanto carico cardiovascolare e fisico hai imposto al tuo corpo, dagli allenamenti a una giornata attiva in piedi. Quando c'è abbastanza storico, viene valutato rispetto al tuo sforzo abituale, non rispetto a uno standard esterno.",
"stressBody": "Lo stress qui è una stima del carico fisiologico sul tuo sistema nervoso, letto soprattutto dagli schemi di variabilità della frequenza cardiaca e non da come ti senti consapevolmente. Un punteggio più basso indica uno stato calmo e ben regolato; uno più alto è un invito a inserire del recupero.",
- "recoveryPageBody": "Il recupero riunisce i segnali notturni registrati dal tuo wearable — ricarica, sforzo, carico e i totali di frequenza cardiaca ed energia della giornata — per mostrare quanto bene il tuo corpo si è ripreso. Guarda l'andamento su più giorni piuttosto che un singolo numero; un valore più basso è un invito a rallentare, non un allarme.",
+ "recoveryPageBody": "Il recupero riunisce i segnali notturni registrati dal tuo wearable (ricarica, sforzo, carico e i totali di frequenza cardiaca ed energia della giornata) per mostrare quanto bene il tuo corpo si è ripreso. Guarda l'andamento su più giorni piuttosto che un singolo numero; un valore più basso è un invito a rallentare, non un allarme.",
"pulsePressureBody": "La pressione differenziale è la differenza tra il valore sistolico e quello diastolico (sistolica meno diastolica). Come orientamento generale, circa 40 mmHg è tipico a riposo; un divario ampio sopra i 60 mmHg circa nella mezza età può riflettere arterie più rigide, mentre un divario stretto sotto i 25 mmHg circa può accompagnare una bassa gittata sistolica (StatPearls/NIH 2023). È uno spunto di discussione, non una diagnosi.",
"meanArterialPressureBody": "La pressione arteriosa media è la pressione media che i tuoi organi vedono in un battito, all'incirca la diastolica più un terzo del divario con la sistolica. Come orientamento generale, un valore a riposo si aggira sui 70–100 mmHg (StatPearls/NIH 2023). La soglia di ~60 mmHg spesso citata è un concetto di terapia intensiva, non un allarme per l'automisurazione.",
"cycleEducationBody": "Il tuo ciclo attraversa quattro fasi guidate dagli ormoni, e la sua durata e regolarità sono una finestra accessibile sulla salute generale. Come orientamento generale, nei adulti i cicli durano tipicamente 21–35 giorni (i riferimenti clinici citano 24–38), con un sanguinamento di circa cinque giorni (ACOG 2015). La fase luteale è l'ancora stabile, quindi l'ovulazione si stima meglio contando a ritroso dalla prossima mestruazione attesa, mai con un «giorno 14» fisso. Una finestra fertile prevista è una stima che richiede più cicli registrati e resta approssimativa, e la temperatura basale conferma l'ovulazione a posteriori anziché prevederla. Non è un metodo contraccettivo.",
@@ -3311,7 +3468,7 @@
"mean": "Media"
},
"showAllValues": "Mostra tutte le misurazioni",
- "valuesPageTitle": "{metric} — tutte le misurazioni",
+ "valuesPageTitle": "{metric}: tutte le misurazioni",
"valuesPageDescription": "Ogni misurazione dietro questa metrica. Modifica o rimuovi una voce direttamente.",
"diversity": {
"trigger": "Suggerimento di misurazione",
@@ -3351,11 +3508,11 @@
"inBed": "A letto",
"nap": "Pisolino",
"napCount": "{count} pisolini",
- "unavailable": "Ancora nessun dato sulle fasi — le fasi del sonno appariranno qui non appena una fonte collegata le fornirà."
+ "unavailable": "Ancora nessun dato sulle fasi. Le fasi del sonno appariranno qui non appena una fonte collegata le fornirà."
},
"empty": {
"title": "Ancora nessun dato sul sonno",
- "description": "Collega una fonte come Apple Health o un wearable — oppure registra il sonno manualmente — e questa vista si riempirà in automatico.",
+ "description": "Collega una fonte come Apple Health o un wearable, oppure registra il sonno manualmente, e questa vista si riempirà in automatico.",
"action": "Gestisci dispositivi"
},
"debt": {
@@ -3364,21 +3521,21 @@
"caption": "Bilancio in sospeso nelle ultime {nights} notti",
"clearValue": "In pari",
"clearCaption": "Niente in sospeso nelle ultime {nights} notti",
- "learning": "Stiamo ancora misurando il tuo sonno — {counted} notti finora, ne mancano {remaining} per un bilancio affidabile.",
+ "learning": "Stiamo ancora misurando il tuo sonno: {counted} notti finora, ne mancano {remaining} per un bilancio affidabile.",
"computedInfo": "Questa è la stima propria di HealthLog: un bilancio mobile tra fabbisogno e sonno delle ultime notti, che il sonno di recupero riduce. Può differire dal dato di debito di sonno nativo di un dispositivo indossabile, che usa un proprio modello di fabbisogno."
},
"average": {
"title": "Media per notte",
"value": "{hours} h {minutes} min",
"caption": "Sonno medio nelle ultime {nights} notti",
- "learning": "Stiamo ancora misurando il tuo sonno — {counted} notti finora, ne mancano {remaining} per una media affidabile."
+ "learning": "Stiamo ancora misurando il tuo sonno: {counted} notti finora, ne mancano {remaining} per una media affidabile."
},
"chronotype": {
"title": "Cronotipo",
- "learning": "Stiamo ancora imparando il tuo ritmo — {counted} di {total} notti libere. Un cronotipo richiede alcune mattine senza sveglia per avere significato.",
+ "learning": "Stiamo ancora imparando il tuo ritmo: {counted} di {total} notti libere. Un cronotipo richiede alcune mattine senza sveglia per avere significato.",
"advancedToggle": "Avanzate",
"socialJetlagValue": "{hours} h {minutes} min di jet lag sociale",
- "socialJetlagCaption": "La differenza tra gli orari del sonno nei giorni lavorativi e in quelli liberi — come un piccolo cambio di fuso orario settimanale.",
+ "socialJetlagCaption": "La differenza tra gli orari del sonno nei giorni lavorativi e in quelli liberi, come un piccolo cambio di fuso orario settimanale.",
"msfScValue": "Metà del sonno corretta {clock}",
"msfScCaption": "Corretta per il sonno recuperato nel fine settimana (MSFsc).",
"dayTypeNote": "Si presume che i giorni liberi siano i fine settimana finché non verrà aggiunto il tracciamento del tipo di giorno.",
@@ -3567,7 +3724,10 @@
"endInFuture": "Finirebbe nel futuro. Imposta come inizio l’ora in cui è cominciato l’allenamento.",
"distanceInvalid": "Inserisci la distanza come numero in un intervallo realistico.",
"energyInvalid": "Inserisci l’energia attiva come numero fino a 20.000 kcal."
- }
+ },
+ "editTitle": "Modifica allenamento",
+ "editAction": "Modifica",
+ "updated": "Allenamento aggiornato"
}
},
"bloodGlucose": {
@@ -3586,7 +3746,7 @@
"labsLink": "Risultati di laboratorio? Registra una HbA1c o la glicemia a digiuno da un prelievo in Laboratorio.",
"learningTitle": "Stiamo ancora imparando la tua glicemia",
"learningBody": "Per ora sono {count} su {days}. Con un po’ più di storico il quadro si chiarisce: continua a registrare o lascia che lo completi un dispositivo connesso.",
- "learningBodyEmpty": "Appena arrivano alcune misurazioni — registrate da te o sincronizzate da un dispositivo — il tempo nel range, la media e la variabilità prendono forma qui.",
+ "learningBodyEmpty": "Appena arrivano alcune misurazioni (registrate da te o sincronizzate da un dispositivo), il tempo nel range, la media e la variabilità prendono forma qui.",
"readingsCount": "{count} misurazioni",
"readingsCountOne": "1 misurazione",
"daysCount": "{days} giorni",
@@ -3741,7 +3901,7 @@
"navSkinTemperature": "Temp. pelle",
"pulseWaveVelocity": {
"title": "Velocità dell’onda di polso",
- "description": "La tua velocità dell’onda di polso arteriosa nel tempo, in m/s — valori più alti indicano arterie più rigide.",
+ "description": "La tua velocità dell’onda di polso arteriosa nel tempo, in m/s. Valori più alti indicano arterie più rigide.",
"chartTitle": "Velocità dell’onda di polso",
"emptyState": {
"title": "Nessun dato",
@@ -3891,7 +4051,6 @@
"navWalkingSpeed": "Velocità di cammino",
"mood": {
"logLink": "Registra un umore",
- "mentalWellbeingLink": "Fai un check-in sul benessere mentale",
"heatmapTitle": "Calendario dell'umore",
"heatmapNoEntry": "Nessun dato",
"legendGreat": "Ottimo",
@@ -3902,7 +4061,7 @@
"distributionDaysUnit": "giorni",
"weekdayTitle": "Media per giorno della settimana",
"weekdayNoData": "Nessun dato",
- "weekdayBestWorst": "Giorno migliore: {best} · Giorno più difficile: {worst}",
+ "weekdayBestWorst": "Giorno migliore: {best}, giorno più difficile: {worst}",
"tagsTitle": "Tag e fattori scatenanti",
"tagAvgMood": "Umore medio con questo tag",
"correlationsTitle": "Umore e altre metriche",
@@ -3918,7 +4077,7 @@
"strengthModerate": "Moderata",
"strengthWeak": "Debole",
"strengthNone": "Nessuna",
- "source": "{n} giorni appaiati · r = {r}",
+ "source": "{n} giorni appaiati, r = {r}",
"emptyTitle": "Non ci sono ancora abbastanza dati appaiati",
"emptyDescription": "Registra umore e questa metrica negli stessi giorni per vedere una correlazione.",
"pendingDescription": "{n} di {needed} giorni appaiati. La correlazione compare quando i giorni bastano a distinguere un pattern dal caso.",
@@ -3944,7 +4103,7 @@
"tagLift": "I giorni con l'etichetta «{tag}» superano in media il tuo umore generale di {delta}.",
"tagDrop": "I giorni con l'etichetta «{tag}» restano {delta} sotto la tua media.",
"inTarget": "Sei stato nel tuo intervallo di buon umore il {pct} % degli ultimi 30 giorni.",
- "streak": "Hai registrato il tuo umore per {days} giorni di fila — continua così.",
+ "streak": "Hai registrato il tuo umore per {days} giorni di fila. Continua così.",
"timeOfDay": "Ti senti meglio di {bucket}, con una media di {value}."
},
"timeOfDay": {
@@ -3977,7 +4136,7 @@
"crosstab": {
"title": "Tag e le tue metriche di salute",
"description": "Come una metrica di salute cambia nei giorni in cui hai aggiunto un tag rispetto ai giorni senza.",
- "pairLabel": "{tag} · {metric}",
+ "pairLabel": "{metric} nei giorni con {tag}",
"metricActiveEnergy": "Energia attiva",
"metricSleepDuration": "Sonno",
"metricNextDayRecovery": "Recupero il giorno dopo",
@@ -3994,7 +4153,7 @@
"factorCrosstab": {
"title": "I tuoi fattori valutati e i tuoi parametri vitali",
"description": "Come cambia un parametro vitale nei giorni in cui hai valutato basso un fattore rispetto ai giorni in cui lo hai valutato alto.",
- "pairLabel": "{factor} · {metric}",
+ "pairLabel": "{metric} nei giorni con {factor}",
"metricSleepDuration": "Sonno",
"metricSteps": "Passi",
"metricRestingHeartRate": "Frequenza cardiaca a riposo",
@@ -4020,7 +4179,7 @@
"moodDown": "Un {factor} più alto tende a coincidere con un umore peggiore il giorno dopo nei tuoi dati.",
"outcomeUp": "Un umore migliore tende a coincidere con un {factor} più alto il giorno dopo nei tuoi dati.",
"outcomeDown": "Un umore migliore tende a coincidere con un {factor} più basso il giorno dopo nei tuoi dati.",
- "stat": "{n} giorni appaiati · r = {r} · q = {q}",
+ "stat": "{n} giorni appaiati, r = {r}, q = {q}",
"statLabel": "Come è stato calcolato",
"explainerLabel": "Come è stato calcolato"
},
@@ -4054,8 +4213,8 @@
"deviationAbove": "La tua valutazione è {amount} punti sopra questo intervallo.",
"deviationBelow": "La tua valutazione è {amount} punti sotto questo intervallo.",
"weightedTitle": "Che cosa ha pesato di più nel confronto di questa giornata",
- "weightedUp": "{label} — contava verso un valore atteso più alto",
- "weightedDown": "{label} — contava verso un valore atteso più basso",
+ "weightedUp": "{label}: contava verso un valore atteso più alto",
+ "weightedDown": "{label}: contava verso un valore atteso più basso",
"caveat": "È un confronto con le tue giornate passate, non un'affermazione su come una giornata sia andata a finire così.",
"learning": "Fase di apprendimento: {entries} giornate su {threshold} registrate. Il confronto compare quando ce ne sono abbastanza.",
"noPattern": "Nelle {entries} giornate registrate finora non c'è nulla di abbastanza costante per confrontare una giornata. Quando registri anche il contesto della giornata, ne rendi confrontabile una parte maggiore.",
@@ -4064,15 +4223,15 @@
},
"derived": {
"coverage": {
- "summary": "{present} di {required} input · {percent}%",
+ "summary": "{present} di {required} input ({percent}%)",
"ratioLabel": "{present}/{required}",
"historyDays": "Basato su {count} giorni di cronologia",
"missing": "Registra questi per affinare il valore: {list}",
- "summaryLabelled": "{label} · {percent}%"
+ "summaryLabelled": "{label} ({percent}%)"
},
"scoreRing": {
"aria": "Punteggio {score} su 100, {band}",
- "ariaProvisional": "Punteggio non ancora disponibile — dati insufficienti",
+ "ariaProvisional": "Punteggio non ancora disponibile: dati insufficienti",
"provisionalCaption": "Dati ancora insufficienti",
"band": {
"green": "buono",
@@ -4086,7 +4245,7 @@
"inputsLabel": "Input",
"noInputs": "Nessun input supporta ancora questo valore.",
"methodLabel": "Metodo",
- "window": "Dai tuoi ultimi {days} giorni · dati {source}",
+ "window": "Dai tuoi ultimi {days} giorni di dati {source}",
"asOf": "al {date}",
"standardLink": "Standard: {name}"
},
@@ -4096,7 +4255,7 @@
"loadingLabel": "Caricamento dei tuoi parametri vitali",
"loadError": "Al momento non è stato possibile caricare i tuoi parametri vitali.",
"infoLabel": "Come viene calcolato questo dato?",
- "building": "Creazione del tuo intervallo abituale — {count} di {target} giorni",
+ "building": "Creazione del tuo intervallo abituale: {count} di {target} giorni",
"typicalRange": "Abituale per te: {low}–{high}",
"fitnessYounger": "Forma cardio di chi ha ~{years} anni in meno",
"fitnessOlder": "Forma cardio ~{years} anni oltre la tua età",
@@ -4128,9 +4287,8 @@
"anatomy": {
"outOf": "/100",
"contributorsLabel": "Fattori",
- "contributorValue": "{value} · {weight}% di peso",
+ "contributorValue": "{value}, peso {weight}%",
"contributorMissing": "nessun dato",
- "disclaimer": "Indicativo — non è una valutazione clinica.",
"insufficient": "Dati ancora insufficienti per assegnare un punteggio. Continua a registrare e comparirà qui."
},
"composite": {
@@ -4149,15 +4307,14 @@
"READINESS": {
"title": "Prontezza",
"description": "Un indicatore quotidiano di benessere in base a come i tuoi segnali si collocano rispetto alla tua baseline.",
- "method": "Una combinazione 0–100 di deviazione della frequenza cardiaca a riposo, deviazione della HRV (SDNN), qualità del sonno, stabilità della frequenza respiratoria e stabilità dell’umore — ciascuna misurata rispetto alla tua baseline personale. I pesi vengono rinormalizzati sui componenti supportati dai tuoi dati. Un indicatore di benessere, non un punteggio di recupero né clinico.",
+ "method": "Una combinazione 0–100 di deviazione della frequenza cardiaca a riposo, deviazione della HRV (SDNN), qualità del sonno, stabilità della frequenza respiratoria e stabilità dell’umore, ciascuna misurata rispetto alla tua baseline personale. I pesi vengono rinormalizzati sui componenti supportati dai tuoi dati. Un indicatore di benessere, non un punteggio di recupero né clinico.",
"component": {
"rhr": "Frequenza cardiaca a riposo",
"hrv": "HRV (SDNN)",
"sleep": "Sonno",
"respiratory": "Frequenza respiratoria",
"mood": "Stabilità dell’umore"
- },
- "caveat": "Un indicatore di benessere, non un punteggio di recupero né clinico."
+ }
},
"VITALS_BASELINE": {
"method": "Il tuo intervallo tipico personale è la mediana dei tuoi valori giornalieri nel periodo, ± una dispersione robusta (deviazione assoluta mediana, scalata). Segue le TUE letture, non una norma di popolazione: un valore dentro la banda è quindi tipico per te."
@@ -4182,40 +4339,39 @@
},
"RECOVERY_SCORE": {
"method": "Un indicatore notturno di recupero da 0 a 100 che combina VFC, frequenza cardiaca a riposo e sonno rispetto alla tua linea di base. Più alto significa più recuperato. Calcolato una volta a notte e qui solo riletto, mai ricalcolato al volo.",
- "caveat": "Un indicatore di benessere descrittivo, non una misura di recupero clinica o da allenamento.",
"description": "Un indicatore notturno di recupero da VFC, frequenza a riposo e sonno."
},
"STRESS_SCORE": {
"method": "Un indicatore notturno di stress da 0 a 100 dalla forma della tua VFC nel corso della giornata rispetto alla tua linea di base. Più alto significa maggiore carico fisiologico.",
- "caveat": "Un indicatore derivato dalla VFC, NON una misura di stress da EDA / attività elettrodermica: il tuo orologio non ha un sensore EDA. Descrittivo, non clinico.",
"description": "Un indicatore notturno di stress dalla forma della tua VFC nel corso della giornata."
},
"STRAIN_SCORE": {
"method": "Un indicatore notturno di sforzo da 0 a 100 dal tuo carico di attività pesato sulla frequenza cardiaca (un modello di impulso d’allenamento tipo TRIMP). Più alto significa una giornata più impegnativa.",
- "anchorPersonal": "Ancorato al tuo carico recente nei giorni di allenamento — valutato rispetto al tuo sforzo abituale, così che una giornata davvero dura per te risulti alta.",
+ "anchorPersonal": "Ancorato al tuo carico recente nei giorni di allenamento: valutato rispetto al tuo sforzo abituale, così che una giornata davvero dura per te risulti alta.",
"anchorPopulation": "Valutato rispetto a un riferimento generale mentre costruisci lo storico di allenamento; quando ne hai abbastanza passa al tuo sforzo abituale.",
- "caveat": "Un indicatore descrittivo di carico di attività, non una misura di carico clinica o da allenamento.",
- "description": "Un indicatore notturno di carico di attività dal tuo sforzo pesato sulla frequenza cardiaca, relativo al tuo sforzo abituale."
+ "description": "Un indicatore notturno di carico di attività dal tuo sforzo pesato sulla frequenza cardiaca, relativo al tuo sforzo abituale.",
+ "deviceCaption": "Sforzo giornaliero del tuo dispositivo: {value} su {max}",
+ "deviceSource": "Finché HealthLog non ha calcolato un proprio valore durante la notte, la scheda mostra lo sforzo giornaliero del tuo dispositivo, riportato su 0–100."
},
"WRIST_TEMPERATURE_BASELINE": {
- "method": "La tua tipica temperatura notturna al polso è la mediana delle tue letture notturne nel periodo, ± una dispersione robusta (deviazione assoluta mediana, scalata). Segue le TUE letture — la notte scorsa è mostrata come scostamento dalla tua baseline, non da una norma di popolazione.",
- "caveat": "Solo una banda di scostamento personale — descrittiva, non un segnale di malattia, febbre o fase del ciclo. Sei tu a interpretarla."
+ "method": "La tua tipica temperatura notturna al polso è la mediana delle tue letture notturne nel periodo, ± una dispersione robusta (deviazione assoluta mediana, scalata). Segue le TUE letture: la notte scorsa è mostrata come scostamento dalla tua baseline, non da una norma di popolazione.",
+ "caveat": "Solo una banda di scostamento personale, descrittiva, non un segnale di malattia, febbre o fase del ciclo. Sei tu a interpretarla."
},
"STAIR_ASCENT_SPEED_BASELINE": {
- "method": "Il tuo passo tipico in salita sulle scale è la mediana dei tuoi valori giornalieri nel periodo, ± una dispersione robusta (deviazione assoluta mediana, scalata). La banda è costruita sui tuoi dati — non esiste una soglia di popolazione per la velocità sulle scale, perché il passo dipende dalla geometria dei gradini e dalla lunghezza delle gambe.",
- "caveat": "Una banda personale di tendenza della mobilità — descrittiva, non una valutazione di fragilità o di rischio di caduta."
+ "method": "Il tuo passo tipico in salita sulle scale è la mediana dei tuoi valori giornalieri nel periodo, ± una dispersione robusta (deviazione assoluta mediana, scalata). La banda è costruita sui tuoi dati: non esiste una soglia di popolazione per la velocità sulle scale, perché il passo dipende dalla geometria dei gradini e dalla lunghezza delle gambe.",
+ "caveat": "Una banda personale di tendenza della mobilità, descrittiva, non una valutazione di fragilità o di rischio di caduta."
},
"STAIR_DESCENT_SPEED_BASELINE": {
- "method": "Il tuo passo tipico in discesa sulle scale è la mediana dei tuoi valori giornalieri nel periodo, ± una dispersione robusta (deviazione assoluta mediana, scalata). La banda è costruita sui tuoi dati — non esiste una soglia di popolazione per la velocità sulle scale, perché il passo dipende dalla geometria dei gradini e dalla lunghezza delle gambe.",
- "caveat": "Una banda personale di tendenza della mobilità — descrittiva, non una valutazione di fragilità o di rischio di caduta."
+ "method": "Il tuo passo tipico in discesa sulle scale è la mediana dei tuoi valori giornalieri nel periodo, ± una dispersione robusta (deviazione assoluta mediana, scalata). La banda è costruita sui tuoi dati: non esiste una soglia di popolazione per la velocità sulle scale, perché il passo dipende dalla geometria dei gradini e dalla lunghezza delle gambe.",
+ "caveat": "Una banda personale di tendenza della mobilità, descrittiva, non una valutazione di fragilità o di rischio di caduta."
},
"SIX_MINUTE_WALK_BAND": {
- "method": "La distanza di cammino di sei minuti STIMATA dal tuo dispositivo, collocata rispetto al riferimento di Enright e Sherrill per la tua età, altezza, peso e sesso, come percentuale del previsto. La distanza è la stima del dispositivo — mai ricalcolata qui. Senza età, altezza, peso e sesso la percentuale è nascosta e vengono mostrate solo distanza e tendenza.",
- "caveat": "Una stima riformulata rispetto a un riferimento pubblicato — un segnale di capacità funzionale, non un test del cammino di sei minuti clinico né una diagnosi. Il riferimento è stato stabilito per le età 40–80; al di fuori di questo intervallo il valore previsto è un’estrapolazione."
+ "method": "La distanza di cammino di sei minuti STIMATA dal tuo dispositivo, collocata rispetto al riferimento di Enright e Sherrill per la tua età, altezza, peso e sesso, come percentuale del previsto. La distanza è la stima del dispositivo, mai ricalcolata qui. Senza età, altezza, peso e sesso la percentuale è nascosta e vengono mostrate solo distanza e tendenza.",
+ "caveat": "Una stima riformulata rispetto a un riferimento pubblicato: un segnale di capacità funzionale, non un test del cammino di sei minuti clinico né una diagnosi. Il riferimento è stato stabilito per le età 40–80; al di fuori di questo intervallo il valore previsto è un’estrapolazione."
},
"TRAJECTORY": {
"method": "Una retta ai minimi quadrati adattata ai tuoi valori giornalieri recenti e proiettata in avanti su un breve orizzonte, con l’intervallo di previsione classico che si allarga man mano che si estende. Calcolata solo quando la tendenza recente è abbastanza marcata e la tua cronologia è abbastanza lunga e aggiornata; altrimenti non viene tracciata alcuna linea.",
- "caveat": "Una proiezione della tua tendenza recente — non una previsione, un obiettivo o una prognosi clinica. Vale solo se lo schema recente continua; la banda ombreggiata mostra quanto rapidamente quella certezza svanisce."
+ "caveat": "Una proiezione della tua tendenza recente, non una previsione, un obiettivo o una prognosi clinica. Vale solo se lo schema recente continua; la banda ombreggiata mostra quanto rapidamente quella certezza svanisce."
},
"SAME_TIME_BASELINE": {
"method": "Il tuo totale progressivo di oggi confrontato con la mediana della stessa ora locale nelle ultime quattro settimane, con una banda robusta mediana ± MAD attorno. Contano solo i giorni con abbastanza dettaglio registrato, e nessun confronto viene mostrato finché non ce ne sono almeno quattordici.",
@@ -4230,7 +4386,9 @@
"recovery": "Punteggio di recupero",
"recoveryCrossLink": "Vedi i dati di recupero e sforzo del tuo wearable",
"stress": "Stress",
- "strain": "Sforzo"
+ "strain": "Sforzo",
+ "historyTitle": "Andamento",
+ "historyWindow": "Ultimi {days} giorni"
},
"coincident": {
"label": "Più parametri fuori dalla base",
@@ -4240,24 +4398,24 @@
"allClear": "Oggi tutti i tuoi parametri vitali sono nel loro intervallo personale.",
"allClearMeta": "Controllati {count} dei tuoi parametri rispetto al tuo intervallo abituale.",
"watch": "Oggi un parametro è fuori dal suo intervallo abituale.",
- "watchVital": "Fuori intervallo: {vital}. Spesso è un episodio isolato — tienilo d’occhio nei prossimi giorni.",
+ "watchVital": "Fuori intervallo: {vital}. Spesso è un episodio isolato: tienilo d’occhio nei prossimi giorni.",
"firedHeadline": "Oggi {count} dei tuoi parametri vitali sono fuori dal loro intervallo personale.",
- "factors": "Possibili fattori — mai una causa: un allenamento intenso, poco sonno, alcol, altitudine, stress o una malattia. È un avviso basato sui tuoi parametri, non una diagnosi.",
- "building": "Stiamo costruendo i tuoi riferimenti personali — registra ancora qualche giorno e comparirà.",
+ "factors": "Possibili fattori, mai una causa: un allenamento intenso, poco sonno, alcol, altitudine, stress o una malattia. È un avviso basato sui tuoi parametri, non una diagnosi.",
+ "building": "Stiamo costruendo i tuoi riferimenti personali. Registra ancora qualche giorno e comparirà.",
"illnessExplained": "Questi scostamenti riflettono probabilmente la condizione che hai registrato."
},
"trajectory": {
"cardTitle": "Proiezione a breve termine",
- "headline": "Se questo schema continua — i prossimi {days} giorni",
+ "headline": "Se questo schema continua: i prossimi {days} giorni",
"range": "Proiettato intorno a {low}–{high} {unit} tra {days} giorni.",
"caveat": "Una proiezione della tua tendenza recente, non una previsione. La banda ombreggiata si allarga per mostrare quanto rapidamente cresce l’incertezza.",
- "insufficient": "Non c’è ancora una tendenza abbastanza chiara da proiettare — continua a registrare e comparirà una proiezione quando lo schema recente sarà abbastanza marcato.",
+ "insufficient": "Non c’è ancora una tendenza abbastanza chiara da proiettare. Continua a registrare e comparirà una proiezione quando lo schema recente sarà abbastanza marcato.",
"daysNeeded": "Servono ancora circa {count} giorni di registrazione prima che possa comparire una proiezione."
},
"sameTime": {
"sectionTitle": "Oggi finora",
"sectionSubtitle": "I tuoi totali progressivi rispetto a ciò che per te è abituale a quest'ora",
- "learning": "Sto imparando la tua giornata tipo — {count} di {target} giorni",
+ "learning": "Sto imparando la tua giornata tipo: {count} di {target} giorni",
"framing": "Abituale entro le {time}: {typical} {unit}",
"comparisonLabel": "La tua giornata tipo"
}
@@ -4344,7 +4502,7 @@
"navBreathingDisturbances": "Respirazione",
"rhythmEvents": {
"sectionTitle": "Notifiche sulla salute del dispositivo",
- "sectionIntro": "Notifiche che il tuo orologio ha inviato autonomamente. HealthLog si limita a mostrarle — non le rileva, conferma né interpreta.",
+ "sectionIntro": "Notifiche che il tuo orologio ha inviato autonomamente. HealthLog si limita a mostrarle: non le rileva, conferma né interpreta.",
"loadError": "Impossibile caricare le notifiche del dispositivo.",
"disclaimer": "Questi sono avvisi generati dal tuo dispositivo con il proprio rilevamento integrato. Sono mostrati qui solo a scopo informativo. Non è una valutazione medica da parte di HealthLog, e HealthLog non diagnostica alcuna condizione. Se una notifica ti preoccupa, parla con il tuo medico.",
"event": {
@@ -4389,9 +4547,9 @@
"detail": "Le pagine di dettaglio nascoste vengono rimosse anche dalla navigazione in alto."
},
"customize": "Personalizza gli Approfondimenti",
- "regenerateRateLimited": "Limite di aggiornamento raggiunto — riprova più tardi.",
- "regenerateUnavailable": "Nessuna nuova analisi ricevuta — riprova più tardi.",
- "regenerateError": "Aggiornamento ancora in corso — riprova tra un momento.",
+ "regenerateRateLimited": "Limite di aggiornamento raggiunto. Riprova più tardi.",
+ "regenerateUnavailable": "Nessuna nuova analisi ricevuta. Riprova più tardi.",
+ "regenerateError": "Aggiornamento ancora in corso. Riprova tra un momento.",
"regenerateSettling": "La valutazione è ancora in preparazione",
"regenerateSettleFailed": "Non è stato possibile completare l’aggiornamento. Riprova più tardi.",
"deviceScore": {
@@ -4413,9 +4571,9 @@
"emptyTitle": "Ancora nessun dato di recupero",
"empty": "Ancora nessun dato di recupero. Collega un account WHOOP, Polar o Oura e questi punteggi si popoleranno appena il dispositivo si sincronizza.",
"block": {
- "ansChargeExplainer": "Quanto è carico il tuo sistema nervoso autonomo per la giornata — una lettura di ricarica ricavata dai segnali notturni di cuore e respiro.",
+ "ansChargeExplainer": "Quanto è carico il tuo sistema nervoso autonomo per la giornata: una lettura di ricarica ricavata dai segnali notturni di cuore e respiro.",
"dayStrainExplainer": "Il carico cardiovascolare della giornata sulla scala di sforzo del tuo dispositivo. Leggilo in rapporto al tuo recupero, non come obiettivo a sé.",
- "workoutStrainExplainer": "Quanto è stato impegnativo un singolo allenamento sulla stessa scala di sforzo — il costo cardiovascolare di quella sessione.",
+ "workoutStrainExplainer": "Quanto è stato impegnativo un singolo allenamento sulla stessa scala di sforzo: il costo cardiovascolare di quella sessione.",
"cardioLoadExplainer": "Una stima del carico di allenamento che traccia il carico cardiovascolare accumulato. Confrontala con i tuoi giorni recenti.",
"averageHeartRateExplainer": "La tua frequenza cardiaca media nell'arco dell'intera giornata o ciclo, come riportata dal tuo wearable.",
"maxHeartRateExplainer": "La tua frequenza cardiaca massima nell'arco dell'intera giornata o ciclo, come riportata dal tuo wearable.",
@@ -4428,7 +4586,7 @@
},
"resilience": {
"explainer": "Quanto bene il tuo corpo bilancia il carico quotidiano con il recupero, letto dai tuoi segnali notturni degli ultimi giorni.",
- "learning": "Stiamo ancora imparando la tua resilienza — qualche giorno in più stabilizzerà il livello.",
+ "learning": "Stiamo ancora imparando la tua resilienza. Qualche giorno in più stabilizzerà il livello.",
"band": {
"limited": "Limitata",
"adequate": "Adeguata",
@@ -4459,6 +4617,7 @@
"shiftUp": "{vital} si è spostato verso l'alto rispetto alla tua norma recente.",
"shiftDown": "{vital} si è spostato verso il basso rispetto alla tua norma recente.",
"rangeContext": "Ora {value}, di solito {low}–{high}.",
+ "rangeContextTimeOfDay": "Ora {value}, di solito {low}–{high} a quest'ora del giorno.",
"shiftContext": "Circa {before} prima, {after} da allora."
},
"breathingScreening": {
@@ -4495,7 +4654,7 @@
"description": "Registra una misura con un dinamometro manuale (annota la migliore di alcune strette) e l'andamento apparirà qui.",
"cta": "Registra la forza di presa"
},
- "coachPrefill": "Non ho ancora registrato la forza di presa — cosa dice sulla salute dei miei muscoli e come la misuro e la miglioro?"
+ "coachPrefill": "Non ho ancora registrato la forza di presa. Cosa dice sulla salute dei miei muscoli e come la misuro e la miglioro?"
},
"pain": {
"title": "Dolore (0–10)",
@@ -4506,7 +4665,7 @@
"description": "Registra come senti il dolore in questo momento sulla scala 0–10 e l'andamento apparirà qui.",
"cta": "Registra un punteggio del dolore"
},
- "coachPrefill": "Non ho ancora registrato punteggi del dolore — come dovrei monitorare il dolore sulla scala 0–10 e cosa conta come variazione significativa?"
+ "coachPrefill": "Non ho ancora registrato punteggi del dolore. Come dovrei monitorare il dolore sulla scala 0–10 e cosa conta come variazione significativa?"
},
"waist": {
"title": "Circonferenza vita",
@@ -4517,7 +4676,7 @@
"description": "Misura la vita nel punto medio tra l'ultima costola e l'osso dell'anca, a fine espirazione normale, e registrala qui.",
"cta": "Registra la circonferenza vita"
},
- "coachPrefill": "Non ho ancora registrato misure della vita — come misuro la vita correttamente e cosa significa il numero per la mia salute?"
+ "coachPrefill": "Non ho ancora registrato misure della vita. Come misuro la vita correttamente e cosa significa il numero per la mia salute?"
},
"waistToHeight": {
"title": "Rapporto vita-altezza",
@@ -4528,7 +4687,7 @@
"description": "Registra un rapporto vita-altezza e l'andamento apparirà qui. Regola pratica: tieni la vita sotto la metà della tua altezza.",
"cta": "Registra il rapporto vita-altezza"
},
- "coachPrefill": "Non ho ancora registrato un rapporto vita-altezza — perché è un numero utile e come lo tengo sotto 0,5?"
+ "coachPrefill": "Non ho ancora registrato un rapporto vita-altezza. Perché è un numero utile e come lo tengo sotto 0,5?"
},
"ecg": {
"sectionTitle": "Registrazioni ECG",
@@ -4569,9 +4728,9 @@
"emptyCta": "Collega un dispositivo"
},
"intradayPulse": {
- "title": "L'andamento della tua giornata",
- "caption": "La tua frequenza cardiaca durante il giorno, mediata in intervalli di 10 minuti.",
- "hourlyNote": "Media oraria per questo giorno — i giorni recenti mostrano il dettaglio completo a 10 minuti.",
+ "title": "Battito nel corso della giornata",
+ "caption": "Media su intervalli di 10 minuti.",
+ "hourlyNote": "Media oraria per questo giorno. I giorni recenti mostrano il dettaglio completo a 10 minuti.",
"empty": "Non ci sono ancora abbastanza misurazioni per questo giorno per mostrarne l'andamento.",
"coverageOne": "Basato su 1 misurazione in {hours}h.",
"coverageMany": "Basato su {count} misurazioni in {hours}h.",
@@ -4580,10 +4739,10 @@
"previousDay": "Giorno precedente",
"nextDay": "Giorno successivo",
"tension": {
- "morning": "Un tratto elevato a riposo stamattina — forse un po' di tensione.",
- "afternoon": "Un tratto elevato a riposo questo pomeriggio — forse un po' di tensione.",
- "evening": "Un tratto elevato a riposo stasera — forse un po' di tensione.",
- "night": "Un tratto elevato a riposo durante la notte — forse un po' di tensione."
+ "morning": "Un tratto elevato a riposo stamattina, forse un po' di tensione.",
+ "afternoon": "Un tratto elevato a riposo questo pomeriggio, forse un po' di tensione.",
+ "evening": "Un tratto elevato a riposo stasera, forse un po' di tensione.",
+ "night": "Un tratto elevato a riposo durante la notte, forse un po' di tensione."
}
},
"navEcg": "ECG",
@@ -4594,6 +4753,19 @@
"settingsBody": "HealthLog funziona completamente senza IA. Collega un fornitore qui sotto se vuoi un riepilogo giornaliero, brevi note sulle tue misurazioni e il Coach.",
"action": "Configura IA",
"dismiss": "Non ora"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Non ho ancora inserito la mia altezza: perché conta il BMI e cosa dovrei sapere prima di impostarla?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Non ho ancora aggiunto farmaci: cosa dovrei sapere prima di iniziare a monitorare qui l’aderenza?"
+ },
+ "scoreHistory": {
+ "healthTitle": "Andamento del punteggio di salute",
+ "healthDescription": "Il tuo punteggio di salute giorno per giorno, come veniva mostrato ogni giorno.",
+ "emptyDescription": "In questo periodo non è ancora stato registrato alcun punteggio.",
+ "usualRange": "In ombra: il tuo intervallo abituale, da {lo} a {hi}.",
+ "seam": "La linea si interrompe dove è cambiato ciò che conta per il punteggio."
}
},
"thresholds": {
@@ -4623,14 +4795,15 @@
"metricBodyFat": "Massa grassa",
"metricSleep": "Durata del sonno",
"metricSteps": "Passi/giorno",
- "metricGlucoseFasting": "Glicemia — a digiuno",
- "metricGlucosePostprandial": "Glicemia — postprandiale",
- "metricGlucoseRandom": "Glicemia — qualsiasi",
- "metricGlucoseBedtime": "Glicemia — prima di coricarsi",
+ "metricGlucoseFasting": "Glicemia (a digiuno)",
+ "metricGlucosePostprandial": "Glicemia (postprandiale)",
+ "metricGlucoseRandom": "Glicemia (qualsiasi)",
+ "metricGlucoseBedtime": "Glicemia (prima di coricarsi)",
"metricBodyWater": "Acqua corporea",
"metricBoneMass": "Massa ossea",
"metricOxygenSaturation": "Saturazione di ossigeno (SpO₂)",
- "unsetExplanation": "Nessun valore personale — è attivo il predefinito calcolato."
+ "unsetExplanation": "Nessun valore personale: è attivo il predefinito calcolato.",
+ "cardTitle": "Intervalli per metrica"
},
"onboarding": {
"errorGeneric": "Qualcosa è andato storto durante il completamento della configurazione. Riprova.",
@@ -4647,7 +4820,7 @@
},
"quickAdd": {
"title": "Registrato in pochi secondi",
- "body": "Con « + Aggiungi » registri qualsiasi misurazione in pochi secondi — peso, pressione, polso, glicemia. Forza nascosta: retrodatare i valori dimenticati. Tutto ciò che inserisci qui alimenta tendenze, punteggio salute e analisi IA."
+ "body": "Con « + Aggiungi » registri qualsiasi misurazione in pochi secondi: peso, pressione, polso, glicemia. Forza nascosta: retrodatare i valori dimenticati. Tutto ciò che inserisci qui alimenta tendenze, punteggio salute e analisi IA."
},
"measurements": {
"title": "Misurazioni e intervalli obiettivo",
@@ -4659,7 +4832,7 @@
},
"labs": {
"title": "Valori di laboratorio",
- "body": "Laboratorio raccoglie i biomarcatori con intervalli di riferimento — emocromo, colesterolo, HbA1c. Forza: importazione ed esportazione FHIR. I valori fuori intervallo riemergono come prove nelle analisi e nel referto medico."
+ "body": "Laboratorio raccoglie i biomarcatori con intervalli di riferimento: emocromo, colesterolo, HbA1c. Forza: importazione ed esportazione FHIR. I valori fuori intervallo riemergono come prove nelle analisi e nel referto medico."
},
"illness": {
"title": "Diario delle malattie",
@@ -4675,11 +4848,11 @@
},
"mood": {
"title": "Umore",
- "body": "Umore cattura come ti senti e dei tag in due secondi. Forza: i tuoi tag personalizzati. Gli andamenti dell’umore si correlano con sonno, farmaci e ciclo — il Coach coglie gli schemi."
+ "body": "Umore cattura come ti senti e dei tag in due secondi. Forza: i tuoi tag personalizzati. Gli andamenti dell’umore si correlano con sonno, farmaci e ciclo, e il Coach coglie gli schemi."
},
"insights": {
"title": "Analisi",
- "body": "Analisi è il cuore: analisi dinamica e basata su prove invece di consigli generici — ogni affermazione punta ai tuoi dati reali. Nascosto: il layout è personalizzabile. Analisi raccoglie tutto ciò che forniscono i moduli sopra."
+ "body": "Analisi è il cuore: analisi dinamica e basata su prove invece di consigli generici, dove ogni affermazione punta ai tuoi dati reali. Nascosto: il layout è personalizzabile. Analisi raccoglie tutto ciò che forniscono i moduli sopra."
},
"coach": {
"title": "Coach",
@@ -4695,7 +4868,7 @@
},
"achievements": {
"title": "Traguardi",
- "body": "I traguardi premiano la costanza — serie e tappe. Un piccolo ciclo di motivazione che rende più facile non mollare."
+ "body": "I traguardi premiano la costanza: serie e tappe. Un piccolo ciclo di motivazione che rende più facile non mollare."
},
"wrapUp": {
"title": "Questa è la visita guidata",
@@ -4730,7 +4903,7 @@
"title": "Tutto pronto",
"body": "Benvenuto a bordo. La tua dashboard è pronta.",
"returnCta": "Apri la dashboard",
- "learning": "Alcune analisi — glicemia, ritmo del sonno, tendenze della pressione, ciclo — si affinano nella prima settimana o due man mano che i tuoi dati si accumulano. Le caselle vuote ora indicano solo che stiamo ancora imparando il tuo riferimento.",
+ "learning": "Alcune analisi (glicemia, ritmo del sonno, tendenze della pressione, ciclo) si affinano nella prima settimana o due man mano che i tuoi dati si accumulano. Le caselle vuote ora indicano solo che stiamo ancora imparando il tuo riferimento.",
"connectCta": "Collega un dispositivo",
"logCta": "Registra la tua prima misurazione",
"importCta": "Importa la cronologia esistente",
@@ -4762,18 +4935,18 @@
"sampleTitle": "Briefing quotidiano",
"sampleTag": "Esempio",
"sampleAriaLabel": "Briefing quotidiano di esempio",
- "sampleBody": "Peso in calo di 1,2 kg in due settimane. Battito a riposo stabile a 58 bpm. Pressione nel range in 6 giorni su 7 — l'unico valore alto è arrivato dopo una cena tardiva e salata.",
- "sampleCaption": "Solo un esempio — non i tuoi dati. Ecco come si legge un briefing quando gli insight sono attivi.",
+ "sampleBody": "Peso in calo di 1,2 kg in due settimane. Battito a riposo stabile a 58 bpm. Pressione nel range in 6 giorni su 7: l'unico valore alto è arrivato dopo una cena tardiva e salata.",
+ "sampleCaption": "Solo un esempio, non i tuoi dati. Ecco come si legge un briefing quando gli insight sono attivi.",
"ladderTitle": "Tre modi per attivarlo",
- "ladderLocalTitle": "Modello locale — le richieste all’IA restano nella tua rete",
- "ladderLocalBody": "Punta HealthLog su Ollama o LM Studio sulla tua macchina. Gratis e del tutto privato — la scelta tranquilla di default.",
+ "ladderLocalTitle": "Modello locale: le richieste all’IA restano nella tua rete",
+ "ladderLocalBody": "Punta HealthLog su Ollama o LM Studio sulla tua macchina. Gratis e del tutto privato: la scelta tranquilla di default.",
"ladderByokTitle": "Usa la tua chiave",
"ladderByokBody": "Usa la tua chiave OpenAI o Anthropic. Gira sul tuo account e sulla tua fatturazione, e il piano API tiene i tuoi dati fuori dall'addestramento in modo predefinito.",
"ladderOauthTitle": "Accedi con un abbonamento",
- "ladderOauthBody": "Collega un account ChatGPT — nessun costo per token, ma i termini del piano consumer possono usare i contenuti per l'addestramento, quindi è una scelta deliberata, mai l'impostazione predefinita per i tuoi dati sanitari.",
- "keylessLine": "HealthLog è pienamente utile anche senza IA. Le tue schede, i grafici, i trend e le esportazioni funzionano tutti — l'IA aggiunge solo la lettura in linguaggio chiaro.",
+ "ladderOauthBody": "Collega un account ChatGPT. Nessun costo per token, ma i termini del piano consumer possono usare i contenuti per l'addestramento, quindi è una scelta deliberata, mai l'impostazione predefinita per i tuoi dati sanitari.",
+ "keylessLine": "HealthLog è pienamente utile anche senza IA. Le tue schede, i grafici, i trend e le esportazioni funzionano tutti; l'IA aggiunge solo la lettura in linguaggio chiaro.",
"setupCta": "Configuralo (circa un minuto)",
- "sharedKeyNote": "Questa istanza fornisce già un provider IA condiviso, quindi gli insight funzionano per te fin da subito — nessuna configurazione necessaria.",
+ "sharedKeyNote": "Questa istanza fornisce già un provider IA condiviso, quindi gli insight funzionano per te fin da subito, senza alcuna configurazione.",
"offer": {
"title": "Questa istanza può già scrivere il tuo riepilogo in parole semplici.",
"body": "Il gestore mette a disposizione un fornitore che ha funzionato nelle ultime 24 ore. Con un tocco attivi le parole semplici; i tuoi dati andranno allora a quell’unico fornitore e a nessun altro.",
@@ -4990,7 +5163,7 @@
"profileDescription": "Altezza, data di nascita e sesso sbloccano obiettivi personalizzati.",
"profileCta": "Apri profilo",
"measurementTitle": "Registra la prima misurazione",
- "measurementDescription": "Una misurazione attiva il tile — il trend si disegna dalla 5ª misurazione.",
+ "measurementDescription": "Una misurazione attiva il tile; il trend si disegna dalla 5ª misurazione.",
"measurementCta": "Aggiungi misurazione",
"medicationTitle": "Aggiungi un farmaco",
"medicationDescription": "Monitora l’aderenza e ricevi promemoria proprio quando ti servono.",
@@ -4999,7 +5172,7 @@
"dataSourceDescription": "Sincronizza peso, pressione, sonno e altro da Withings, WHOOP, Oura, Polar, Nightscout, Fitbit o Apple Health.",
"dataSourceCta": "Collega",
"notificationsTitle": "Configura le notifiche",
- "notificationsDescription": "Scegli come HealthLog ti ricorda — Telegram, ntfy o Web Push.",
+ "notificationsDescription": "Scegli come HealthLog ti ricorda: Telegram, ntfy o Web Push.",
"notificationsCta": "Apri notifiche",
"insightsTitle": "Attiva gli insight (facoltativo)",
"insightsDescription": "Aggiungi un modello locale o la tua chiave IA per sbloccare il briefing quotidiano e Coach.",
@@ -5088,11 +5261,6 @@
"brief": "Breve",
"default": "Predefinito",
"detailed": "Dettagliato",
- "defaultWindow": "Periodo predefinito",
- "last7days": "Ultimi 7 giorni",
- "last30days": "Ultimi 30 giorni",
- "last90days": "Ultimi 90 giorni",
- "allTime": "Tutto il periodo",
"excludedMetrics": "Metriche escluse",
"dataClusters": "Gruppi di dati",
"customClusters": "Usa una selezione personalizzata di gruppi",
@@ -5152,12 +5320,10 @@
"sections": {
"account": {
"title": "Account",
- "description": "Profilo, password, passkey e il tour di benvenuto.",
"subtitle": "Il tuo profilo, la tua identità e i dati di accesso."
},
"integrations": {
"title": "Integrazioni",
- "description": "Servizi collegati, canali di consegna e priorità delle fonti in un unico posto.",
"subtitle": "Collega e gestisci le tue fonti di dati e i dispositivi.",
"connectionsHeading": "Connessioni",
"channelsHeading": "Canali di recapito",
@@ -5166,16 +5332,15 @@
},
"notifications": {
"title": "Notifiche",
- "description": "Tutti i promemoria in un unico posto — cosa e quando, e dove vengono recapitati.",
"subtitle": "Scegli quali promemoria ricevere e dove vengono recapitati.",
"remindersHeading": "Promemoria",
"matrixLink": "Notifiche per canale",
- "channelsLink": "Canali di recapito"
+ "channelsLink": "Canali di recapito",
+ "displayHeading": "Sul telefono"
},
"layout": {
- "title": "Aspetto",
- "description": "Scegli l'aspetto e la disposizione di dashboard, approfondimenti e dei tuoi moduli di monitoraggio.",
- "backToHub": "Torna ad Aspetto",
+ "title": "Layout",
+ "backToHub": "Torna a Layout",
"dashboard": {
"title": "Cruscotto",
"description": "Layout e ordine dei riquadri."
@@ -5184,7 +5349,7 @@
"title": "Approfondimenti",
"description": "Ordine della panoramica e delle schede di navigazione."
},
- "subtitle": "Personalizza l'aspetto di dashboard, approfondimenti e moduli di monitoraggio.",
+ "subtitle": "Scegli cosa mostrano dashboard, approfondimenti e moduli di monitoraggio, e in quale ordine.",
"medications": {
"title": "Farmaci",
"description": "Vista elenco e ordine manuale."
@@ -5204,6 +5369,10 @@
"mood": {
"title": "Tag dell’umore",
"description": "Gruppi, tag e tag archiviati del selettore dell’umore."
+ },
+ "documents": {
+ "title": "Documenti",
+ "description": "Visualizzazione e disposizione dei mesi."
}
},
"dashboard": {
@@ -5211,15 +5380,13 @@
},
"thresholds": {
"title": "Obiettivi",
- "description": "Per metrica: il tuo intervallo target per valori sani.",
"subtitle": "Imposta i tuoi intervalli target personali per metrica."
},
"sources": {
"title": "Priorità delle fonti",
- "description": "Scegli quale fonte prevale quando più di una registra la stessa metrica lo stesso giorno.",
"integrationsHint": "Collega o scollega le fonti dati nella pagina Integrazioni.",
"integrationsHintLink": "Apri le integrazioni",
- "cardTitle": "Priorità delle fonti per metrica",
+ "cardTitle": "Ordine per metrica",
"help": "Se più fonti forniscono lo stesso valore nello stesso giorno, la fonte in cima conta come canonica.",
"resetDefaults": "Ripristina i valori predefiniti",
"resetDefaultsTitle": "Reimpostare l’ordine delle fonti?",
@@ -5232,7 +5399,7 @@
"resetSuccess": "Priorità delle fonti ripristinata ai valori predefiniti",
"perMetricToggle": "Personalizzazioni per metrica ({count})",
"perMetricHelp": "Metriche che hai personalizzato rispetto all’ordine predefinito. Usa questo elenco per riportare una singola metrica al suo valore predefinito senza toccare le altre.",
- "perMetricEmpty": "Nessuna personalizzazione per metrica — ogni metrica usa l’ordine globale qui sopra.",
+ "perMetricEmpty": "Nessuna personalizzazione per metrica: ogni metrica usa l’ordine globale qui sopra.",
"resetMetric": "Reimposta",
"deviceTypeToggle": "Priorità per tipo di dispositivo ({count})",
"deviceTypeHelp": "All’interno di una fonte, quale dispositivo prevale. Quando un Apple Watch e un iPhone registrano entrambi i passi, il tipo di dispositivo più in alto conta come lettura canonica.",
@@ -5257,7 +5424,8 @@
"respiratoryRate": "Frequenza respiratoria",
"recovery": "Recupero",
"stress": "Stress",
- "sleepDebt": "Debito di sonno"
+ "sleepDebt": "Debito di sonno",
+ "hrvRmssd": "HRV (RMSSD)"
},
"sourceLabels": {
"WITHINGS": "Withings",
@@ -5274,7 +5442,8 @@
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava",
- "EXTERNAL": "Dispositivo esterno"
+ "EXTERNAL": "Dispositivo esterno",
+ "HEALTH_CONNECT": "Health Connect"
},
"deviceLabels": {
"watch": "Orologio",
@@ -5296,26 +5465,23 @@
"moduleHintLink": "Gestisci moduli",
"detail": "Sola lettura: i valori appartengono all'app di origine."
},
- "helpDetail": "Le altre restano memorizzate per la traccia di controllo."
+ "helpDetail": "Le altre restano memorizzate per la traccia di controllo. Sono elencate solo le fonti collegate o con dati.",
+ "noneInUse": "Ancora nessuna fonte da ordinare. Collegane una in Integrazioni o registra un valore."
},
"ai": {
"title": "Provider IA",
- "description": "Provider, modello e chiave API per gli approfondimenti, oltre al tuo contesto «su di me».",
"subtitle": "Scegli il tuo provider IA e usa la tua chiave."
},
"api": {
- "title": "API & Tokens",
- "description": "Token Bearer per i tuoi script e le tue app — registra misurazioni o assunzioni di farmaci da ovunque.",
+ "title": "API e token",
"subtitle": "Gestisci gli endpoint API e i token di accesso."
},
"advanced": {
"title": "Avanzate",
- "description": "La via d'uscita: cancella per sempre tutti i dati di salute o chiudi l'account.",
"subtitle": "Ripristino dati ed eliminazione account."
},
"export": {
"title": "Esporta e importa",
- "description": "Scarica i tuoi dati come CSV o backup JSON completo, oppure importa da Apple Salute, JSON o CSV. Il referto medico ora si trova in Cartella sanitaria.",
"otherOptionsHeading": "Altre opzioni di esportazione",
"downloadFailed": "Impossibile scaricare l'esportazione. Riprova.",
"actions": {
@@ -5342,7 +5508,7 @@
"fullBackup": {
"title": "Backup completo",
"description": "L'intera cartella in un unico file JSON.",
- "scopeNote": "I metadati dei documenti e il riepilogo IA, se presente, sono inclusi in chiaro; i file originali dei documenti e i dati di percorso GPS/campionamento degli allenamenti non lo sono — scaricali separatamente dall'archivio documenti o dall'esportazione degli allenamenti.",
+ "scopeNote": "I metadati dei documenti e il riepilogo IA, se presente, sono inclusi in chiaro; i file originali dei documenti e i dati di percorso GPS/campionamento degli allenamenti non lo sono. Scaricali separatamente dall'archivio documenti o dall'esportazione degli allenamenti.",
"encryptToggle": "Cifra con una passphrase",
"passphrasePlaceholder": "Passphrase (min. 12 caratteri)",
"passphraseHelp": "Deriva la chiave dell'archivio con Argon2id e lo sigilla con AES-256-GCM. Salva il file con estensione .hlx.",
@@ -5373,6 +5539,9 @@
"rowsImported": "{count} righe importate finora",
"doneSummary": "Fatto: {imported} di {read} record importati, {skipped} record clinici saltati.",
"doneNothing": "Da questo archivio non è stato importato nulla: sono arrivati 0 record su {read}.",
+ "writtenByHealthLogOne": "{count} registrazione che HealthLog ha scritto in Salute è stata esclusa, perché è già nei tuoi dati.",
+ "writtenByHealthLogOther": "{count} registrazioni che HealthLog ha scritto in Salute sono state escluse, perché sono già nei tuoi dati.",
+ "writtenByHealthLogFew": "{count} registrazioni che HealthLog ha scritto in Salute sono state escluse, perché sono già nei tuoi dati.",
"ecgResult": "ECG: {discovered} rilevati, {imported} importati, {updated} aggiornati, {skipped} ignorati e {failed} non riusciti. Gli errori ECG non eliminano i record supportati importati da export.xml.",
"estimateWarning": "I totali cumulativi di {count} giorno/i sono stati stimati dalla fonte di esportazione più alta. Le statistiche native di Apple Salute restano autorevoli.",
"phase": {
@@ -5381,12 +5550,12 @@
"parsing": "Lettura dell'esportazione…",
"upserting": "Importazione dei record…"
},
- "skipUnsupported": "{count} campioni saltati — tipi di dati che HealthLog non importa.",
+ "skipUnsupported": "{count} campioni saltati: tipi di dati che HealthLog non importa.",
"skipRefused": "{count} campioni non sono stati scritti (illeggibili, fuori intervallo o salvataggio non riuscito).",
"skipBreakdown": "Mostra dettaglio ({count} tipi)",
"skipBreakdownMore": "+ {count} altri tipi",
"cycleSummary": "{samples} campioni del ciclo uniti in {days} registri giornalieri.",
- "cycleModuleOff": "{count} campioni del ciclo saltati — il monitoraggio del ciclo è disattivato per questo account.",
+ "cycleModuleOff": "{count} campioni del ciclo saltati perché il monitoraggio del ciclo è disattivato per questo account.",
"cycleDaysFailed": "{count} giorni del ciclo non sono stati scritti: {reason}"
},
"json": {
@@ -5471,8 +5640,8 @@
"failed": "L'importazione è fallita. Non è stato scritto nulla.",
"columnsHeading": "Cosa fa ogni colonna",
"jsonOnly": "solo JSON",
- "previewLine": "Righe lette: {rows} · da importare: {queued} · rifiutate: {refused}",
- "submittedLine": "Righe lette: {rows} · in coda per l'importazione: {queued}",
+ "previewLine": "{rows} righe lette, {queued} da importare, {refused} rifiutate",
+ "submittedLine": "{rows} righe lette, {queued} in coda per l'importazione",
"unmatched": "Nessun farmaco registrato corrisponde a: {names}",
"ambiguous": "Corrisponde a più di un farmaco: {names}",
"mirrored": "Copiato da un'altra app, quindi non si può aggiungere alcuna dose: {names}",
@@ -5508,18 +5677,59 @@
"tooManyRows": "Quel file contiene più righe di quante un'importazione possa accoglierne. Dividilo e importa le parti."
},
"detail": "Un file copre tutti i farmaci che contiene."
+ },
+ "healthConnect": {
+ "title": "Esportazione di Health Connect",
+ "description": "Importa il file ZIP che l’app Health Connect su Android crea quando esporti i tuoi dati.",
+ "choose": "Scegli file ZIP",
+ "uploading": "Caricamento…",
+ "uploadFailed": "Il caricamento non è riuscito. Riprova.",
+ "tooLarge": "Il file supera la dimensione accettata da questo server.",
+ "doneSummary": "{count} valori importati.",
+ "failed": "L’importazione non è riuscita: {reason}",
+ "phase": {
+ "queued": "In attesa di avvio",
+ "unpacking": "Estrazione del file",
+ "parsing": "Lettura dei record",
+ "upserting": "Salvataggio dei valori",
+ "done": "Completato",
+ "failed": "Non riuscito"
+ },
+ "steps": {
+ "export": "Sul tuo telefono Android apri Health Connect, vai su Gestisci dati, poi Esporta e importa, e avvia un'esportazione.",
+ "cloud": "Health Connect salva l'esportazione solo in un'app di archiviazione cloud come Google Drive, Dropbox o Nextcloud, mai sul telefono stesso. Scegline una.",
+ "upload": "Scarica il file ZIP da quel cloud su questo dispositivo e caricalo qui."
+ },
+ "dropLabel": "Trascina qui lo ZIP di Health Connect",
+ "dropHint": "oppure fai clic per sceglierlo. Fino a 1 GB.",
+ "fileInputLabel": "File ZIP dell'esportazione di Health Connect",
+ "privacyNote": "Il file viene eliminato dal server appena l'importazione termina. I dati delle app che colleghi già direttamente, come Withings o Fitbit, vengono esclusi perché nulla arrivi due volte. Caricare di nuovo lo stesso file non cambia nulla.",
+ "rateLimited": "Troppi caricamenti in poco tempo. Attendi un minuto e riprova.",
+ "busy": "Per il tuo account è già in corso un'importazione. Attendi che finisca.",
+ "rowsSoFar": "{count} valori salvati finora",
+ "doneNothing": "L'importazione è terminata, ma non c'era nulla di nuovo da aggiungere.",
+ "workouts": "Allenamenti",
+ "leftOutConnected": "{count} dati da app che colleghi già direttamente sono stati esclusi.",
+ "nutrientsModuleOff": "I totali di acqua e nutrienti non sono stati importati perché il modulo Nutrienti è disattivato.",
+ "cycleModuleOff": "I dati del ciclo mestruale non sono stati importati perché il monitoraggio del ciclo è disattivato.",
+ "failure": {
+ "unsupportedVersion": "Questa esportazione di Health Connect è troppo vecchia per essere letta. Aggiorna Health Connect sul telefono ed esporta di nuovo.",
+ "notHealthConnect": "Questo file non è un'esportazione di Health Connect. Scegli il file ZIP salvato da Health Connect.",
+ "unsafeSchema": "Questo file non ha la struttura di un'esportazione di Health Connect e non è stato letto.",
+ "tooLarge": "L'esportazione è più grande di quanto accetti questo server.",
+ "stagingMissing": "Il file caricato non era più sul server. Caricalo di nuovo.",
+ "interrupted": "Un riavvio del server ha interrotto l'importazione. Carica di nuovo il file."
+ }
}
},
"subtitle": "Esporta i tuoi dati o importa da un backup."
},
"about": {
"title": "Informazioni su questa app",
- "description": "Versione, licenza, link.",
"subtitle": "Versione, licenza e link al progetto."
},
"sharing": {
"title": "Condivisione",
- "description": "Crea un link di sola lettura per condividere la tua cartella clinica con un medico.",
"subtitle": "Condividi l'accesso in sola lettura ai tuoi dati."
},
"insights": {
@@ -5554,12 +5764,10 @@
},
"gesundheitsakte": {
"title": "Cartella clinica",
- "description": "Esporta la tua cartella clinica completa in PDF, FHIR R4 o in un pacchetto ZIP combinato.",
"subtitle": "Esporta una cartella clinica completa, pronta per il medico."
},
"coach": {
"title": "Coach",
- "description": "Attiva il Coach, tono e ambito, ricordi salvati.",
"subtitle": "Regola il Coach, i suoi suggerimenti proattivi e la sua memoria."
},
"security": {
@@ -5568,8 +5776,7 @@
},
"privacy": {
"title": "Dati e privacy",
- "subtitle": "I tuoi dati, la conservazione, la cifratura e la sicurezza dell'account in un unico posto.",
- "description": "Esportazione, eliminazione, conservazione, stato della cifratura, sessioni e attività di sicurezza."
+ "subtitle": "I tuoi dati, la conservazione, la cifratura e la sicurezza dell'account in un unico posto."
},
"mcp": {
"title": "Connettore MCP",
@@ -5577,8 +5784,7 @@
},
"anamnesis": {
"title": "Anamnesi",
- "subtitle": "Gestisci le tue allergie e la tua anamnesi familiare.",
- "description": "Record strutturati di allergie e anamnesi familiare."
+ "subtitle": "La tua anamnesi, lo stile di vita, i dati di emergenza e ciò che l’IA può leggere."
},
"environment": {
"title": "Posizione e meteo",
@@ -5612,7 +5818,8 @@
"add": "Aggiungi {label}",
"remove": "Rimuovi periodo di posizione",
"removeConfirmTitle": "Rimuovere questo viaggio?",
- "removeConfirmBody": "Le misurazioni di quel periodo restano nel tuo archivio; sparisce solo la voce del viaggio."
+ "removeConfirmBody": "Le misurazioni di quel periodo restano nel tuo archivio; sparisce solo la voce del viaggio.",
+ "range": "Dal {start} al {end}"
},
"backfill": {
"title": "Recupera i giorni passati",
@@ -5622,7 +5829,21 @@
"noResults": "Nessun luogo corrispondente trovato.",
"fetchFailedOne": "L’ultimo aggiornamento meteo in background non è riuscito ({when}).",
"fetchFailedFew": "Gli ultimi {count} aggiornamenti meteo in background non sono riusciti, il più recente {when}.",
- "fetchFailedOther": "Gli ultimi {count} aggiornamenti meteo in background non sono riusciti, il più recente {when}."
+ "fetchFailedOther": "Gli ultimi {count} aggiornamenti meteo in background non sono riusciti, il più recente {when}.",
+ "airQuality": {
+ "title": "Qualità dell’aria e pollini",
+ "description": "Qualità dell’aria, pollini e UV giornalieri per la tua località, dal modello CAMS tramite Open-Meteo.",
+ "toggle": "Includi qualità dell’aria e pollini",
+ "operatorDisabled": "La qualità dell’aria è disattivata su questo server.",
+ "coverage": "{count} giorni con dati sulla qualità dell’aria",
+ "pollenEuropeOnly": "I dati sui pollini coprono solo l’Europa.",
+ "historyProgress": "Recupero della qualità dell’aria: {done} di {total} giorni",
+ "historyComplete": "La qualità dell’aria è completata per tutti i giorni passati con registrazioni coperti dalla fonte ({total} giorni). I giorni più vecchi restano vuoti.",
+ "attribution": "Fonti dei dati e licenze",
+ "saveError": "Impossibile salvare l’impostazione.",
+ "latest": "Giorno più recente ({date})",
+ "modelNote": "Valori esterni modellati per la tua zona, non la tua esposizione personale."
+ }
},
"score": {
"title": "Punteggio di salute",
@@ -5749,7 +5970,6 @@
},
"changePassword": "Cambia password",
"changePasswordDescription": "Sostituisci la password attuale con una nuova.",
- "passwordReset": "Reimposta password",
"currentPassword": "Password attuale",
"newPassword": "Nuova password",
"confirmNewPassword": "Conferma nuova password",
@@ -5794,9 +6014,9 @@
"botToken": "Token del bot",
"chatId": "ID chat",
"enableNotifications": "Attiva le notifiche",
- "telegramStep1": "1. Crea un bot tramite @BotFather su Telegram e copia il token.",
- "telegramStep2": "2. Invia /start al tuo bot per attivare la chat.",
- "telegramStep3": "3. Trova il tuo ID chat tramite @userinfobot o l’API del bot.",
+ "telegramStep1": "Crea un bot tramite @BotFather su Telegram e copia il token.",
+ "telegramStep2": "Invia /start al tuo bot per attivare la chat.",
+ "telegramStep3": "Trova il tuo ID chat tramite @userinfobot o l’API del bot.",
"ntfy": "ntfy",
"ntfyDescription": "Notifiche tramite ntfy (self-hosted o ntfy.sh).",
"ntfyEnable": "Attiva ntfy",
@@ -5805,7 +6025,7 @@
"ntfyAuthToken": "Token di autenticazione (facoltativo)",
"ntfyAuthTokenHint": "Necessario solo per topic privati con controllo degli accessi.",
"webhook": "Webhook",
- "webhookDescription": "Invia le notifiche via POST a qualsiasi URL — Gotify, Discord, Slack, Matrix o Home Assistant.",
+ "webhookDescription": "Invia le notifiche via POST a un URL: Discord, Slack, Gotify, Home Assistant, n8n o un relay che accetti JSON.",
"webhookEnable": "Attiva webhook",
"webhookUrl": "URL del webhook",
"webhookUrlHint": "L’URL completo per il POST. Un indirizzo su una rete privata deve essere autorizzato dall’amministratore del server.",
@@ -5813,6 +6033,7 @@
"webhookHeaderValue": "Valore intestazione (opzionale)",
"webhookFormat": "Formato del messaggio",
"webhookFormatGeneric": "JSON generico",
+ "webhookFormatGenericHint": "Incolla così com’è un URL webhook di Discord o Slack. Home Assistant e n8n leggono i campi title, message, eventType e priority.",
"webhookFormatGotify": "Gotify",
"webhookFormatGotifyHint": "Usa l’URL che termina con /message e inserisci il token dell’applicazione nell’intestazione X-Gotify-Key.",
"email": "Email",
@@ -5829,7 +6050,6 @@
"webPushSubscribed": "Notifiche push attivate!",
"webPushUnsubscribed": "Notifiche push disattivate.",
"webPushSubscribeFailed": "Attivazione non riuscita",
- "kiInsightsDescription": "Facoltativo: salva la tua chiave OpenAI per analisi giornaliere e automatiche in Insights.",
"codexConnected": "ChatGPT connesso correttamente! Insights è ora attivo.",
"codexDisconnected": "Connessione a ChatGPT rimossa.",
"codexConnectionFailed": "Connessione a ChatGPT non riuscita. Riprova.",
@@ -5839,7 +6059,7 @@
"rawDataWarning": "Cronologia dettagliata attiva: ogni analisi invia anche le medie giornaliere degli ultimi 90 giorni, le medie settimanali dell’anno precedente e le medie mensili fino a 5 anni fa.",
"regenerateInsights": "Rigenera i report",
"regenerateSuccess": "Report rigenerati correttamente",
- "regenerateRateLimit": "Attendi — hai raggiunto il limite orario di analisi.",
+ "regenerateRateLimit": "Attendi: hai raggiunto il limite orario di analisi.",
"lastGeneratedAt": "Ultima generazione",
"ai": {
"chatgptConnectedBadge": "ChatGPT connesso",
@@ -5849,11 +6069,11 @@
"deviceCodeHeading": "Completa la connessione su chatgpt.com",
"deviceCodeStep1": "Apri questo link su qualsiasi dispositivo:",
"deviceCodeStep2": "Inserisci questo codice monouso:",
- "deviceCodeStep3": "Approva la connessione — questa pagina si aggiorna automaticamente.",
+ "deviceCodeStep3": "Approva la connessione. Questa pagina si aggiorna automaticamente.",
"deviceCodeCopy": "Copia",
"deviceCodeWaiting": "In attesa di approvazione…",
"deviceCodeCancel": "Annulla",
- "oauthNotConfigured": "L’OAuth di ChatGPT non è configurato su questa istanza — usa invece la tua chiave API qui sotto.",
+ "oauthNotConfigured": "L’OAuth di ChatGPT non è configurato su questa istanza. Usa invece la tua chiave API qui sotto.",
"modelLabel": "Modello",
"modelOptionDefault": "— Predefinito —",
"modelOptionCustom": "Personalizzato…",
@@ -5877,7 +6097,7 @@
"admin-openai": "OpenAI amministratore"
},
"title": "Catena di fallback",
- "description": "Se il provider principale fallisce, HealthLog percorre la catena in ordine. Trascina le righe per riordinarle; usa l’interruttore per disattivare un provider senza rimuoverlo.",
+ "description": "Se il fornitore attivo non risponde, HealthLog prova il successivo attivo in quest’ordine.",
"moveUp": "Sposta su",
"moveDown": "Sposta giù",
"removeFromChain": "Rimuovi dalla catena",
@@ -5890,11 +6110,8 @@
"resetConfirmTitle": "Ripristinare la catena ai valori predefiniti?",
"resetConfirmBody": "La catena torna a Codex → OpenAI → Anthropic → Locale → OpenAI amministratore. Le credenziali salvate non vengono toccate."
},
- "providerCardTitle": "Provider IA",
"activeProviderHeading": "Provider attivo",
- "activeProviderBody": "Scegli il provider da usare per primo. Il modulo qui sotto configura solo il provider selezionato; la catena di fallback in fondo decide cosa accade se fallisce.",
"activeProviderLabel": "Provider principale",
- "providerConfigTitle": "Configurazione del provider",
"providerSelect": {
"codex": "Account ChatGPT (Codex)",
"openai": "OpenAI (la tua chiave API)",
@@ -5940,25 +6157,24 @@
"notConfigured": "L’operatore non ha configurato una chiave OpenAI condivisa su questa istanza."
},
"testProvider": "Prova il provider attivo",
- "testSuccess": "OK — {provider} ({model})",
+ "testSuccess": "OK: {provider} ({model})",
"testFailedShort": "Prova non riuscita: {message}",
- "testUnexpectedResponse": "Connessione al provider IA non riuscita — risposta inattesa dal server.",
- "testReasonCredentials": "Il provider ha rifiutato le credenziali — autenticati di nuovo nelle impostazioni IA.",
- "testReasonRateLimited": "Il provider ha limitato la richiesta — riprova a breve.",
+ "testUnexpectedResponse": "Connessione al provider IA non riuscita: risposta inattesa dal server.",
+ "testReasonCredentials": "Il provider ha rifiutato le credenziali. Autenticati di nuovo nelle impostazioni IA.",
+ "testReasonRateLimited": "Il provider ha limitato la richiesta. Riprova a breve.",
"testReasonServerError": "Il provider IA ha restituito un errore del server.",
"testReasonBadRequest": "Il provider ha rifiutato la richiesta (HTTP {status}): l'endpoint ha risposto, quindi il problema riguarda il formato della richiesta o il nome del modello, non la connessione.",
"testReasonUnreachable": "Impossibile raggiungere il provider IA.",
"coachMemory": {
"title": "Ciò che il Coach ricorda",
"description": "Informazioni durature che hai comunicato al Coach.",
- "empty": "Il Coach non ha ancora memorizzato alcuna informazione. Mentre chatti, ricorda i dettagli duraturi che condividi — i tuoi obiettivi, le condizioni, le preferenze — e puoi cancellarli qui in qualsiasi momento.",
+ "empty": "Il Coach non ha ancora memorizzato alcuna informazione. Mentre chatti, ricorda i dettagli duraturi che condividi (i tuoi obiettivi, le condizioni, le preferenze) e puoi cancellarli qui in qualsiasi momento.",
"learnedPrefix": "appreso {when}",
"categoryPreference": "Preferenze",
"categoryCondition": "Condizioni",
"categoryGoal": "Obiettivi",
"categoryConstraint": "Vincoli",
"categoryContext": "Contesto",
- "forget": "Dimentica",
"forgetAria": "Dimentica questa informazione",
"forgetTitle": "Dimenticare questa informazione?",
"forgetBody": "Il Coach smette di usarla dal messaggio successivo. Un’informazione dimenticata non può essere recuperata.",
@@ -5968,13 +6184,14 @@
"forgetAllConfirmBody": "Questo rimuove tutte le informazioni memorizzate. Il Coach ripartirà da zero alla prossima chat. L'operazione non può essere annullata.",
"forgetAllConfirmAction": "Dimentica tutto",
"cancel": "Annulla",
- "summaryNote": "Il Coach può anche conservare un breve riassunto progressivo di ogni conversazione. Non esiste un controllo per ogni voce — eliminando una conversazione si elimina anche il suo riassunto.",
+ "summaryNote": "Il Coach può anche conservare un breve riassunto progressivo di ogni conversazione. Non esiste un controllo per ogni voce: eliminando una conversazione si elimina anche il suo riassunto.",
"loadError": "Impossibile caricare ciò che il Coach ricorda.",
"forgotToast": "Informazione dimenticata.",
"forgotError": "Impossibile dimenticare questa informazione. Riprova.",
"clearedToast": "Dimenticate {count} informazioni memorizzate.",
"clearError": "Impossibile cancellare le informazioni memorizzate. Riprova.",
- "detail": "Le usa per mantenere pertinenti le sue risposte. Rimuovi tutto ciò che preferisci dimentichi."
+ "detail": "Le usa per mantenere pertinenti le sue risposte. Rimuovi tutto ciò che preferisci dimentichi.",
+ "categoryMedication": "Farmaci"
},
"coachReminders": {
"title": "I tuoi promemoria",
@@ -5988,7 +6205,7 @@
"markDone": "Fatto",
"dismiss": "Ignora",
"deleteAria": "Elimina questo promemoria",
- "note": "I promemoria sono memorizzati cifrati. Eliminane uno in qualsiasi momento — viene rimosso da ciò che il Coach può vedere.",
+ "note": "I promemoria sono memorizzati cifrati. Eliminane uno in qualsiasi momento e viene rimosso da ciò che il Coach può vedere.",
"status": {
"proposed": "Suggerito",
"active": "Salvato",
@@ -6035,19 +6252,19 @@
"title": "Leggi i documenti automaticamente con l'IA",
"subLabel": "I documenti vengono letti e indicizzati dal provider di IA configurato, senza confermare ogni documento.",
"honesty": "Quando è attivo, ogni documento che carichi viene inviato al provider di IA configurato per essere letto, descritto e indicizzato per la ricerca, senza conferma per singolo documento. Attivarlo agisce anche retroattivamente: i documenti che hai già caricato e che non hanno ancora un riassunto vengono inviati anch'essi, fino a 200 per ogni attivazione. Questo avviene sul server e il contenuto del documento lascia questa macchina verso quel provider. Se il provider configurato è una connessione in abbonamento (un account IA con accesso effettuato anziché una chiave API), si applicano le sue impostazioni sui dati: i provider in abbonamento possono usare il contenuto per migliorare i propri modelli e nessun accordo sul trattamento dei dati lo copre. I documenti letti da un modello locale self-hosted non lasciano mai il tuo server. Disattivato per impostazione predefinita; attivalo solo se accetti questo compromesso per i tuoi documenti.",
- "confirm": "Ho capito — attiva",
+ "confirm": "Ho capito, attiva",
"cancel": "Annulla"
},
"centralCodex": {
"title": "Usa l'accesso IA condiviso del server",
- "subLabel": "Instrada le richieste IA attraverso la connessione che l'operatore ha configurato per questo server, insieme ai tuoi provider come ripiego.",
+ "subLabel": "Le richieste IA passano dalla connessione condivisa del server, con i tuoi provider come ripiego.",
"honesty": "L'accesso condiviso è un unico account IA con accesso eseguito che l'operatore ha collegato per tutti su questo server, non la tua chiave. Le richieste che vi effettui vengono usate per impostazione predefinita per addestrare i modelli del provider e nessun accordo sul trattamento dei dati le copre, i limiti orari e settimanali dell'account sono condivisi con tutti gli altri che hanno aderito, e i tuoi provider vengono comunque provati per primi. Attivalo solo se accetti questo compromesso.",
- "confirm": "Ho capito — usa la connessione",
+ "confirm": "Ho capito, usa la connessione",
"cancel": "Annulla"
},
"consent": {
"title": "Consenso IA",
- "activeSince": "Concesso il {date}. I tuoi dati sanitari possono essere inviati al provider configurato per le funzioni IA.",
+ "activeSince": "Concesso il {date}: i tuoi dati sanitari possono essere inviati al provider configurato per le funzioni IA.",
"withdrawn": "Revocato. Salvo che un altro consenso lo copra, il Coach e Insights non usano più l'accesso IA fornito da questo server e nessun modello online legge i tuoi documenti. Con la tua chiave o un modello locale, il Coach e Insights continuano a funzionare.",
"withdraw": "Revoca il consenso",
"confirmBody": "Salvo che un altro consenso li copra ancora, si fermano subito: il Coach e Insights quando usano l'accesso IA fornito da questo server, e la lettura dei documenti con un modello online. Con la tua chiave o un modello locale, il Coach e Insights continuano a funzionare. Salvo che un altro consenso copra ancora Insights, le note che un modello ha scritto per te vengono eliminate: note di stato, il riepilogo giornaliero, i riepiloghi di periodo scritti da un modello, le righe di arrivo e le note sugli allenamenti. Le tue conversazioni con il Coach, ciò che il Coach ricordava, i tuoi piani e i riepiloghi dei tuoi documenti restano, e puoi leggerli o eliminarli tu stesso.",
@@ -6058,7 +6275,7 @@
"error": "Non è stato possibile salvare la revoca. Riprova.",
"documentReading": {
"title": "Consenso alla lettura dei documenti",
- "activeSince": "Dato il {date}. Documenti, referti di laboratorio scansionati e descrizioni di farmaci possono essere inviati al tuo servizio di IA per la lettura.",
+ "activeSince": "Dato il {date}: documenti, referti scansionati e testi dei farmaci possono andare al tuo servizio di IA per la lettura.",
"confirmBody": "Documenti, referti di laboratorio scansionati e descrizioni di farmaci non vengono più inviati per la lettura. Nulla di ciò che hai già viene eliminato."
}
},
@@ -6080,7 +6297,21 @@
"title": "Piani del Coach",
"description": "I piani concordati con il Coach, da leggere o rimuovere.",
"open": "Apri i piani"
- }
+ },
+ "reasoning": {
+ "label": "Ragionamento",
+ "hint": "I modelli di ragionamento come Gemma o Qwen pensano prima di rispondere. Se lo disattivi, tutto il budget della risposta resta per la risposta stessa.",
+ "options": {
+ "default": "Predefinito",
+ "none": "Disattivato",
+ "low": "Basso",
+ "medium": "Medio",
+ "high": "Alto"
+ },
+ "coachNote": "Nel Coach vale la profondità di ragionamento delle impostazioni del Coach."
+ },
+ "providerCardDescription": "Il fornitore che HealthLog interpella per primo, con chiave e modello.",
+ "runtimeTitle": "Verifica e aggiorna"
},
"dashboard": {
"units": {
@@ -6104,8 +6335,8 @@
"withingsClientId": "ID client",
"withingsClientSecret": "Secret client",
"withingsCredentialsSaved": "Credenziali salvate",
- "withingsCredentialsSavedPlaceholder": "Salvato — inseriscine uno nuovo per sostituirlo",
- "withingsCredentialsSavedPlaceholderSecret": "Salvato — inseriscine uno nuovo per sostituirlo",
+ "withingsCredentialsSavedPlaceholder": "Salvato. Inseriscine uno nuovo per sostituirlo",
+ "withingsCredentialsSavedPlaceholderSecret": "Salvato. Inseriscine uno nuovo per sostituirlo",
"withingsSaveCredentials": "Salva",
"configured": "Configurato",
"withingsSync": "Sincronizza ora",
@@ -6120,7 +6351,7 @@
},
"withingsFullSyncResult": "{count} misurazioni sincronizzate completamente",
"withingsSyncFailed": "Sincronizzazione non riuscita",
- "withingsOauthConnected": "Withings collegato — la prima sincronizzazione parte in background.",
+ "withingsOauthConnected": "Withings collegato: la prima sincronizzazione parte in background.",
"withingsOauthFailed": "Collegamento a Withings non riuscito",
"withingsOauthError": {
"csrf1": "Il controllo di sicurezza non è riuscito perché mancava il cookie del browser. Avvia di nuovo il collegamento nello stesso browser.",
@@ -6145,8 +6376,8 @@
"whoopClientId": "Client ID",
"whoopClientSecret": "Client Secret",
"whoopCredentialsSaved": "Credenziali salvate",
- "whoopCredentialsSavedPlaceholder": "Salvato — inserisci nuove per sostituire",
- "whoopCredentialsSavedPlaceholderSecret": "Salvato — inserisci nuove per sostituire",
+ "whoopCredentialsSavedPlaceholder": "Salvato. Inserisci nuove per sostituire",
+ "whoopCredentialsSavedPlaceholderSecret": "Salvato. Inserisci nuove per sostituire",
"whoopSaveCredentials": "Salva",
"whoopSync": "Sincronizza ora",
"whoopFullSync": "Sincronizza tutti i dati",
@@ -6161,7 +6392,7 @@
"whoopConnect": "Connetti con WHOOP",
"whoopNoCredentials": "Inserisci le tue credenziali API qui sopra per connettere WHOOP.",
"whoopBackfillInProgress": "Importazione dello storico WHOOP in background…",
- "whoopOauthConnected": "WHOOP collegato — la prima sincronizzazione parte in background.",
+ "whoopOauthConnected": "WHOOP collegato: la prima sincronizzazione parte in background.",
"whoopOauthFailed": "Connessione a WHOOP non riuscita",
"whoopOauthError": {
"csrf1": "Il controllo di sicurezza è fallito perché il cookie del browser era assente. Riavvia la connessione nello stesso browser.",
@@ -6175,14 +6406,14 @@
},
"fitbit": "Fitbit",
"fitbitTag": "Fitbit e Pixel",
- "fitbitDescription": "Collega Fitbit per sincronizzare i dati di Fitbit e Pixel Watch — peso, frequenza cardiaca e altro.",
+ "fitbitDescription": "Collega Fitbit per sincronizzare i dati di Fitbit e Pixel Watch: peso, frequenza cardiaca e altro.",
"fitbitExperimentalBadge": "Sperimentale",
"fitbitCredentials": "Credenziali API",
"fitbitClientId": "Client ID",
"fitbitClientSecret": "Client Secret",
"fitbitCredentialsSaved": "Credenziali salvate",
- "fitbitCredentialsSavedPlaceholder": "Salvato — inserisci nuove per sostituire",
- "fitbitCredentialsSavedPlaceholderSecret": "Salvato — inserisci nuove per sostituire",
+ "fitbitCredentialsSavedPlaceholder": "Salvato. Inserisci nuove per sostituire",
+ "fitbitCredentialsSavedPlaceholderSecret": "Salvato. Inserisci nuove per sostituire",
"fitbitSaveCredentials": "Salva",
"fitbitSync": "Sincronizza ora",
"fitbitFullSync": "Sincronizza tutti i dati",
@@ -6197,18 +6428,18 @@
"fitbitConnect": "Connetti con Fitbit",
"fitbitNoCredentials": "Inserisci le tue credenziali API qui sopra per connettere Fitbit.",
"fitbitBackfillInProgress": "Importazione dello storico Fitbit in background…",
- "fitbitOauthConnected": "Fitbit collegato — la prima sincronizzazione parte in background.",
+ "fitbitOauthConnected": "Fitbit collegato: la prima sincronizzazione parte in background.",
"fitbitOauthFailed": "Connessione a Fitbit non riuscita",
"googleHealth": "Google Health",
"googleHealthTag": "Fitbit e Pixel",
"googleHealthBetaBadge": "Beta",
- "googleHealthDescription": "Collega Fitbit, Pixel Watch e Fitbit Air tramite il tuo account Google — peso, frequenza cardiaca, sonno e attività. Sostituisce la connessione Fitbit classica, che Google ritira a settembre 2026.",
+ "googleHealthDescription": "Collega Fitbit, Pixel Watch e Fitbit Air tramite il tuo account Google: peso, frequenza cardiaca, sonno e attività. Sostituisce la connessione Fitbit classica, che Google ritira a settembre 2026.",
"googleHealthCredentials": "Credenziali API",
"googleHealthClientId": "ID client",
"googleHealthClientSecret": "Client secret",
"googleHealthCredentialsSaved": "Credenziali salvate",
- "googleHealthCredentialsSavedPlaceholder": "Salvate — inseriscine di nuove per sostituirle",
- "googleHealthCredentialsSavedPlaceholderSecret": "Salvate — inseriscine di nuove per sostituirle",
+ "googleHealthCredentialsSavedPlaceholder": "Salvate. Inseriscine di nuove per sostituirle",
+ "googleHealthCredentialsSavedPlaceholderSecret": "Salvate. Inseriscine di nuove per sostituirle",
"googleHealthSaveCredentials": "Salva",
"googleHealthSync": "Sincronizza ora",
"googleHealthSyncCheckStatus": "Aggiorna stato",
@@ -6242,9 +6473,9 @@
"googleHealthConnect": "Connetti con Google",
"googleHealthNoCredentials": "Inserisci le tue credenziali API qui sopra per connettere Google Health.",
"googleHealthBackfillInProgress": "Importazione della cronologia di Google Health in background…",
- "googleHealthReauthBanner": "Google ha fatto scadere la connessione — accade periodicamente finché la tua app OAuth resta in modalità Test. Riconnettiti per riprendere la sincronizzazione.",
+ "googleHealthReauthBanner": "Google ha fatto scadere la connessione. Accade periodicamente finché la tua app OAuth resta in modalità Test. Riconnettiti per riprendere la sincronizzazione.",
"googleHealthReconnect": "Riconnetti",
- "googleHealthOauthConnected": "Google Health connesso — la prima sincronizzazione parte in background.",
+ "googleHealthOauthConnected": "Google Health connesso: la prima sincronizzazione parte in background.",
"googleHealthOauthFailed": "Connessione a Google Health non riuscita",
"googleHealthOauthError": {
"csrf1": "Il controllo di sicurezza non è riuscito perché mancava il cookie del browser. Riavvia la connessione nello stesso browser.",
@@ -6279,10 +6510,10 @@
},
"integrationPill": {
"connected": "Connesso",
- "errorReconnect": "Errore — riconnetti",
+ "errorReconnect": "Errore, riconnetti",
"warningServerError": "Sincronizzazione in errore",
"failureCount": "{count}/{threshold}",
- "parkedReconnect": "In pausa — riconnetti",
+ "parkedReconnect": "In pausa, riconnetti",
"notConnected": "Non connesso",
"justNow": "proprio ora",
"minutesAgo": "{count} min fa",
@@ -6297,7 +6528,7 @@
},
"integrationFreshness": {
"title": "Dati ricevuti",
- "summary": "{count} tipi di dati · più recente {relative}",
+ "summary": "{count} tipi di dati, più recente {relative}",
"quiet": "{count} in silenzio",
"workouts": "Allenamenti"
},
@@ -6311,31 +6542,33 @@
"fitbitViewData": "Vedi il tuo sonno",
"googleHealthViewData": "Vedi i tuoi passi",
"apiTokens": "Token API",
- "apiTokensDescription": "Rivedi e revoca i token API per la registrazione esterna delle assunzioni di farmaci.",
+ "apiTokensDescription": "Rivedi e revoca i token per misurazioni, allenamenti, documenti e assunzioni di farmaci.",
"measurementsToken": {
"title": "Token per misurazioni",
"description": "Crea un token perché una bilancia, un orologio o la tua domotica aggiungano misurazioni.",
"detail": "Il token viene mostrato una sola volta.",
"scopeNote": "Può soltanto aggiungere nuove misurazioni alla tua cartella: non può leggere i tuoi dati, modificare o eliminare nulla, né creare un altro token. Le misurazioni vengono contrassegnate come provenienti da un dispositivo esterno, così le riconosci in seguito, e puoi comunque correggerle.",
- "createFailed": "Non è stato possibile creare il token. Riprova."
+ "createFailed": "Non è stato possibile creare il token. Riprova.",
+ "namePlaceholder": "Nome, ad es. Home Assistant"
},
"workoutsToken": {
"title": "Token per allenamenti",
"description": "Crea un token perché un ponte dell'orologio o uno script di sincronizzazione aggiungano i tuoi allenamenti.",
"detail": "Il token viene mostrato una sola volta.",
"scopeNote": "Può soltanto aggiungere nuovi allenamenti alla tua cartella. Non può leggere i tuoi dati, modificare o eliminare nulla, né creare un altro token. Gli allenamenti che aggiunge vengono contrassegnati come provenienti da un dispositivo esterno e, se il telefono ha registrato la stessa sessione, vedi la copia del telefono.",
- "createFailed": "Non è stato possibile creare il token. Riprova."
+ "createFailed": "Non è stato possibile creare il token. Riprova.",
+ "namePlaceholder": "Nome, ad es. Garmin"
},
"documentsToken": {
"title": "Token per documenti",
"description": "Crea un token perché Paperless-ngx, Papra o lo script di importazione aggiungano documenti al tuo archivio.",
"detail": "Il token viene mostrato una sola volta.",
"scopeNote": "Può soltanto aggiungere nuovi documenti al tuo archivio. Non può elencare, aprire, scaricare, modificare o eliminare nulla, né creare un altro token. Riceve solo l'informazione se un file è stato salvato, non come l'hai archiviato.",
- "createFailed": "Non è stato possibile creare il token. Riprova."
+ "createFailed": "Non è stato possibile creare il token. Riprova.",
+ "namePlaceholder": "Nome, ad es. Paperless-ngx"
},
- "tokenMintMovedDescription": "I token vengono emessi dove si applicano: qui sopra per le misurazioni e i documenti, oppure nell'endpoint API di un farmaco per quel singolo farmaco.",
- "tokenNamePlaceholder": "Nome del token (ad es. Comando rapido iPhone)",
- "tokenCreated": "Token creato — copialo ora! Non verrà mostrato di nuovo.",
+ "tokenMintMovedDescription": "I token vengono emessi dove si applicano: qui sopra per le misurazioni, gli allenamenti e i documenti, oppure nell'endpoint API di un farmaco per quel singolo farmaco.",
+ "tokenCreated": "Token creato: copialo ora! Non verrà mostrato di nuovo.",
"tokenCopy": "Copia token",
"tokenCopied": "Token copiato negli appunti",
"tokenCopyFailed": "Impossibile copiare il token",
@@ -6374,7 +6607,7 @@
"deleteAccountCta": "Elimina account",
"deleteAccountConfirmTitle": "Eliminare definitivamente l’account?",
"deleteAccountConfirmDescription": "Il tuo account, tutti i dati sanitari, le passkey, le sessioni e le voci di audit verranno eliminati definitivamente. Verrai disconnesso subito dopo.",
- "deleteAccountSuccess": "Account eliminato — disconnessione in corso.",
+ "deleteAccountSuccess": "Account eliminato, disconnessione in corso.",
"deleteAccountFailed": "Impossibile eliminare l’account",
"deleteAccountFinal": "Elimina definitivamente",
"saved": "Salvato",
@@ -6397,7 +6630,7 @@
"testing": "Test in corso…",
"ok": "Connesso (latenza {latency} ms)",
"errors": {
- "credentials_rejected": "Credenziali rifiutate — controlla il token",
+ "credentials_rejected": "Credenziali rifiutate: controlla il token",
"rate_limited": "Limitato dal servizio upstream",
"timeout": "Richiesta scaduta",
"upstream_error": "Il servizio upstream ha restituito un errore",
@@ -6408,7 +6641,7 @@
"private_origin_not_approved": "Destinazione su una rete privata. L’operatore deve aggiungere questa origine a NOTIFICATION_PRIVATE_ORIGINS",
"private_origin_not_grantable": "Questo indirizzo è link-local, dei metadati o non specificato; nessuna autorizzazione dell’operatore può aprirlo. Usa l’indirizzo su cui il relay è in ascolto",
"url_invalid": "L’URL non è valido",
- "redirected": "L’endpoint reindirizza — controlla l’URL",
+ "redirected": "L’endpoint reindirizza: controlla l’URL",
"endpoint_not_found": "Endpoint non trovato all’URL",
"credentials_unreadable": "Impossibile decifrare le credenziali",
"upstream_invalid_json": "Il servizio upstream ha inviato un JSON non valido",
@@ -6419,7 +6652,7 @@
},
"notificationStatus": {
"title": "Affidabilità dei canali",
- "description": "Stato di ogni canale — «Disattivato automaticamente» significa fermato dopo errori permanenti.",
+ "description": "Stato di ogni canale; «Disattivato automaticamente» significa fermato dopo errori permanenti.",
"emptyDescription": "Aggiungi un canale in Impostazioni › Integrazioni › Canali di recapito.",
"stateActive": "Attivo",
"stateAutoDisabled": "Disattivato automaticamente",
@@ -6445,7 +6678,7 @@
},
"healthRecord": {
"title": "Esporta cartella clinica",
- "description": "Una cartella completa per il medico — in PDF, in FHIR o entrambi.",
+ "description": "Una cartella completa per il medico, in PDF, in FHIR o entrambi.",
"format": "Formato",
"format_pdf": "PDF",
"format_fhir": "Strutturato (FHIR)",
@@ -6477,7 +6710,7 @@
"createTitle": "Nuovo link di condivisione",
"createDescription": "Il link è di sola lettura e a tempo.",
"label": "Etichetta",
- "labelPlaceholder": "es. Dott. Rossi — controllo annuale",
+ "labelPlaceholder": "es. Dott. Rossi, controllo annuale",
"range": "Giorni di cronologia",
"rangeHint": "Fin dove si estende la finestra condivisa.",
"expiry": "Scade tra (giorni)",
@@ -6497,7 +6730,7 @@
"statusRevoked": "Revocato",
"statusExpired": "Scaduto",
"inactiveTitle": "Revocati e scaduti ({count})",
- "shownOnce": "Mostrato una sola volta. Salva ora il codice QR e la passphrase — non potranno essere recuperati in seguito.",
+ "shownOnce": "Mostrato una sola volta. Salva ora il codice QR e la passphrase: non potranno essere recuperati in seguito.",
"qrAlt": "Codice QR contenente il link di condivisione e la passphrase",
"linkLabel": "Link",
"passphraseLabel": "Passphrase",
@@ -6526,7 +6759,8 @@
"statusNeedsReselect": "Scegli di nuovo il contenuto",
"reselectReason": "Questo link mostrava un ambito che nessuno aveva scelto, perciò è stato chiuso. Creane uno nuovo e scegli cosa deve contenere.",
"reselectAction": "Crea un sostituto",
- "createDetail": "Il token viene mostrato una sola volta alla creazione — salvalo subito; non può essere recuperato."
+ "createDetail": "Il token viene mostrato una sola volta alla creazione: salvalo subito, non può essere recuperato.",
+ "activeDescription": "I link che hai condiviso, quando scadono e quante volte sono stati aperti."
},
"genderOther": "Altro",
"nightscout": "Nightscout",
@@ -6534,10 +6768,10 @@
"nightscoutDescription": "Collega il tuo sito Nightscout per sincronizzare le letture continue del glucosio nel pannello glucosio.",
"nightscoutViewData": "Vedi la tua glicemia",
"nightscoutUrl": "URL Nightscout",
- "nightscoutUrlSavedPlaceholder": "Salvato — inserisci un nuovo URL per sostituirlo",
+ "nightscoutUrlSavedPlaceholder": "Salvato. Inserisci un nuovo URL per sostituirlo",
"nightscoutToken": "Token API",
"nightscoutTokenOptional": "Facoltativo per un sito pubblico",
- "nightscoutTokenSavedPlaceholder": "Salvato — inserisci un nuovo token per sostituirlo",
+ "nightscoutTokenSavedPlaceholder": "Salvato. Inserisci un nuovo token per sostituirlo",
"nightscoutTokenHelp": "Incolla un token di accesso dagli strumenti di amministrazione di Nightscout. Lascia vuoto se il tuo sito è leggibile pubblicamente.",
"nightscoutPrivateOperatorTitle": "L’accesso a un Nightscout privato richiede l’approvazione dell’amministratore del server.",
"nightscoutPrivateOperatorDescription": "Chiedi all’amministratore del server di approvare esattamente questa origine Nightscout.",
@@ -6558,7 +6792,7 @@
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
"polarCredentialsSaved": "Credenziali salvate",
- "polarCredentialsSavedPlaceholder": "Salvato — inserisci nuove per sostituire",
+ "polarCredentialsSavedPlaceholder": "Salvato. Inserisci nuove per sostituire",
"polarSaveCredentials": "Salva",
"polarUnavailable": "Polar non è configurato su questo server. Aggiungi sopra le tue credenziali OAuth di Polar oppure chiedi al tuo amministratore di configurare l'app condivisa.",
"polarConnect": "Collega Polar",
@@ -6568,7 +6802,7 @@
"polarDisconnect": "Disconnetti",
"polarDisconnectTitle": "Disconnettere Polar?",
"polarDisconnectDescription": "Le sincronizzazioni future si interrompono. Le letture già importate restano nella tua cronologia.",
- "polarOauthConnected": "Polar collegato — la prima sincronizzazione parte in background.",
+ "polarOauthConnected": "Polar collegato: la prima sincronizzazione parte in background.",
"polarOauthFailed": "Connessione a Polar non riuscita",
"polarOauthError": {
"csrf1": "Il controllo di sicurezza è fallito perché il cookie del browser era assente. Riavvia la connessione nello stesso browser.",
@@ -6587,7 +6821,7 @@
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
"ouraCredentialsSaved": "Credenziali salvate",
- "ouraCredentialsSavedPlaceholder": "Salvato — inserisci nuove per sostituire",
+ "ouraCredentialsSavedPlaceholder": "Salvato. Inserisci nuove per sostituire",
"ouraSaveCredentials": "Salva",
"ouraUnavailable": "Oura non è configurato su questo server. Aggiungi sopra le tue credenziali OAuth di Oura oppure chiedi al tuo amministratore di configurare l'app condivisa.",
"ouraConnect": "Collega Oura",
@@ -6597,7 +6831,7 @@
"ouraDisconnect": "Disconnetti",
"ouraDisconnectTitle": "Disconnettere Oura?",
"ouraDisconnectDescription": "Le sincronizzazioni future si interrompono. Le letture già importate restano nella tua cronologia.",
- "ouraOauthConnected": "Oura collegato — la prima sincronizzazione parte in background.",
+ "ouraOauthConnected": "Oura collegato: la prima sincronizzazione parte in background.",
"ouraOauthFailed": "Connessione a Oura non riuscita",
"ouraOauthError": {
"csrf1": "Il controllo di sicurezza è fallito perché il cookie del browser era assente. Riavvia la connessione nello stesso browser.",
@@ -6607,6 +6841,7 @@
"nocreds": "Oura non è configurato su questo server. Chiedi al tuo amministratore di aggiungere le credenziali OAuth di Oura.",
"token": "Lo scambio del token con Oura è fallito. Riavvia la connessione.",
"rate_limited": "Troppi tentativi di connessione. Attendi un momento e riprova.",
+ "issuer": "La risposta di accesso non proveniva dal server di autorizzazione di Oura, quindi la connessione è stata interrotta per la tua sicurezza. Avvia di nuovo la connessione. Se succede ancora, avvisa il tuo amministratore.",
"generic": "Errore sconosciuto durante la connessione. Riavvia la connessione."
},
"integrationSetupGuide": "Guida alla configurazione",
@@ -6618,10 +6853,17 @@
"savedOn": "Coach attivato.",
"savedOff": "Coach nascosto.",
"saveError": "Impossibile salvare la modifica. Riprova."
+ },
+ "memory": {
+ "edit": "Modifica",
+ "source": {
+ "user": "da te",
+ "coach": "dal Coach"
+ }
}
},
"integrationRedirectGuide": {
- "intro": "Questa connessione passa dalla tua app sviluppatore {provider} — circa 5 minuti di configurazione:",
+ "intro": "Questa connessione passa dalla tua app sviluppatore {provider} (circa 5 minuti di configurazione):",
"step1": "Crea un'app nel portale sviluppatori {provider}.",
"step2": "Imposta il suo URL di reindirizzamento/callback su:",
"step3": "Incolla qui sopra l'ID client e il client secret ricevuti."
@@ -6745,12 +6987,11 @@
},
"stored": {
"title": "Cosa viene memorizzato",
- "measurements": "Misurazioni (peso, pressione, polso, glicemia, sonno, composizione corporea)",
- "medications": "Farmaci e cronologia delle assunzioni",
- "moodLabs": "Voci dell'umore e referti di laboratorio",
- "coach": "Conversazioni con il coach",
+ "measurements": "Misurazioni: peso, pressione, battito e ogni altro valore registrato da te o da un dispositivo",
"integrations": "Token di integrazione (cifrati)",
- "security": "Attività dell'account e di sicurezza (sessioni, registro degli accessi)"
+ "security": "Attività dell'account e di sicurezza (sessioni, registro degli accessi)",
+ "profile": "Il tuo profilo, l'anamnesi e i dati della cartella sanitaria",
+ "modules": "Dati di ogni modulo: {list}"
},
"retention": {
"title": "Conservazione",
@@ -6781,8 +7022,6 @@
"mcp": {
"enableTitle": "Endpoint MCP remoto",
"enableDescription": "Consenti a un assistente compatibile con MCP (Claude.ai, ChatGPT) di leggere i tuoi dati sulla salute tramite un token limitato.",
- "enabled": "Attivo",
- "disabled": "Inattivo",
"enableToggleLabel": "Attiva l'endpoint MCP",
"endpointLabel": "URL del connettore",
"connectDescription": "Incolla questo URL nella configurazione del connettore personalizzato del tuo assistente e autorizza con il token qui sotto o accedi quando richiesto.",
@@ -6801,7 +7040,8 @@
"connectionRevokeDescription": "Questo termina immediatamente l'accesso dell'assistente e ne impedisce il rinnovo. Per riconnetterlo sarà necessaria una nuova autorizzazione.",
"connectionRevokeFailed": "Impossibile disconnettere l'assistente.",
"tokensDetail": "Il token viene mostrato una sola volta.",
- "connectionsDetail": "Revocare una connessione ne termina subito l'accesso, inclusa la capacità di rinnovarsi."
+ "connectionsDetail": "Revocare una connessione ne termina subito l'accesso, inclusa la capacità di rinnovarsi.",
+ "tokenNamePlaceholder": "Nome, ad es. Claude Desktop"
},
"strava": "Strava",
"stravaDescription": "Collega il tuo account Strava per sincronizzare le tue attività con la cronologia degli allenamenti.",
@@ -6810,7 +7050,7 @@
"stravaClientId": "ID client",
"stravaClientSecret": "Secret client",
"stravaCredentialsSaved": "Credenziali salvate",
- "stravaCredentialsSavedPlaceholder": "Salvate — inseriscine di nuove per sostituirle",
+ "stravaCredentialsSavedPlaceholder": "Salvate. Inseriscine di nuove per sostituirle",
"stravaSaveCredentials": "Salva",
"stravaUnavailable": "Strava non è configurato su questo server. Aggiungi sopra le tue credenziali API di Strava o chiedi al tuo amministratore di configurare l'app condivisa.",
"stravaConnect": "Collega Strava",
@@ -6820,7 +7060,7 @@
"stravaDisconnect": "Disconnetti",
"stravaDisconnectTitle": "Disconnettere Strava?",
"stravaDisconnectDescription": "Le sincronizzazioni future si interrompono. Le attività già importate restano nella cronologia.",
- "stravaOauthConnected": "Strava collegato — la prima sincronizzazione parte in background.",
+ "stravaOauthConnected": "Strava collegato: la prima sincronizzazione parte in background.",
"stravaOauthFailed": "Connessione a Strava non riuscita",
"stravaOauthError": {
"csrf1": "Il controllo di sicurezza non è riuscito perché mancava il cookie del browser. Riavvia la connessione nello stesso browser.",
@@ -6848,26 +7088,33 @@
"title": "Avanzamento della sincronizzazione",
"receivedLabel": "Record ricevuti",
"oldestLabel": "Lettura più vecchia raggiunta",
- "flowing": "Nell'ultima ora sono arrivati dati — l'app per iPhone sta ancora inviando.",
- "waiting": "In attesa che l'app per iPhone invii altri dati. Il trasferimento continua quando l'app è aperta o quando iOS le concede tempo in background — una pausa durante il primo grande recupero è normale, non un errore."
+ "flowing": "Nell'ultima ora sono arrivati dati. L'app per iPhone sta ancora inviando.",
+ "waiting": "In attesa che l'app per iPhone invii altri dati. Il trasferimento continua quando l'app è aperta o quando iOS le concede tempo in background. Una pausa durante il primo grande recupero è normale, non un errore."
},
"delivery": {
"title": "Consegna",
"lastTriggerLabel": "L'ultima sincronizzazione è arrivata",
"lastBackgroundLabel": "Ultima sincronizzazione in background",
- "backgroundNever": "Mai — finora ogni sincronizzazione è arrivata con l'app aperta",
+ "backgroundNever": "Mai: finora ogni sincronizzazione è arrivata con l'app aperta",
"noSyncYet": "Non è ancora arrivata alcuna sincronizzazione, quindi qui non c'è nulla da riferire.",
"trigger": {
"foreground": "con l'app aperta",
"background": "in background",
"push": "da una sveglia push",
- "unknown": "trigger non segnalato dall'app"
+ "unknown": "trigger non segnalato dall'app",
+ "manual": "da «Sincronizza tutto»"
}
+ },
+ "freshness": {
+ "title": "Per tipo di dato",
+ "lastReceived": "Ultima ricezione {relative}",
+ "lastNewValue": "Ultimo nuovo valore {relative}",
+ "never": "Non ancora ricevuto"
}
},
"garminInfo": {
"title": "Garmin",
- "body": "Garmin non ha una connessione diretta qui. Collegalo tramite Apple Health (iOS) o Google Health Connect (Android). I punteggi proprietari di Garmin — Body Battery, Training Load, stato HRV, VO₂ max — non vengono condivisi da Garmin con nessuna delle due piattaforme.",
+ "body": "Garmin non ha una connessione diretta qui. Collegalo tramite Apple Health (iOS) o Google Health Connect (Android). I punteggi proprietari di Garmin (Body Battery, Training Load, stato HRV, VO₂ max) non vengono condivisi da Garmin con nessuna delle due piattaforme.",
"docsLink": "Leggi la guida"
},
"glucoseReference": {
@@ -6923,6 +7170,12 @@
"description": "Questa azione condivide o esporta i tuoi dati sanitari. Conferma che sei tu per continuare.",
"signInAgain": "Accedi di nuovo, poi riprova. Il tuo account non ha una password o una passkey da confermare qui.",
"failed": "Non ha funzionato. Controlla quanto inserito e riprova."
+ },
+ "passwordTitle": "Password",
+ "tokenNameLabel": "Nome del token",
+ "displayCard": {
+ "title": "Visualizzazione",
+ "description": "Lingua, unità e formati, salvati appena li modifichi."
}
},
"admin": {
@@ -6955,14 +7208,14 @@
"aiServerKey": {
"title": "Chiave IA del server",
"description": "Una chiave dell'operatore per gli account che non ne hanno una propria.",
- "byokHint": "La chiave personale dell'utente (BYOK, accesso ChatGPT o provider locale) ha sempre la precedenza — la chiave del server è l'ultimo passo della catena dei provider.",
+ "byokHint": "La chiave personale dell'utente (BYOK, accesso ChatGPT o provider locale) ha sempre la precedenza: la chiave del server è l'ultimo passo della catena dei provider.",
"consentHint": "Le richieste tramite la chiave del server richiedono il consenso IA attivo dell'utente; senza una ricevuta di consenso non viene inviato nulla.",
"baseUrlHint": "L'URL di base determina l'API: un URL Anthropic come https://api.anthropic.com/v1 viene riconosciuto, qualsiasi altro host viene chiamato tramite l'API compatibile con OpenAI.",
"statusConfigured": "Configurata",
"statusNotConfigured": "Non configurata",
"keyLabel": "Chiave API",
"keyPlaceholder": "sk-...",
- "keyPlaceholderSet": "Salvata ({preview}) — inserisci una nuova chiave per sostituirla",
+ "keyPlaceholderSet": "Salvata ({preview}). Inserisci una nuova chiave per sostituirla",
"modelLabel": "Modello",
"baseUrlLabel": "URL di base",
"save": "Salva",
@@ -6996,7 +7249,10 @@
"typeCodex": "Accesso ChatGPT (personale)",
"typeLocal": "Modello locale (personale)",
"typeGateway": "Gateway compatibile OpenAI (personale)",
- "spendToday": "Oggi ({date}): {total} token, di cui {operator} sulla chiave dell'operatore."
+ "spendToday": "Oggi, secondo il giorno di calendario di ciascun account: {total} token, di cui {operator} sulla chiave dell'operatore (account con consumo: {accounts}).",
+ "statusPaused": "In pausa",
+ "pausedHint": "In pausa dopo errori ripetuti per {count} utenti fino al {at}. Poi viene ritentato automaticamente.",
+ "modelNotServed": "L’endpoint non offre più il modello {model}. Scegli nelle impostazioni IA un modello che offre."
},
"aiQuality": {
"title": "Qualità delle analisi",
@@ -7040,9 +7296,9 @@
"defaultLanguage": "Lingua predefinita",
"defaultLanguageDescription": "Usata per i nuovi utenti e come ripiego.",
"defaultUserTimezone": "Fuso orario predefinito per i nuovi account",
- "defaultUserTimezoneDescription": "Usato per i nuovi account privi di un fuso rilevato dal browser. Non influisce sugli utenti esistenti.",
+ "defaultUserTimezoneDescription": "Usato per i nuovi account senza fuso rilevato dal browser; gli account esistenti mantengono il proprio.",
"defaultUserTimezoneFallback": "Europe/Berlin (predefinito)",
- "servicesGlobal": "Servizi (globale)",
+ "servicesGlobal": "Canali e API",
"servicesGlobalDescription": "I servizi disattivati non sono disponibili per alcun utente.",
"telegramGlobal": "Consentire le notifiche Telegram.",
"ntfyGlobal": "Consentire le notifiche ntfy.",
@@ -7050,19 +7306,21 @@
"integrationWebPush": "Web Push",
"offlineGeoLabel": "Dati geografici",
"offlineGeoEnabled": "Offline (GeoLite2)",
- "offlineGeoFallback": "Online ({host}) — monta i database GeoLite2 e imposta GEOLITE2_DIR",
+ "offlineGeoFallback": "Online ({host}): monta i database GeoLite2 e imposta GEOLITE2_DIR",
+ "offlineGeoOff": "Disattivato",
+ "offlineGeoChosen": "Online ({host})",
"apiGlobal": "Consentire endpoint API e token API.",
"umamiTitle": "Umami",
- "umamiDescription": "Analisi web con Umami",
+ "umamiDescription": "Analisi web con Umami.",
"glitchtipTitle": "GlitchTip",
- "glitchtipDescription": "Tracciamento degli errori con GlitchTip",
+ "glitchtipDescription": "Tracciamento degli errori con GlitchTip.",
"webPushVapidTitle": "Web Push VAPID",
- "umamiEnabled": "Abilitare il tracciamento Umami.",
+ "umamiEnabled": "Abilita il tracciamento Umami",
"umamiScriptUrl": "URL dello script Umami",
"umamiScriptUrlPlaceholder": "https://analytics.example.com/script.js",
"umamiWebsiteId": "ID del sito web Umami",
"umamiWebsiteIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "glitchtipEnabled": "Abilitare il tracciamento degli errori GlitchTip.",
+ "glitchtipEnabled": "Abilita il tracciamento degli errori GlitchTip",
"glitchtipDsn": "GlitchTip DSN",
"glitchtipDsnPlaceholder": "https://@glitchtip.example.com/",
"glitchtipEnvironment": "Ambiente GlitchTip",
@@ -7113,18 +7371,23 @@
"documentAi": {
"title": "Lettura dei documenti",
"description": "Lettura con IA dei documenti salvati, scansione dei referti di laboratorio ed estrazione dei farmaci dal testo. La quota maggiore dei dati che lasciano il server."
+ },
+ "reasoning": {
+ "enabled": "Consenti ragionamento approfondito",
+ "max": "Profondità massima di ragionamento",
+ "description": "I modelli riflettono prima di rispondere: il Coach, il briefing giornaliero e i riepiloghi di periodo. Più precisi, usano più token."
}
},
"modules": {
"title": "Disponibilità dei moduli",
"description": "Disattiva un modulo per l'intero server.",
"saveError": "Impossibile aggiornare la disponibilità dei moduli.",
- "detail": "Un modulo disattivato qui è spento per ogni account, indipendentemente dalle preferenze personali. I domini principali (peso, pressione, polso, farmaci) sono sempre attivi e non possono essere disattivati.",
- "coachFollowsSwitch": "Segue l'interruttore Coach in Assistente."
+ "detail": "Peso, pressione e polso sono misure di base e restano sempre attivi.",
+ "coachFollowsSwitch": "Segue l'interruttore Coach nella sezione Coach."
},
"invites": {
- "title": "Inviti",
- "description": "Link di invito per la registrazione: funzionano anche quando la registrazione è chiusa.",
+ "title": "Link di invito",
+ "description": "Ogni link consente una registrazione anche quando la registrazione è chiusa.",
"create": "Crea invito",
"createTitle": "Crea invito",
"createDescription": "Il link consente una registrazione anche quando la registrazione è chiusa.",
@@ -7170,8 +7433,8 @@
"revokedToast": "Invito revocato.",
"revokeError": "Impossibile revocare l'invito."
},
- "userManagement": "Gestione utenti",
- "userManagementDescription": "Gestisci gli account: ruoli, reset delle password e disconnessioni forzate.",
+ "userManagement": "Account",
+ "userManagementDescription": "Ruoli, reset delle password e disconnessioni forzate per ogni account.",
"userName": "Nome",
"userEmail": "E-mail",
"userRole": "Ruolo",
@@ -7205,7 +7468,6 @@
"tokenNeverUsed": "Mai",
"tokensEmptyTitle": "Ancora nessun token API",
"tokensEmptyDescription": "I token vengono emessi quando un client nativo accede o tramite Impostazioni → Account.",
- "loginOverview": "Panoramica degli accessi",
"loginOverviewDescription": "Eventi di accesso e autenticazione di tutti gli account, filtrabili ed esportabili in CSV.",
"allAuthEvents": "Tutti gli eventi di autenticazione",
"failedOnly": "Solo non riusciti",
@@ -7238,6 +7500,7 @@
"finalDelete": "Elimina definitivamente",
"deletedResult": "Eliminate {rows} righe in {tables} tabelle. Account utente e credenziali di accesso conservati.",
"authRegister": "Registrazione",
+ "authClaim": "Profilo preso in carico",
"authLogin": "Accedi",
"authLoginPasskey": "Accesso con passkey",
"authLoginPassword": "Accesso con password",
@@ -7312,7 +7575,7 @@
},
"services": {
"title": "Servizi",
- "subtitle": "Abilita o disabilita i canali di notifica e le integrazioni a livello di sistema."
+ "subtitle": "Attiva o disattiva i canali di notifica e l'API per l'intero server."
},
"integrations": {
"title": "Integrazioni",
@@ -7398,7 +7661,8 @@
"refresh": "Aggiorna",
"eventDetails": "Dettagli dell'evento",
"closeDetails": "Chiudi dettagli",
- "processDetail": "Il buffer è volatile e limitato a 500 voci."
+ "processDetail": "Il buffer è volatile e limitato a 500 voci.",
+ "cardTitle": "Eventi recenti"
},
"backups": {
"title": "Backup",
@@ -7425,6 +7689,12 @@
"scheduleFailedDescription": "{when}: {reason}",
"scheduleFailedNoReason": "nessun motivo registrato",
"scheduleRemedy": "Avvia subito un backup e verifica che compaia una riga nuova. Se fallisce di nuovo, il log del worker indica l'account su cui si è fermato.",
+ "activity": {
+ "running": "In corso dal {when}.",
+ "interruptedTitle": "Un'esecuzione si è interrotta a metà e non è più ripartita",
+ "interruptedAccount": "{username}: iniziata {when}, mai terminata",
+ "interruptedRemedy": "Molto probabilmente l'app si è riavviata o ha esaurito la memoria mentre scriveva questa copia. La prossima esecuzione prende questi account per ultimi, così tutti gli altri ricevono prima la loro copia. Se succede di nuovo, assegna più memoria al container."
+ },
"download": "Scarica",
"downloadAria": "Scarica il backup di {username}",
"downloadFailed": "Download non riuscito",
@@ -7444,7 +7714,7 @@
"restorePromptLabel": "Digita RESTORE per continuare",
"previewTitle": "Contenuto del backup",
"previewLoading": "Lettura del contenuto…",
- "previewUnavailable": "Impossibile leggere il contenuto — il ripristino resta possibile.",
+ "previewUnavailable": "Impossibile leggere il contenuto, ma il ripristino resta possibile.",
"previewStillReading": "Questo backup è stato creato da una versione precedente, quindi il suo contenuto viene letto per intero, e con un archivio grande ci vuole un po'. Riapri questa finestra tra qualche minuto per vederlo. Il ripristino non dipende da questo.",
"previewEmpty": "Il file non contiene record.",
"previewOther": "Altri record",
@@ -7487,7 +7757,7 @@
"restoreSkippedMedicationTarget": "Analita obiettivo di un farmaco",
"restoreSkippedScheduleRevisionLink": "Successione dei periodi di schema",
"restoreSkippedAccountSetting": "Impostazione dell'account",
- "restoreMissingTitle": "Ripristino rifiutato — il file è incompleto",
+ "restoreMissingTitle": "Ripristino rifiutato: il file è incompleto",
"restoreMissingDescription": "Il manifesto del file dichiara di contenere la sezione qui sotto, e non la contiene. Non è stato modificato nulla: un ripristino sostituisce un'intera classe di record, quindi una sezione assente l'avrebbe svuotata.",
"restoreMissingRemedy": "Usa uno snapshot di disaster recovery, oppure esporta di nuovo l'account e carica il file nuovo. Una sezione che il manifesto dichiara omessa è un'altra cosa e si ripristina normalmente.",
"restoreMissingDocuments": "Documenti",
@@ -7555,7 +7825,12 @@
"restoreFailureKeyMissing": "Il backup richiede la chiave di cifratura {keys}, con cui questo server non riesce ad aprirlo. Aggiungi di nuovo la chiave a ENCRYPTION_KEYS, riavvia e avvia di nuovo il ripristino.",
"restoreFailureForeignReference": "Il file rimanda a voci che appartengono a un altro account. Non è stato scritto nulla.",
"previewKeyMissing": "Questo backup non può essere ripristinato su questo server: richiede la chiave di cifratura {keys}. Aggiungi prima di nuovo la chiave a ENCRYPTION_KEYS e riavvia.",
- "restoreSkippedSymptomEpisodeReference": "Voce di sintomo collegata a una malattia assente dal file (mantenuta senza collegamento)"
+ "restoreSkippedSymptomEpisodeReference": "Voce di sintomo collegata a una malattia assente dal file (mantenuta senza collegamento)",
+ "restoreSkippedCycleCiphertext": "Nota o etichetta del ciclo cifrata con una chiave che questo server non possiede (giorno o sintomo mantenuto senza)",
+ "restoreSkippedMoodLabelCiphertext": "Nome di tag o gruppo dell’umore cifrato con una chiave che questo server non possiede (mantenuto senza nome)",
+ "restoreSkippedVisitCiphertext": "Testo di visita o professionista cifrato con una chiave che questo server non possiede (mantenuto senza)",
+ "restoreSkippedVaccinationCiphertext": "Nota di vaccinazione cifrata con una chiave che questo server non possiede (dose mantenuta senza)",
+ "restoreSkippedEnvironmentLocationCiphertext": "Luogo di un periodo o giorno ambientale cifrato con una chiave che questo server non possiede (mantenuto cifrato, quei giorni non vengono aggiornati)"
},
"danger-zone": {
"title": "Zona pericolosa",
@@ -7572,7 +7847,6 @@
"encryption": {
"title": "Crittografia",
"subtitle": "Copertura della crittografia e stato della rotazione delle chiavi per i dati a riposo.",
- "loading": "Caricamento dello stato della crittografia...",
"loadError": "Impossibile caricare lo stato della crittografia.",
"coverageTitle": "Copertura",
"coverageDescription": "Quante righe cifrate stanno sotto ogni id di chiave.",
@@ -7617,12 +7891,45 @@
"backupsNone": "Nessun backup ha ancora registrato le chiavi di cui ha bisogno.",
"backupsStillNeed": "Tutte le righe sono ruotate, ma i backup hanno ancora bisogno di {keys}",
"backupsUnrecordedBadge": "Tutte le righe sono ruotate, ma backup più vecchi potrebbero ancora richiedere una chiave ritirata",
- "backupsDetail": "La rotazione cifra di nuovo ogni riga, ma un backup conserva la chiave con cui è stato scritto il suo contenuto, e ripristinarlo richiede quella chiave. Tieni una chiave ritirata in ENCRYPTION_KEYS finché compare qui sotto."
+ "backupsDetail": "La rotazione cifra di nuovo ogni riga, ma un backup conserva la chiave con cui è stato scritto il suo contenuto, e ripristinarlo richiede quella chiave. Tieni una chiave ritirata in ENCRYPTION_KEYS finché compare qui sotto.",
+ "columnsShow": "Mostra la tabella ({count} con righe)",
+ "columnsHide": "Nascondi la tabella",
+ "columnsShowAll": "Mostra tutte le {count} colonne",
+ "columnsShowWithRows": "Solo colonne con righe",
+ "columnsNoneWithRows": "Nessuna colonna cifrata contiene ancora righe."
+ },
+ "maintenance": {
+ "title": "Manutenzione della tabella delle misurazioni",
+ "description": "Libera lo spazio occupato dai vecchi residui di sincronizzazione, una volta terminata la pulizia.",
+ "body": "Esegue un vacuum della tabella delle misurazioni e poi ricostruisce i suoi indici, uno alla volta. Lettura e salvataggio continuano a funzionare durante l'esecuzione. Scegli un'ora tranquilla e lascia spazio libero su disco per un'altra copia dell'indice più grande.",
+ "tableSize": "Tabella con indici",
+ "indexSize": "Indici",
+ "loading": "Verifica dello stato della manutenzione…",
+ "loadError": "Impossibile leggere lo stato della manutenzione.",
+ "unavailable": "Nessun processo in background è collegato, quindi la manutenzione non può essere eseguita qui.",
+ "purgePending": "La pulizia dei vecchi residui di sincronizzazione è ancora in corso. La manutenzione potrà partire quando sarà terminata.",
+ "state": {
+ "idle": "Non ancora eseguita.",
+ "queued": "In attesa del processo in background.",
+ "running": "In corso dalle {since}. Su un'istanza grande può richiedere un po' di tempo.",
+ "completed": "Terminata il {at}.",
+ "refusedPurge": "Non avviata, perché la pulizia era ancora in corso. Riavviala quando la pulizia sarà terminata.",
+ "alreadyRunning": "Non avviata, perché un'altra esecuzione era già in corso.",
+ "failed": "Interrotta il {at}. Il registro dell'app indica il passaggio e il runbook spiega cosa controllare."
+ },
+ "start": "Avvia manutenzione",
+ "confirmTitle": "Avviare la manutenzione adesso?",
+ "confirmBody": "Viene eseguita in background, un passaggio alla volta, e può richiedere un po' di tempo. Se oggi non hai ancora creato un backup, creane uno prima.",
+ "confirm": "Avvia",
+ "cancel": "Annulla",
+ "started": "Manutenzione avviata.",
+ "alreadyQueued": "Una manutenzione è già in coda o in corso.",
+ "startFailed": "Impossibile avviare la manutenzione."
}
},
"carrier": "Operatore",
"mfaRequired": "Richiedi un secondo fattore",
- "mfaRequiredDescription": "Ogni account senza un secondo fattore attivo viene indirizzato alla configurazione dopo l'accesso. Questo invita gli utenti web ad attivarlo, ma non blocca l'accesso diretto via API o nativo (Bearer) per gli account non ancora configurati.",
+ "mfaRequiredDescription": "Gli account senza secondo fattore vengono indirizzati alla configurazione dopo un accesso web; l'accesso via API o nativo (Bearer) resta aperto.",
"mfaEnforcedOn": "MFA attivo",
"mfaEnforcedOff": "MFA disattivato",
"mfaEnforcedOnHint": "Per questo utente è richiesto un secondo fattore. Clicca per non richiederlo più.",
@@ -7652,7 +7959,7 @@
"connectedSince": "Connesso dal {when}",
"connected": "Connessione IA condivisa collegata",
"disconnected": "Connessione IA condivisa rimossa",
- "error": "Qualcosa è andato storto — riprova"
+ "error": "Qualcosa è andato storto. Riprova"
},
"failingJobs": {
"title": "Processi in background",
@@ -7720,7 +8027,9 @@
"restartNote": "Riavvia HealthLog dopo la modifica.",
"lastResort": "Ultima risorsa, solo se sei certo che la chiave sia giusta e nessuna delle tre soluzioni vale: imposta {setting} e riavvia. HealthLog serve comunque e si limita a registrare il problema nel log. Così si disattiva un controllo di sicurezza: con una chiave sbagliata ogni valore cifrato fallisce e i nuovi dati vengono scritti con quella chiave.",
"codes": "Per chi gestisce il server: le richieste API rispondono 503 con il codice di errore {code}, e /api/health indica il motivo {reason}."
- }
+ },
+ "loginOverviewCardTitle": "Eventi di accesso",
+ "userColumn": "Utente"
},
"achievements": {
"title": "Obiettivi",
@@ -7736,7 +8045,9 @@
"noneUnlockedYet": "Nessun obiettivo ancora sbloccato.",
"pointsValue": "{points} punti",
"metricCount": "{count}",
- "metricDays": "{count} giorni",
+ "metricDaysOne": "{count} giorno",
+ "metricDaysFew": "{count} giorni",
+ "metricDaysOther": "{count} giorni",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -7759,14 +8070,6 @@
"title": "Leggenda delle assunzioni",
"description": "Registra 300 assunzioni di farmaci."
},
- "overIntake1": {
- "title": "Doppia dose",
- "description": "Assumi il farmaco almeno una volta in più del previsto."
- },
- "skippedIntake1": {
- "title": "Pausa presa",
- "description": "Salta almeno un’assunzione pianificata."
- },
"passkeyCreated1": {
"title": "Passkey configurata",
"description": "Crea la tua prima passkey."
@@ -7792,19 +8095,19 @@
"description": "Assumi tutti i farmaci in orario per 1 giorno."
},
"compliance801": {
- "title": "Aderenza 80% · 1g",
+ "title": "Aderenza all'80% per 1 giorno",
"description": "Raggiungi almeno l’80% di aderenza su 30 giorni per 1 giorno."
},
"bmiGreen1": {
- "title": "IMC nel verde · 1g",
+ "title": "IMC nel verde per 1 giorno",
"description": "Mantieni l’IMC nell’intervallo normale per 1 giorno."
},
"bpGreen1": {
- "title": "Pressione nel verde · 1g",
+ "title": "Pressione nel verde per 1 giorno",
"description": "Mantieni la pressione nell’intervallo normale per 1 giorno."
},
"pulseGreen1": {
- "title": "Polso nel verde · 1g",
+ "title": "Polso nel verde per 1 giorno",
"description": "Mantieni il polso a riposo nell’intervallo normale per 1 giorno."
},
"onTimePerfect7": {
@@ -7812,19 +8115,19 @@
"description": "Assumi tutti i farmaci in orario per 7 giorni consecutivi."
},
"compliance807": {
- "title": "Aderenza 80% · 7g",
+ "title": "Aderenza all'80% per 7 giorni",
"description": "Raggiungi almeno l’80% di aderenza su 30 giorni per 7 giorni consecutivi."
},
"bmiGreen7": {
- "title": "IMC nel verde · 7g",
+ "title": "IMC nel verde per 7 giorni",
"description": "Mantieni l’IMC nell’intervallo normale per 7 giorni consecutivi."
},
"bpGreen7": {
- "title": "Pressione nel verde · 7g",
+ "title": "Pressione nel verde per 7 giorni",
"description": "Mantieni la pressione nell’intervallo normale per 7 giorni consecutivi."
},
"pulseGreen7": {
- "title": "Polso nel verde · 7g",
+ "title": "Polso nel verde per 7 giorni",
"description": "Mantieni il polso a riposo nell’intervallo normale per 7 giorni consecutivi."
},
"onTimePerfect30": {
@@ -7832,19 +8135,19 @@
"description": "Assumi tutti i farmaci in orario per 30 giorni consecutivi."
},
"compliance8030": {
- "title": "Aderenza 80% · 30g",
+ "title": "Aderenza all'80% per 30 giorni",
"description": "Raggiungi almeno l’80% di aderenza su 30 giorni per 30 giorni consecutivi."
},
"bmiGreen30": {
- "title": "IMC nel verde · 30g",
+ "title": "IMC nel verde per 30 giorni",
"description": "Mantieni l’IMC nell’intervallo normale per 30 giorni consecutivi."
},
"bpGreen30": {
- "title": "Pressione nel verde · 30g",
+ "title": "Pressione nel verde per 30 giorni",
"description": "Mantieni la pressione nell’intervallo normale per 30 giorni consecutivi."
},
"pulseGreen30": {
- "title": "Polso nel verde · 30g",
+ "title": "Polso nel verde per 30 giorni",
"description": "Mantieni il polso a riposo nell’intervallo normale per 30 giorni consecutivi."
},
"onTimePerfect180": {
@@ -7852,19 +8155,19 @@
"description": "Assumi tutti i farmaci in orario per 180 giorni consecutivi."
},
"compliance80180": {
- "title": "Aderenza 80% · 180g",
+ "title": "Aderenza all'80% per 180 giorni",
"description": "Raggiungi almeno l’80% di aderenza su 30 giorni per 180 giorni consecutivi."
},
"bmiGreen180": {
- "title": "IMC nel verde · 180g",
+ "title": "IMC nel verde per 180 giorni",
"description": "Mantieni l’IMC nell’intervallo normale per 180 giorni consecutivi."
},
"bpGreen180": {
- "title": "Pressione nel verde · 180g",
+ "title": "Pressione nel verde per 180 giorni",
"description": "Mantieni la pressione nell’intervallo normale per 180 giorni consecutivi."
},
"pulseGreen180": {
- "title": "Polso nel verde · 180g",
+ "title": "Polso nel verde per 180 giorni",
"description": "Mantieni il polso a riposo nell’intervallo normale per 180 giorni consecutivi."
},
"onTimePerfect360": {
@@ -7872,19 +8175,19 @@
"description": "Assumi tutti i farmaci in orario per 360 giorni consecutivi."
},
"compliance80360": {
- "title": "Aderenza 80% · 360g",
+ "title": "Aderenza all'80% per 360 giorni",
"description": "Raggiungi almeno l’80% di aderenza su 30 giorni per 360 giorni consecutivi."
},
"bmiGreen360": {
- "title": "IMC nel verde · 360g",
+ "title": "IMC nel verde per 360 giorni",
"description": "Mantieni l’IMC nell’intervallo normale per 360 giorni consecutivi."
},
"bpGreen360": {
- "title": "Pressione nel verde · 360g",
+ "title": "Pressione nel verde per 360 giorni",
"description": "Mantieni la pressione nell’intervallo normale per 360 giorni consecutivi."
},
"pulseGreen360": {
- "title": "Polso nel verde · 360g",
+ "title": "Polso nel verde per 360 giorni",
"description": "Mantieni il polso a riposo nell’intervallo normale per 360 giorni consecutivi."
},
"moodFirst": {
@@ -7968,15 +8271,15 @@
"description": "Hai cambiato la lingua dell’app almeno una volta."
},
"missFree7": {
- "title": "Nessuna dose persa · 7g",
+ "title": "Nessuna dose persa in 7 giorni",
"description": "7 giorni consecutivi senza una dose persa."
},
"missFree30": {
- "title": "Nessuna dose persa · 30g",
+ "title": "Nessuna dose persa in 30 giorni",
"description": "30 giorni consecutivi senza una dose persa."
},
"missFree90": {
- "title": "Nessuna dose persa · 90g",
+ "title": "Nessuna dose persa in 90 giorni",
"description": "90 giorni consecutivi senza una dose persa."
},
"measurementWeeks4": {
@@ -7985,10 +8288,10 @@
},
"selfContextComplete": {
"title": "Autodichiarazione aggiornata",
- "description": "Tutti e quattro i campi dell’autodichiarazione compilati — il Coach conosce il tuo contesto."
+ "description": "Tutti e quattro i campi dell’autodichiarazione compilati: il Coach conosce il tuo contesto."
},
"sleepLog7": {
- "title": "Diario del sonno · 7g",
+ "title": "Diario del sonno per 7 giorni",
"description": "Dati del sonno registrati per 7 giorni consecutivi."
}
},
@@ -8007,7 +8310,7 @@
},
"hiddenCard": {
"title": "Obiettivo nascosto",
- "description": "Continua a monitorare — potresti imbatterti in esso.",
+ "description": "Continua a monitorare: potresti imbatterti in esso.",
"ariaLabel": "Obiettivo nascosto bloccato"
},
"hiddenUnlockToast": {
@@ -8016,7 +8319,7 @@
"dashboardCard": {
"title": "Sbloccati di recente",
"viewAll": "Vedi tutti",
- "empty": "Ancora nessun obiettivo — continua a registrare per sbloccare il primo."
+ "empty": "Ancora nessun obiettivo. Continua a registrare per sbloccare il primo."
}
},
"notifications": {
@@ -8078,9 +8381,6 @@
"enabledToast": "Avvisi proattivi del Coach attivati.",
"disabledToast": "Avvisi proattivi del Coach disattivati.",
"saveError": "Impossibile salvare. Riprova più tardi.",
- "suggestionsLabel": "Suggerimenti di esempio",
- "suggestionsDesc": "Mostra di tanto in tanto una domanda di esempio pronta all'uso nella schermata del Coach.",
- "suggestionsAria": "Attiva o disattiva i suggerimenti di esempio del Coach",
"aiComposedLabel": "Lascia che sia il Coach a formulare il promemoria",
"aiComposedDesc": "Quando è disponibile un provider, il promemoria proattivo viene scritto dal modello anziché dal modello di testo fisso. Disattivato per impostazione predefinita.",
"aiComposedAria": "Attiva o disattiva il promemoria proattivo scritto dall'IA"
@@ -8151,7 +8451,46 @@
"loginRequired": "Accedi per gestire le notifiche.",
"someChannelsGloballyDisabled": "Uno o più canali sono disattivati nelle impostazioni e non riceveranno notifiche.",
"reminderSettingsLink": "Impostazioni promemoria",
- "channelsLink": "Canali"
+ "channelsLink": "Canali",
+ "handover": {
+ "claimedTitle": "{name} ora ha un proprio account",
+ "claimedEnd": "{name} ha preso in carico la propria cartella sanitaria. Il tuo accesso è terminato.",
+ "claimedRead": "{name} ha preso in carico la propria cartella sanitaria. Puoi ancora vederla, ma non più modificarla.",
+ "claimedManage": "{name} ha preso in carico la propria cartella sanitaria. Puoi ancora gestirla finché non deciderà altrimenti.",
+ "changedTitle": "{name} ha cambiato il tuo accesso",
+ "changedEnd": "Il tuo accesso alla cartella sanitaria di {name} è terminato.",
+ "changedRead": "Ora puoi vedere la cartella sanitaria di {name}, ma non modificarla.",
+ "changedManage": "Puoi gestire la cartella sanitaria di {name}.",
+ "unnamed": {
+ "claimedTitle": "La persona di cui ti occupavi ora ha un proprio account",
+ "claimedEnd": "La persona di cui ti occupavi ha preso in carico la propria cartella sanitaria. Il tuo accesso è terminato.",
+ "claimedRead": "La persona di cui ti occupavi ha preso in carico la propria cartella sanitaria. Puoi ancora consultarla, ma non più modificarla.",
+ "claimedManage": "La persona di cui ti occupavi ha preso in carico la propria cartella sanitaria. Puoi ancora gestirla finché non deciderà diversamente.",
+ "changedTitle": "Il tuo accesso a una cartella sanitaria è cambiato",
+ "changedEnd": "Il tuo accesso alla cartella sanitaria della persona di cui ti occupavi è terminato.",
+ "changedRead": "Ora puoi consultare la cartella sanitaria della persona di cui ti occupavi, ma non modificarla.",
+ "changedManage": "Puoi gestire la cartella sanitaria della persona di cui ti occupavi."
+ }
+ },
+ "links": {
+ "open": "Apri in HealthLog"
+ },
+ "quickActions": {
+ "moodGood": "Bene",
+ "moodOkay": "Così così",
+ "moodBad": "Male"
+ },
+ "content": {
+ "title": "Valori di salute nelle notifiche",
+ "description": "Se una notifica può mostrare valori come il tuo punteggio del mattino.",
+ "detail": "Disattivato, una notifica dice solo di cosa si tratta, quindi la schermata di blocco non mostra valori di salute. Attivato, aggiunge i valori, per esempio il punteggio del mattino nel riepilogo. Toccando una notifica si apre il punto giusto dell'app in entrambi i casi.",
+ "toggleAria": "Mostra i valori di salute nelle notifiche",
+ "statusDetailed": "Con valori",
+ "statusGeneric": "Senza valori",
+ "detailedToast": "Le notifiche ora includono valori di salute.",
+ "genericToast": "Le notifiche non includono più valori di salute.",
+ "saveError": "Impossibile salvare. Riprova più tardi."
+ }
},
"i18n": {
"maintainershipBanner": {
@@ -8182,7 +8521,7 @@
}
},
"edit": {
- "title": "Modifica range obiettivo — {metric}",
+ "title": "Modifica range obiettivo: {metric}",
"description": "Regola il range obiettivo usato in grafici, insights e referto medico. Valori in {unit}.",
"derivedHint": "Questa card è derivata da altre metriche. Modifica gli obiettivi sottostanti (es. pressione o peso).",
"minLabel": "Min",
@@ -8206,15 +8545,19 @@
"currentValue": "Attuale: {value} {unit}",
"targetRangeValue": "Range obiettivo: {min}–{max} {unit}",
"sourceLabel": "Fonte: {source}",
+ "sourceNames": {
+ "moodEntries": "Voci dell’umore",
+ "sevenDays": "7 giorni"
+ },
"inTarget": "Nel range obiettivo",
"aboveTarget": "{delta} {unit} sopra il range",
"belowTarget": "{delta} {unit} sotto il range",
"diastolic": "Diastolica",
- "glucoseFasting": "Glicemia — a digiuno",
- "glucosePostprandial": "Glicemia — postprandiale",
- "glucoseRandom": "Glicemia — qualsiasi",
- "glucoseBedtime": "Glicemia — prima di coricarsi",
- "glucoseUnspecified": "Glicemia — non indicata",
+ "glucoseFasting": "Glicemia (a digiuno)",
+ "glucosePostprandial": "Glicemia (postprandiale)",
+ "glucoseRandom": "Glicemia (qualsiasi)",
+ "glucoseBedtime": "Glicemia (prima di coricarsi)",
+ "glucoseUnspecified": "Glicemia (non indicata)",
"status": {
"underweight": "Sottopeso",
"normal": "Normal",
@@ -8300,7 +8643,7 @@
"askMedicationName": "Indica il farmaco, es. preso Ramipril.",
"medicationNotFoundExact": "Farmaco non trovato. Invia il nome esattamente come appare in HealthLog.",
"helpHeader": "Comandi disponibili:",
- "helpBody": "/help — mostra questa guida\n/add — registra un'assunzione\n/start — avvia il bot\n\nComandi testuali:\ntaken <nome> — conferma un'assunzione\n\nPulsanti nei promemoria:\n• Assunto — conferma l'assunzione\n• 🕐 1h / 🕐 3h — posticipa il promemoria\n• ⏭ Salta — salta l'assunzione\n• ✓ Conferma — conferma un'assunzione saltata",
+ "helpBody": "/help: mostra questa guida\n/add: registra un'assunzione\n/start: avvia il bot\n\nComandi testuali:\ntaken <nome>: conferma un'assunzione\n\nPulsanti nei promemoria:\n• Assunto: conferma l'assunzione\n• 🕐 1h / 🕐 3h: posticipa il promemoria\n• ⏭ Salta: salta l'assunzione\n• ✓ Conferma: conferma un'assunzione saltata",
"measureValuePrompt": "Rispondi con il valore per registrarlo (facoltativo).",
"buttonMeasureValue": "Valore…",
"measureValueSaved": "Misurazione salvata.",
@@ -8325,9 +8668,9 @@
},
"lowStockReminders": {
"title": "Scorte in esaurimento: {medName}",
- "bodyToday": "{medName}: {units} unità rimaste — meno di un giorno di scorta.",
- "bodyReorder": "{medName}: {units} unità rimaste. Le scorte finiscono intorno al {runsOutOn} — riordina entro il {reorderBy}.",
- "bodyLastDose": "{medName}: {units} unità rimaste — circa la tua ultima dose. Le scorte finiscono intorno al {runsOutOn}.",
+ "bodyToday": "{medName}: {units} unità rimaste, meno di un giorno di scorta.",
+ "bodyReorder": "{medName}: {units} unità rimaste. Le scorte finiscono intorno al {runsOutOn}: riordina entro il {reorderBy}.",
+ "bodyLastDose": "{medName}: {units} unità rimaste, circa la tua ultima dose. Le scorte finiscono intorno al {runsOutOn}.",
"bodyExpired": "{medName}: nessuna scorta utilizzabile. {units} unità sono contrassegnate come scadute.",
"bodyAsNeeded": "{medName}: restano solo {units} unità. Riordina prima di aver bisogno della prossima dose."
},
@@ -8354,11 +8697,11 @@
"questionConditions": "Hai malattie croniche che il Coach dovrebbe conoscere?",
"questionFocus": "C'è qualcosa di specifico a cui il Coach dovrebbe fare attenzione?",
"questionAllergies": "Hai allergie o intolleranze?",
- "questionAboutMe": "Vuoi raccontare al Coach qualcosa di te — abitudini, obiettivi, contesto?"
+ "questionAboutMe": "Vuoi raccontare al Coach qualcosa di te? Abitudini, obiettivi, contesto?"
},
"clinicianView": {
"title": "Cartella sanitaria condivisa",
- "period": "Periodo del report: {start} – {end}",
+ "period": "Periodo del report: {start}–{end}",
"expires": "Questo link scade il {date}.",
"provenance": "Questo è un riepilogo di sola lettura condiviso dalla persona a cui appartiene, esportato dai suoi dati HealthLog autoregistrati. I valori sono autodichiarati e possono includere letture sincronizzate da dispositivi; vanno considerati come riferiti dal paziente, non come un esame clinico.",
"labs": "Valori di laboratorio",
@@ -8382,7 +8725,7 @@
"strain": "Punteggio di sforzo",
"score": "Punteggio di benessere"
},
- "footer": "Condiviso da HealthLog — un tracker della salute self-hosted.",
+ "footer": "Condiviso da HealthLog, un tracker della salute self-hosted.",
"unlock": {
"title": "Inserisci la passphrase",
"description": "Questo referto sanitario condiviso è protetto. Inserisci la passphrase che hai ricevuto per visualizzarlo.",
@@ -8417,7 +8760,11 @@
},
"course": "Ciclo {n}",
"courseRange": "dal {start} al {end}",
- "courseSince": "dal {start}"
+ "courseSince": "dal {start}",
+ "unavailable": {
+ "title": "Link scaduto o non valido",
+ "description": "Questo link di condivisione è scaduto, è stato revocato o è incompleto. Chiedi un nuovo link alla persona che lo ha condiviso."
+ }
},
"cycle": {
"symptomCategory": {
@@ -8470,7 +8817,7 @@
},
"crosstab": {
"description": "Come si confronta ogni misurazione tra la fase luteale e quella follicolare, sui tuoi dati.",
- "pairLabel": "{metric} · luteale vs follicolare",
+ "pairLabel": "{metric}: fase luteale vs follicolare",
"detail": "Luteale {lutealAvg} {unit} ({lutealDays} giorni) vs follicolare {follicularAvg} {unit} ({follicularDays} giorni).",
"metricRestingHeartRate": "Frequenza cardiaca a riposo",
"metricHrv": "Variabilità della frequenza cardiaca",
@@ -8593,8 +8940,8 @@
"ariaUnknown": "Nessun ciclo attivo ancora",
"caption": "del tuo ciclo",
"firstPeriodCta": "Registra il mio primo ciclo",
- "periodOverdue": "Il ciclo è in ritardo rispetto al solito — il conteggio dei giorni resta in pausa finché non registri la prossima mestruazione.",
- "periodOverdueDays": "Il ciclo è in ritardo di {days} giorni rispetto al solito — il conteggio dei giorni resta in pausa finché non registri la prossima mestruazione.",
+ "periodOverdue": "Il ciclo è in ritardo rispetto al solito: il conteggio dei giorni resta in pausa finché non registri la prossima mestruazione.",
+ "periodOverdueDays": "Il ciclo è in ritardo di {days} giorni rispetto al solito: il conteggio dei giorni resta in pausa finché non registri la prossima mestruazione.",
"logPeriodCta": "Registra mestruazione"
},
"calendar": {
@@ -8619,7 +8966,7 @@
"predictions": {
"title": "Cosa arriva dopo",
"nextPeriod": "Prossimo ciclo",
- "nextPeriodWindow": "{low} – {high}",
+ "nextPeriodWindow": "{low}–{high}",
"fertileWindow": "Finestra fertile",
"ovulation": "Ovulazione stimata",
"confidenceHigh": "Alta",
@@ -8628,7 +8975,7 @@
"stillLearning": "Sto ancora imparando",
"stillLearningBody": "Registra almeno tre cicli completi e le previsioni miglioreranno. Fino ad allora è una stima approssimativa.",
"rawMode": "Previsione disattivata",
- "rawModeBody": "Hai disattivato le previsioni — vengono mostrati solo i tuoi dati registrati, senza previsione.",
+ "rawModeBody": "Hai disattivato le previsioni: vengono mostrati solo i tuoi dati registrati, senza previsione.",
"none": "Nessuna previsione ancora. Registra un ciclo per iniziare.",
"ovulationConfirmed": "Ovulazione (confermata)"
},
@@ -8690,7 +9037,7 @@
"temperatureSection": "Temperatura e ovulazione",
"intimacySection": "Intimità e contraccezione",
"testsSection": "Test",
- "phaseDayHeader": "Giorno {day} · {phase}",
+ "phaseDayHeader": "Giorno {day}, {phase}",
"bbtHint": "Misurala appena sveglia, prima di alzarti.",
"titleForDate": "Registrazione del {date}",
"descriptionToday": "Oggi. Flusso, sintomi e note.",
@@ -8757,15 +9104,15 @@
"fieldInfo": {
"bbt": "La tua temperatura a riposo, misurata appena sveglia prima di alzarti. Dopo l'ovulazione sale leggermente (circa 0,2–0,3 °C).",
"bbtLabel": "Cos'è la temperatura basale?",
- "mucus": "Le perdite vaginali cambiano durante il ciclo — da secche o appiccicose a cremose, poi acquose e simili ad albume nei giorni più fertili.",
+ "mucus": "Le perdite vaginali cambiano durante il ciclo: da secche o appiccicose a cremose, poi acquose e simili ad albume nei giorni più fertili.",
"mucusLabel": "Cos'è il muco cervicale?",
- "ovulationTest": "Un test delle urine domestico che rileva il picco di LH — l'aumento ormonale circa 24–36 h prima dell'ovulazione. Un positivo segnala la finestra più fertile.",
+ "ovulationTest": "Un test delle urine domestico che rileva il picco di LH, l'aumento ormonale circa 24–36 h prima dell'ovulazione. Un positivo segnala la finestra più fertile.",
"ovulationTestLabel": "Cos'è un test di ovulazione?",
"spotting": "Lieve sanguinamento al di fuori delle mestruazioni.",
"spottingLabel": "Cos'è lo spotting tra le mestruazioni?",
"pregnancyTest": "Risultato di un test di gravidanza (hCG) domestico.",
"pregnancyTestLabel": "Cos'è un test di gravidanza?",
- "progesteroneTest": "Un test che conferma l'avvenuta ovulazione — il progesterone sale dopo.",
+ "progesteroneTest": "Un test che conferma l'avvenuta ovulazione: il progesterone sale dopo.",
"progesteroneTestLabel": "Cos'è un test del progesterone?",
"intercourse": "Se in questo giorno ci sono stati rapporti sessuali.",
"intercourseLabel": "Cosa registra i rapporti?",
@@ -8818,14 +9165,14 @@
"postpone": {
"menuItem": "Rimanda…",
"title": "Rimanda la scadenza",
- "description": "Sposta solo la scadenza attuale — il ritmo regolare resta invariato.",
+ "description": "Sposta solo la scadenza attuale. Il ritmo regolare resta invariato.",
"chip": "+{days} giorni",
"dateLabel": "Nuova data",
"confirm": "Rimanda"
},
"skip": {
"heading": "Salta questa scadenza",
- "explainer": "Registrato onestamente come saltato — non come fatto. L'intervallo riparte da oggi.",
+ "explainer": "Registrato onestamente come saltato, non come fatto. L'intervallo riparte da oggi.",
"action": "Salta",
"confirmTitle": "Saltare questa scadenza?"
},
@@ -8867,10 +9214,10 @@
"VISCERAL_FAT": "Grasso viscerale",
"BODY_MASS_INDEX": "Indice di massa corporea (IMC)",
"WAIST_CIRCUMFERENCE": "Circonferenza vita",
- "PHQ9_SCORE": "PHQ-9 · umore",
- "GAD7_SCORE": "GAD-7 · preoccupazione",
- "WHO5_SCORE": "WHO-5 · benessere",
- "SCI_SCORE": "SCI · sonno"
+ "PHQ9_SCORE": "PHQ-9 (umore)",
+ "GAD7_SCORE": "GAD-7 (preoccupazione)",
+ "WHO5_SCORE": "WHO-5 (benessere)",
+ "SCI_SCORE": "SCI (sonno)"
},
"typeGroups": {
"vitals": "Parametri vitali",
@@ -8966,7 +9313,7 @@
"localOcr": {
"heading": "Leggi le scansioni su questo dispositivo",
"description": "Legge la foto sul tuo dispositivo e invia al provider solo il testo estratto.",
- "detail": "Utile quando il tuo provider IA non legge le immagini. Meno preciso — controlla ogni valore prima di salvare."
+ "detail": "Utile quando il tuo provider IA non legge le immagini. Meno preciso: controlla ogni valore prima di salvare."
},
"sort": {
"heading": "Ordinamento",
@@ -9028,7 +9375,7 @@
"emptyDescription": "Aggiungi un valore per seguire questo biomarcatore nel tempo.",
"title": "Biomarcatore",
"genericDescription": "Un biomarcatore di laboratorio che monitori. Aggiungi misurazioni per seguirne l'andamento rispetto all'intervallo di riferimento.",
- "valuesTitle": "{marker} — tutte le letture",
+ "valuesTitle": "{marker}: tutte le letture",
"valuesDescription": "Ogni lettura di questo biomarcatore. Modifica o rimuovi una voce direttamente.",
"backToMarker": "Torna a {marker}",
"latestResult": "Ultimo risultato"
@@ -9048,7 +9395,7 @@
},
"biomarker": {
"manageTitle": "Biomarcatori",
- "managerDescription": "Definisci un marcatore una volta — unità e intervallo valgono per ogni misurazione.",
+ "managerDescription": "Definisci un marcatore una volta: unità e intervallo valgono per ogni misurazione.",
"define": "Definisci",
"defineFirst": "Definisci il tuo primo biomarcatore",
"defineTitle": "Definisci biomarcatore",
@@ -9067,7 +9414,7 @@
"form": {
"seedLabel": "Parti da un marcatore comune",
"seedPlaceholder": "Scegli un marcatore (facoltativo)",
- "seedHint": "Sceglierne uno compila i campi — adatta l'intervallo al tuo laboratorio.",
+ "seedHint": "Sceglierne uno compila i campi. Adatta l'intervallo al tuo laboratorio.",
"name": "Nome",
"namePlaceholder": "es. Colesterolo LDL",
"unit": "Unità",
@@ -9081,7 +9428,7 @@
"upperPlaceholder": "Limite superiore",
"context": "Contesto",
"contextPlaceholder": "Nota facoltativa su questo marcatore",
- "rangeHint": "Entrambi i limiti sono facoltativi — lasciane uno vuoto per un intervallo aperto (es. LDL < 116).",
+ "rangeHint": "Entrambi i limiti sono facoltativi: lasciane uno vuoto per un intervallo aperto (es. LDL < 116).",
"requiredError": "Nome e unità sono obbligatori.",
"rangeOrderError": "Il limite inferiore non deve superare quello superiore.",
"save": "Salva biomarcatore",
@@ -9147,16 +9494,16 @@
"ft3": "Il T3 libero (triiodotironina) è la forma non legata e biologicamente attiva dell'ormone tiroideo più potente, la frazione disponibile ad agire sui tessuti. La maggior parte del T3 circolante è legata a proteine di trasporto e inattiva, perciò la quota libera riflette la reale disponibilità dell'ormone. Si misura per caratterizzare la funzione tiroidea, soprattutto insieme a TSH e T4 libero quando i risultati non sono chiari. I valori dipendono molto dal metodo di analisi e si leggono nel loro contesto clinico.",
"ft4": "Il T4 libero (tiroxina) è la forma non legata del principale ormone che la tiroide rilascia nel sangue e funge da riserva che i tessuti convertono nel T3 più attivo secondo necessità. La frazione libera riflette ciò che è realmente disponibile per il corpo, indipendentemente dal livello delle proteine di trasporto. Di solito si misura insieme al TSH per distinguere i problemi della tiroide da quelli della segnalazione ipofisaria. La gravidanza e alcuni farmaci possono spostarlo.",
"ferritin": "La ferritina è la principale proteina di deposito del ferro dell'organismo e il suo valore nel sangue rispecchia a grandi linee le riserve totali di ferro. Un valore basso indica riserve esaurite, mentre uno alto può riflettere un sovraccarico di ferro, oppure semplicemente un'infiammazione, perché la ferritina sale anche come reattante della fase acuta. Per questo si interpreta spesso insieme ad altri marcatori del ferro e a segni di infiammazione. È una delle misure centrali per valutare lo stato del ferro.",
- "transferrin-saturation": "La saturazione della transferrina è la percentuale di transferrina — la proteina che trasporta il ferro nel sangue — attualmente carica di ferro. Collega la quantità di ferro in transito alla capacità di trasporto dell'organismo, offrendo un'istantanea della disponibilità di ferro che completa la visione delle riserve data dalla ferritina. Una saturazione bassa suggerisce un ferro scarso, mentre un valore alto può indicare un sovraccarico. Si legge insieme alla ferritina e all'assetto marziale completo.",
+ "transferrin-saturation": "La saturazione della transferrina è la percentuale di transferrina (la proteina che trasporta il ferro nel sangue) attualmente carica di ferro. Collega la quantità di ferro in transito alla capacità di trasporto dell'organismo, offrendo un'istantanea della disponibilità di ferro che completa la visione delle riserve data dalla ferritina. Una saturazione bassa suggerisce un ferro scarso, mentre un valore alto può indicare un sovraccarico. Si legge insieme alla ferritina e all'assetto marziale completo.",
"vitamin-d": "La vitamina D si misura come 25-idrossivitamina D, la forma di deposito circolante e il marcatore standard dello stato complessivo di vitamina D. Il corpo la produce nella pelle esposta alla luce solare e ne assorbe una parte dagli alimenti; sostiene l'equilibrio del calcio, la salute delle ossa e la funzione immunitaria. I valori variano con la stagione, la latitudine, il tono della pelle e l'integrazione. Viene rilevata per stimare nel tempo se le riserve sono adeguate.",
- "vitamin-b12": "La vitamina B12 (cobalamina) è una vitamina essenziale, ottenuta soprattutto da alimenti di origine animale e immagazzinata in gran parte nel fegato. È necessaria per la formazione dei globuli rossi, la sintesi del DNA e la salute del sistema nervoso. Una carenza prolungata — da scarso apporto o assorbimento alterato — può portare ad anemia e a sintomi neurologici. Si misura per valutare lo stato nutrizionale, in particolare quando apporto o assorbimento possono essere limitati.",
+ "vitamin-b12": "La vitamina B12 (cobalamina) è una vitamina essenziale, ottenuta soprattutto da alimenti di origine animale e immagazzinata in gran parte nel fegato. È necessaria per la formazione dei globuli rossi, la sintesi del DNA e la salute del sistema nervoso. Una carenza prolungata, da scarso apporto o assorbimento alterato, può portare ad anemia e a sintomi neurologici. Si misura per valutare lo stato nutrizionale, in particolare quando apporto o assorbimento possono essere limitati.",
"folate": "Il folato (vitamina B9) è una vitamina del gruppo B presente nelle verdure a foglia verde, nei legumi e negli alimenti fortificati, centrale per la formazione dei globuli rossi e la sintesi del DNA. Lavora a stretto contatto con la vitamina B12 e una carenza dell'uno o dell'altra può produrre un tipo simile di anemia. Un folato adeguato è particolarmente importante intorno all'inizio della gravidanza per uno sviluppo sano del feto. Si misura per valutare lo stato nutrizionale e la causa di alcune anemie.",
"hs-crp": "La PCR ad alta sensibilità misura la proteina C-reattiva, un marcatore che il fegato rilascia in risposta all'infiammazione, con una tecnica affinata per rilevare le basse concentrazioni rilevanti per il rischio cardiovascolare. A differenza della PCR standard usata per le infezioni acute, la versione ad alta sensibilità coglie l'infiammazione lieve e cronica legata alla malattia vascolare. Poiché qualsiasi infezione o lesione recente può aumentarla, si interpreta lontano da una malattia acuta e spesso si conferma con un prelievo ripetuto. Si legge come uno dei fattori del rischio cardiovascolare complessivo.",
"creatinine": "La creatinina è un prodotto di scarto del normale metabolismo muscolare che i reni filtrano dal sangue a un ritmo abbastanza costante. Poiché la produzione è relativamente stabile, il suo valore nel sangue funge da misura pratica di quanto bene i reni eliminano gli scarti. I valori dipendono anche dalla massa muscolare, dall'idratazione e dalla dieta, perciò si interpretano di solito insieme alla velocità di filtrazione stimata. È un marcatore centrale della funzione renale.",
"egfr": "L'eGFR (velocità di filtrazione glomerulare stimata) stima quanto sangue i reni filtrano ogni minuto, calcolata dalla creatinina insieme a età e sesso. Traduce un valore grezzo di creatinina in una misura della funzione renale più immediatamente interpretabile. Un eGFR in calo nel tempo è il modo principale per classificare e seguire le alterazioni renali croniche. Essendo una stima, si legge come una tendenza e non come un'unica cifra esatta.",
"alt": "L'ALT (alanina aminotransferasi) è un enzima concentrato nelle cellule del fegato, rilasciato nel sangue quando queste cellule sono sotto stress o danneggiate. Essendo relativamente specifico del fegato, è uno dei principali marcatori per rilevare un danno o un'infiammazione epatica. I valori possono salire con il fegato grasso, l'epatite virale, l'alcol o alcuni farmaci. Di solito si legge insieme all'AST e agli altri marcatori del profilo epatico.",
"ast": "L'AST (aspartato aminotransferasi) è un enzima presente nel fegato ma anche nel muscolo, nel cuore e nei globuli rossi, rilasciato quando uno di questi tessuti subisce un danno. Essendo meno specifico del fegato rispetto all'ALT, i due si interpretano insieme e il loro rapporto può orientare sull'origine e sulla natura del problema. Un esercizio intenso o una lesione muscolare possono aumentarlo indipendentemente dal fegato. È uno degli enzimi standard del profilo epatico.",
- "ggt": "La GGT (gamma-glutamil transferasi) è un enzima del fegato e delle vie biliari che sale in particolare quando il flusso della bile è ostruito o in caso di esposizione all'alcol. Spesso si usa per chiarire perché un altro marcatore epatico è elevato — per esempio, per distinguere un'origine epatica o biliare. È sensibile ma poco specifica, perciò si legge nel contesto del resto del profilo epatico. È uno dei vari marcatori usati per valutare la salute del fegato e delle vie biliari.",
+ "ggt": "La GGT (gamma-glutamil transferasi) è un enzima del fegato e delle vie biliari che sale in particolare quando il flusso della bile è ostruito o in caso di esposizione all'alcol. Spesso si usa per chiarire perché un altro marcatore epatico è elevato, per esempio per distinguere un'origine epatica o biliare. È sensibile ma poco specifica, perciò si legge nel contesto del resto del profilo epatico. È uno dei vari marcatori usati per valutare la salute del fegato e delle vie biliari.",
"sodium": "Il sodio è il principale elettrolita del liquido al di fuori delle cellule del corpo e il motore fondamentale dell'equilibrio dei liquidi e del volume del sangue. L'organismo lo regola in modo stretto tramite la sete, gli ormoni e i reni, perciò il suo valore nel sangue riflette più l'equilibrio tra acqua e sale che il solo apporto alimentare. Valori anomali segnalano di solito un disturbo dell'equilibrio idrico più che del sodio stesso. È una parte centrale del pannello metabolico di base e degli elettroliti.",
"potassium": "Il potassio è il principale elettrolita all'interno delle cellule del corpo ed è essenziale per la trasmissione nervosa e per il ritmo regolare del muscolo cardiaco. Il suo valore nel sangue è mantenuto entro un intervallo ristretto dai reni e dai segnali ormonali, perché sia i valori alti sia quelli bassi possono disturbare l'attività elettrica del cuore. I valori possono essere influenzati dalla funzione renale, dall'idratazione e da diversi farmaci comuni. È una misura centrale del pannello degli elettroliti.",
"hemoglobin": "L'emoglobina è la proteina contenente ferro dei globuli rossi che lega l'ossigeno nei polmoni e lo trasporta ai tessuti. La sua concentrazione è la principale misura della capacità del sangue di trasportare ossigeno e il marcatore centrale per rilevare l'anemia. Valori bassi possono riflettere perdita di sangue, carenza di ferro o di vitamine, o una produzione ridotta, mentre valori alti possono seguire una disidratazione o condizioni di scarso ossigeno. È un pilastro dell'emocromo completo.",
@@ -9165,8 +9512,8 @@
"platelets": "Le piastrine sono piccoli frammenti cellulari del sangue che si aggregano e formano il tappo iniziale che arresta il sanguinamento da un vaso danneggiato. Il loro numero riflette l'equilibrio tra la produzione nel midollo osseo e il consumo o la distruzione. Un numero basso può aumentare la tendenza al sanguinamento, mentre uno alto può seguire un'infiammazione o altri stimoli. Fanno parte dell'emocromo completo e sono una misura di base della capacità di coagulazione.",
"apob": "L'apolipoproteina B è la proteina strutturale presente su ciascuna delle particelle di lipoproteine aterogene, quindi misurarla significa contare direttamente quelle particelle anziché il colesterolo che trasportano. Poiché c'è una molecola di ApoB su ogni particella di LDL, VLDL e affini, può cogliere un rischio cardiovascolare che un valore di colesterolo LDL non rileva, soprattutto quando il numero di particelle e il loro contenuto di colesterolo divergono. Per questo è sempre più usata per affinare la valutazione del rischio basata sui lipidi. Si interpreta insieme al consueto profilo lipidico.",
"lp-a": "La lipoproteina(a) è una particella simile all'LDL con una proteina aggiuntiva attaccata, il cui livello è in gran parte determinato dalla genetica e resta piuttosto stabile per tutta la vita. Una Lp(a) elevata è un fattore di rischio indipendente ed ereditario di malattia cardiovascolare e di restringimento della valvola aortica. Poiché varia poco, di solito si misura una sola volta per stabilire il rischio di una persona lungo l'arco della vita, anziché seguirla ripetutamente. Si esprime in unità molari e si legge nell'ambito del rischio cardiovascolare complessivo.",
- "fasting-insulin": "L'insulina a digiuno è il livello di insulina — l'ormone che fa passare il glucosio dal sangue alle cellule — misurato dopo un digiuno notturno. Insieme alla glicemia a digiuno, aiuta a stimare la resistenza all'insulina, lo stato in cui il corpo ha bisogno di più insulina per mantenere il glucosio nel suo intervallo. Un'insulina a digiuno alta con una glicemia normale può essere un segno precoce che questa compensazione è in atto. Si legge nell'ambito di un quadro di salute metabolica e non isolatamente.",
- "omega-3-index": "L'indice omega-3 è la quota di EPA e DHA — i due acidi grassi omega-3 marini a catena lunga — tra gli acidi grassi delle membrane dei globuli rossi. Poiché i globuli rossi si rinnovano lentamente, riflette l'apporto di omega-3 a lungo termine anziché un pasto recente. Un indice più alto è stato associato, in studi osservazionali, a un rischio cardiovascolare più basso. Si segue come un marcatore stabile dello stato di omega-3 che risponde alla dieta nell'arco di mesi."
+ "fasting-insulin": "L'insulina a digiuno è il livello di insulina (l'ormone che fa passare il glucosio dal sangue alle cellule) misurato dopo un digiuno notturno. Insieme alla glicemia a digiuno, aiuta a stimare la resistenza all'insulina, lo stato in cui il corpo ha bisogno di più insulina per mantenere il glucosio nel suo intervallo. Un'insulina a digiuno alta con una glicemia normale può essere un segno precoce che questa compensazione è in atto. Si legge nell'ambito di un quadro di salute metabolica e non isolatamente.",
+ "omega-3-index": "L'indice omega-3 è la quota di EPA e DHA (i due acidi grassi omega-3 marini a catena lunga) tra gli acidi grassi delle membrane dei globuli rossi. Poiché i globuli rossi si rinnovano lentamente, riflette l'apporto di omega-3 a lungo termine anziché un pasto recente. Un indice più alto è stato associato, in studi osservazionali, a un rischio cardiovascolare più basso. Si segue come un marcatore stabile dello stato di omega-3 che risponde alla dieta nell'arco di mesi."
},
"apob": "Apolipoproteina B (ApoB)",
"lp-a": "Lipoproteina(a)",
@@ -9199,12 +9546,12 @@
"dateLabel": "Data",
"refLowLabel": "Riferimento inferiore",
"refHighLabel": "Riferimento superiore",
- "newBiomarker": "Nuovo biomarcatore — verrà creato",
+ "newBiomarker": "Nuovo biomarcatore, verrà creato",
"linksExisting": "Collegato a {name} esistente",
"duplicateWarning": "Esiste già un valore corrispondente",
"lowConfidence": "Controlla questo valore",
"lowConfidenceBadge": "Bassa attendibilità",
- "valueUnreadable": "Valore illeggibile — inseriscilo manualmente",
+ "valueUnreadable": "Valore illeggibile: inseriscilo manualmente",
"saveSelected": "Salva {count} selezionati",
"discardAll": "Scarta tutto",
"savedToast": "{count} risultati di laboratorio salvati",
@@ -9320,6 +9667,10 @@
"vaccinations": {
"label": "Vaccinazioni",
"description": "Il libretto vaccinale strutturato: dose, serie, prossimo richiamo."
+ },
+ "timeline": {
+ "label": "Cronologia",
+ "description": "Anni a colpo d’occhio: malattie, farmaci e visite accanto ai tuoi valori, più i tuoi eventi di vita."
}
},
"moduleOff": {
@@ -9351,8 +9702,8 @@
},
"form": {
"date": "Data",
- "catalogLabel": "Vaccino (catalogo)",
- "identityHint": "Scegli dal catalogo o scrivilo sotto: ne basta uno.",
+ "catalogLabel": "Vaccino",
+ "identityHint": "Scegli un vaccino dall'elenco o scrivilo sotto. Ne basta uno.",
"catalogNone": "Scegli un vaccino",
"catalogSearch": "Cerca vaccini",
"catalogNoMatch": "Nessuna voce del catalogo trovata",
@@ -9472,10 +9823,31 @@
"overdue": "Richiamo scaduto dal {date}",
"dueSoon": "Richiamo previsto il {date}",
"current": "Prossimo richiamo il {date}"
+ },
+ "custom": {
+ "title": "Vaccini personali",
+ "add": "Aggiungi vaccino personale",
+ "name": "Nome",
+ "components": "Protegge da",
+ "seriesDoses": "Dosi del ciclo primario",
+ "boosterMonths": "Richiamo ogni (mesi)",
+ "saved": "Vaccino salvato",
+ "deleted": "Vaccino rimosso",
+ "deleteConfirm": "Rimuovere questo vaccino? Le dosi già registrate mantengono il loro nome.",
+ "saveFailed": "Impossibile salvare il vaccino.",
+ "nameTaken": "Hai già un vaccino con questo nome.",
+ "editTitle": "Modifica vaccino personale",
+ "description": "Un vaccino assente dal catalogo, contato come qualsiasi voce del catalogo.",
+ "componentsHint": "Scegli tutte le malattie da cui protegge.",
+ "boosterHint": "Lascia vuoto se non serve un richiamo.",
+ "protectsAgainst": "Protegge da {list}.",
+ "deleteTitle": "Rimuovere il vaccino personale?",
+ "sectionDescription": "Vaccini aggiunti da te perché il catalogo non li elenca.",
+ "catalogGroup": "Catalogo"
}
},
"illness": {
- "title": "Diario delle malattie",
+ "title": "Malattia",
"subtitle": "Un registro retrospettivo dei tuoi episodi di malattia e disturbi.",
"settings": {
"viewDescription": "Scegli se gli episodi appaiono come schede o come elenco compatto.",
@@ -9614,7 +9986,7 @@
"loadError": "Impossibile caricare questo episodio.",
"backToList": "Torna al diario",
"detail": {
- "note": "Note"
+ "note": "Nota"
},
"edit": {
"title": "Modifica episodio",
@@ -9663,7 +10035,7 @@
"dismiss": "Non ora",
"applied": "Aggiunto",
"dismissed": "Ignorato",
- "failed": "Impossibile aggiungere — riprova",
+ "failed": "Impossibile aggiungere. Riprova",
"checkup": {
"title": "Aggiungere un promemoria di controllo?"
},
@@ -9715,7 +10087,7 @@
},
"refusal": {
"outOfScope": "Posso aiutarti solo con i dati di salute del tuo registro: misurazioni, farmaci, umore, tendenze. Non rispondo a domande al di fuori di questo ambito.",
- "promptInjection": "Ho notato istruzioni che cercano di aggirare le mie direttive. Sono il Coach di HealthLog e riassumo soltanto i tuoi dati di salute — riformula la domanda in questi termini, per favore.",
+ "promptInjection": "Ho notato istruzioni che cercano di aggirare le mie direttive. Sono il Coach di HealthLog e riassumo soltanto i tuoi dati di salute: riformula la domanda in questi termini, per favore.",
"conversationPoisoned": "Un messaggio precedente di questa conversazione contiene istruzioni che cercano di aggirare le mie direttive. Inizia una nuova conversazione, per favore."
},
"noRecordedData": "Non hai misurazioni registrate su quello che chiedi, quindi non posso darti alcun valore. Appena ne inserisci qualcuna, richiedimelo e userò i tuoi dati.",
@@ -9726,9 +10098,6 @@
"showResult": "Apertura di una tabella precedente",
"reuse": "Uso della tabella di prima",
"headerActive": "{label}…",
- "headerDoneOne": "1 fonte consultata",
- "headerDoneFew": "{count} fonti consultate",
- "headerDoneOther": "{count} fonti consultate",
"listLabel": "Cosa ha consultato il Coach",
"toggleShow": "Mostra cosa ha consultato il Coach",
"toggleHide": "Nascondi cosa ha consultato il Coach",
@@ -9754,7 +10123,9 @@
"illness": "Malattia e recupero",
"cycle": "Ciclo",
"correlations": "Relazioni tra parametri",
- "snapshot": "Riepilogo dei dati"
+ "snapshot": "Riepilogo dei dati",
+ "environment": "Meteo e aria",
+ "day": "Una giornata"
},
"window": {
"last7days": "ultimi 7 giorni",
@@ -9878,7 +10249,8 @@
"relatedMetric": "Vedi anche: {metric}",
"continue": "Continua a cercare",
"settingLabel": "Suggerisci domande di approfondimento",
- "settingHint": "Mostra fino a tre suggerimenti sotto l'ultima risposta del Coach."
+ "settingHint": "Mostra fino a tre suggerimenti sotto l'ultima risposta del Coach.",
+ "changeAssumption": "Invece: {value}"
},
"clarify": {
"cardLabel": "Il Coach ha una domanda",
@@ -9891,6 +10263,12 @@
"last90days": "Ultimi 90 giorni",
"lastYear": "Ultimi 12 mesi",
"allTime": "Intero periodo"
+ },
+ "comparison": {
+ "baseline90d": "Confronta con i miei soliti 90 giorni"
+ },
+ "anchor": {
+ "illness": "Dalla malattia del {date}"
}
},
"reuse": {
@@ -9936,10 +10314,10 @@
"saved": "Salvato",
"error": "Qualcosa è andato storto. Riprova.",
"instrument": {
- "phq9": "PHQ-9 · umore",
- "gad7": "GAD-7 · preoccupazione",
- "who5": "WHO-5 · benessere",
- "sci": "SCI · sonno"
+ "phq9": "PHQ-9 (umore)",
+ "gad7": "GAD-7 (preoccupazione)",
+ "who5": "WHO-5 (benessere)",
+ "sci": "SCI (sonno)"
},
"instrumentSub": {
"phq9": "Screening della depressione, 9 domande",
@@ -9960,7 +10338,7 @@
"2": "Molto difficile",
"3": "Estremamente difficile"
},
- "functionalOptionalHint": "Facoltativo — puoi inviare anche senza rispondere a questa domanda.",
+ "functionalOptionalHint": "Facoltativo: puoi inviare anche senza rispondere a questa domanda.",
"result": {
"title": "Il tuo risultato",
"totalLabel": "Punteggio totale",
@@ -10380,12 +10758,34 @@
"daylight": "Ore di luce",
"precip": "Precipitazioni",
"pressureMean": "Pressione atmosferica",
- "pressureDelta": "Variazione di pressione"
+ "pressureDelta": "Variazione di pressione",
+ "pm25": "Particolato fine (PM2,5)",
+ "pm10": "Particolato (PM10)",
+ "no2": "Biossido di azoto",
+ "ozone8h": "Ozono (massimo su 8 ore)",
+ "europeanAqi": "Indice europeo di qualità dell’aria",
+ "uvIndexMax": "Indice UV (massimo)",
+ "dustMax": "Polvere del Sahara",
+ "pollenMax": "Pollini (valore più alto)"
+ },
+ "chip": {
+ "day": "Meteo del {date}",
+ "pollenHigh": "Molti pollini: {kinds}",
+ "hotNight": "Notte calda",
+ "veryPoorAir": "Aria molto scadente"
+ },
+ "pollen": {
+ "alder": "ontano",
+ "birch": "betulla",
+ "grass": "graminacee",
+ "mugwort": "artemisia",
+ "olive": "olivo",
+ "ragweed": "ambrosia"
}
},
"documents": {
"title": "Documenti",
- "subtitle": "Ogni lettera, referto e scansione in un unico posto ricercabile — cifrato a riposo.",
+ "subtitle": "Ogni lettera, referto e scansione in un unico posto ricercabile, cifrato a riposo.",
"pageUpload": "Carica",
"ownerOnlyUpload": "Solo il titolare della cartella può aggiungere documenti.",
"kind": {
@@ -10405,11 +10805,13 @@
"searchPlaceholder": "Cerca titolo o nome file…",
"typeGroup": "Tipo di documento",
"conditionGroup": "Patologia",
+ "procedureGroup": "Procedure e interventi chirurgici",
"yearGroup": "Anno",
"clear": "Cancella filtri",
"typeAll": "Tutti i tipi",
"typeCount": "{count} tipi",
"conditionAll": "Tutte le patologie",
+ "procedureAll": "Tutte le procedure",
"yearAll": "Tutti gli anni"
},
"card": {
@@ -10425,15 +10827,15 @@
},
"error": {
"fileTooLarge": "Questo file supera il limite di {maxSize}.",
- "quotaExceeded": "Lo spazio è pieno — {used} di {quota} usati.",
+ "quotaExceeded": "Lo spazio è pieno: {used} di {quota} usati.",
"unsupportedType": "Questo tipo di file non è supportato.",
"purged": "Questo documento è stato rimosso definitivamente e non può essere ripristinato.",
"duplicateExists": "Un documento identico è già archiviato.",
- "rateLimited": "Troppi caricamenti — riprova tra qualche minuto.",
+ "rateLimited": "Troppi caricamenti. Riprova tra qualche minuto.",
"generic": "Caricamento non riuscito. Riprova."
},
"toast": {
- "duplicate": "Già archiviato — il documento esistente viene evidenziato.",
+ "duplicate": "Già archiviato. Il documento esistente è evidenziato.",
"deleted": "Documento eliminato",
"restoreFailed": "Impossibile ripristinare il documento."
},
@@ -10456,7 +10858,7 @@
},
"empty": {
"title": "Il tuo archivio documenti è vuoto",
- "description": "Conserva lettere mediche, referti di laboratorio, imaging, prescrizioni e certificati vaccinali — cifrati e ricercabili per tipo, patologia e anno.",
+ "description": "Conserva lettere mediche, referti di laboratorio, imaging, prescrizioni e certificati vaccinali. Sono cifrati e ricercabili per tipo, patologia e anno.",
"action": "Carica un primo documento",
"noMatchesTitle": "Nessun documento corrispondente",
"noMatchesDescription": "Nulla corrisponde alla ricerca e ai filtri attuali."
@@ -10487,6 +10889,7 @@
"saveError": "Impossibile salvare la modifica.",
"deleteError": "Impossibile eliminare il documento.",
"download": "Scarica",
+ "openInNewTab": "Apri in una nuova scheda",
"delete": "Elimina",
"titleLabel": "Titolo",
"titlePlaceholder": "Aggiungi un titolo",
@@ -10496,9 +10899,9 @@
"linkCondition": "Collega",
"noConditions": "Non collegato a una patologia",
"previewUnavailable": "Nessuna anteprima integrata per questo tipo di file",
- "uploadedMeta": "Aggiunto il {date} · {size}",
+ "uploadedMeta": "Aggiunto il {date}, {size}",
"importedFrom": "Importato da {system}",
- "importedFromWithId": "Importato da {system} · ID {id}",
+ "importedFromWithId": "Importato da {system}, ID {id}",
"sourceOther": "un altro sistema",
"share": "Condividi",
"labValuesLink": "Vedi {count} valori di laboratorio",
@@ -10598,7 +11001,7 @@
"CONDITION": "Condizione",
"MEDICATION_STATEMENT": "Farmaco"
},
- "statedRange": "intervallo indicato {range}",
+ "statedRange": "Intervallo indicato {range}",
"needsCheck": "Controlla questo valore rispetto al documento, poi salvalo.",
"verify": "Controlla",
"verifySave": "Salva valore",
@@ -10714,6 +11117,21 @@
"finishedHint": "Cerca di nuovo o cambia un filtro per scegliere altri documenti.",
"maxReached": "Puoi importare al massimo {max} documenti alla volta.",
"noMatchHint": "Prova con parte del nome, oppure azzera etichetta e date."
+ },
+ "customize": "Personalizza visualizzazione",
+ "layout": {
+ "viewDescription": "Schede con anteprima oppure un elenco compatto.",
+ "arrangementHeading": "Disposizione",
+ "arrangementDescription": "Ogni mese in un blocco a sé oppure tutto di seguito su tutta la larghezza.",
+ "stacked": "Mesi impilati",
+ "flow": "Continuo",
+ "saveFailed": "Impossibile salvare la visualizzazione."
+ },
+ "deleteConfirm": {
+ "titleOne": "Eliminare questo documento?",
+ "titleMany": "Eliminare {count} documenti?",
+ "body": "Puoi annullare subito dopo. Dopo 30 giorni viene rimosso definitivamente.",
+ "action": "Elimina"
}
},
"selfDescription": {
@@ -10800,7 +11218,7 @@
"genericDescription": "Una misurazione che monitori. Aggiungi valori per seguirla nel tempo.",
"emptyTitle": "Ancora nessun valore",
"emptyDescription": "Aggiungi il primo valore per iniziare a rappresentare questa metrica.",
- "valuesTitle": "{metric} — tutti i valori",
+ "valuesTitle": "{metric}: tutti i valori",
"valuesDescription": "Tutti i valori registrati per questa metrica.",
"backToMetric": "Torna a {metric}"
}
@@ -10853,29 +11271,35 @@
}
},
"hydration": {
- "referenceMeta": "Apporto di riferimento {value} {unit}/giorno · EFSA"
+ "referenceMeta": "Apporto di riferimento {value} {unit}/giorno (EFSA)"
},
"caffeine": {
- "ceilingMeta": "Limite di sicurezza {value} {unit}/giorno · EFSA"
+ "ceilingMeta": "Limite di sicurezza {value} {unit}/giorno (EFSA)"
},
"micronutrients": {
"title": "Vitamine e minerali",
- "rowMeta": "{days} di {window} giorni · rif. {value} {unit} (EFSA)",
+ "rowMeta": "{days} di {window} giorni, rif. {value} {unit} (EFSA)",
"rowMetaNoRef": "{days} di {window} giorni",
"footer": "{tracked} di {total} micronutrienti monitorati hanno dati in questo periodo.",
"emptyTitle": "Nessun dato sui micronutrienti ancora sincronizzato",
"loadError": "Non è stato possibile caricare i tuoi dati sui nutrienti"
},
- "sparseNote": "Un valore basso qui di solito indica una registrazione poco frequente, non una carenza — i totali giornalieri di Apple Health riflettono ciò che le tue app hanno registrato, non l'apporto alimentare totale."
+ "sparseNote": "Un valore basso qui di solito indica una registrazione poco frequente, non una carenza: i totali giornalieri di Apple Health riflettono ciò che le tue app hanno registrato, non l'apporto alimentare totale."
},
"daily": {
"line": {
- "score": "Il tuo punteggio di salute oggi è {score}.",
- "allClear": "Oggi nulla richiede la tua attenzione."
+ "allClear": "Oggi nulla richiede la tua attenzione.",
+ "scoreReady": "Il tuo punteggio di salute di oggi è pronto."
},
"push": {
"title": "Il tuo riepilogo mattutino",
- "bodyProvisional": "{line} · Il sonno della scorsa notte non è ancora disponibile."
+ "bodyProvisional": "{line}\nIl sonno della scorsa notte non è ancora disponibile.",
+ "titleScore": "Punteggio del mattino {score}",
+ "band": {
+ "green": "Oggi tutto sembra stabile.",
+ "yellow": "Oggi vale la pena dare un'occhiata ad alcune cose.",
+ "red": "Oggi alcune aree richiedono attenzione."
+ }
},
"item": {
"doseWindow": {
@@ -10893,7 +11317,7 @@
},
"syncIssue": {
"title": "La sincronizzazione richiede attenzione",
- "body": "{integration} non si sincronizza — riconnettilo per mantenere aggiornati i tuoi dati."
+ "body": "{integration} non si sincronizza. Riconnettilo per mantenere aggiornati i tuoi dati."
},
"coachCheckin": {
"title": "È il momento di rivedere un piano",
@@ -10905,10 +11329,10 @@
"tensionWindow": {
"title": "Forse un po' di tensione oggi",
"body": {
- "morning": "La tua frequenza cardiaca è rimasta sopra il valore a riposo per un tratto prolungato stamattina mentre eri fermo — forse un po' di tensione.",
- "afternoon": "La tua frequenza cardiaca è rimasta sopra il valore a riposo per un tratto prolungato questo pomeriggio mentre eri fermo — forse un po' di tensione.",
- "evening": "La tua frequenza cardiaca è rimasta sopra il valore a riposo per un tratto prolungato stasera mentre eri fermo — forse un po' di tensione.",
- "night": "La tua frequenza cardiaca è rimasta sopra il valore a riposo per un tratto prolungato durante la notte mentre eri fermo — forse un po' di tensione."
+ "morning": "La tua frequenza cardiaca è rimasta sopra il valore a riposo per un tratto prolungato stamattina mentre eri fermo: forse un po' di tensione.",
+ "afternoon": "La tua frequenza cardiaca è rimasta sopra il valore a riposo per un tratto prolungato questo pomeriggio mentre eri fermo: forse un po' di tensione.",
+ "evening": "La tua frequenza cardiaca è rimasta sopra il valore a riposo per un tratto prolungato stasera mentre eri fermo: forse un po' di tensione.",
+ "night": "La tua frequenza cardiaca è rimasta sopra il valore a riposo per un tratto prolungato durante la notte mentre eri fermo: forse un po' di tensione."
}
},
"ecgNewRecording": {
@@ -10933,9 +11357,9 @@
},
"upcomingVisit": {
"title": "Appuntamento in arrivo",
- "bodyToday": "{what} — oggi",
- "bodyTomorrow": "{what} — domani",
- "bodyDayAfterTomorrow": "{what} — dopodomani"
+ "bodyToday": "{what}, oggi",
+ "bodyTomorrow": "{what}, domani",
+ "bodyDayAfterTomorrow": "{what}, dopodomani"
}
},
"action": {
@@ -10957,7 +11381,31 @@
"sleepPending": "Il sonno di stanotte non è ancora arrivato: la lettura di oggi è provvisoria.",
"worthALook": "Da tenere d'occhio",
"allClear": "Oggi nulla richiede la tua attenzione.",
- "ringLink": "Apri i dettagli di {metric}"
+ "ringLink": "Apri i dettagli di {metric}",
+ "heading": "Oggi",
+ "steadyWeeksOne": "Stabile da {count} settimana",
+ "steadyWeeksFew": "Stabile da {count} settimane",
+ "steadyWeeksOther": "Stabile da {count} settimane",
+ "valueWithUnit": "{value} {unit}",
+ "rangeWithUnit": "da {low} a {high} {unit}",
+ "rangeAtThisTime": "{range} per quest'ora del giorno",
+ "vital": {
+ "RESTING_HEART_RATE": "Frequenza cardiaca a riposo",
+ "HEART_RATE_VARIABILITY": "Variabilità della frequenza cardiaca",
+ "RESPIRATORY_RATE": "Frequenza respiratoria",
+ "OXYGEN_SATURATION": "Saturazione di ossigeno",
+ "BODY_TEMPERATURE": "Temperatura corporea",
+ "SKIN_TEMPERATURE": "Temperatura cutanea",
+ "BLOOD_PRESSURE_SYS": "Pressione sistolica",
+ "BLOOD_PRESSURE_DIA": "Pressione diastolica",
+ "BLOOD_GLUCOSE": "Glicemia",
+ "PULSE": "Polso",
+ "WEIGHT": "Peso",
+ "generic": "Un parametro vitale"
+ },
+ "steadyAtLeastWeeksOne": "Stabile da almeno {count} settimana",
+ "steadyAtLeastWeeksFew": "Stabile da almeno {count} settimane",
+ "steadyAtLeastWeeksOther": "Stabile da almeno {count} settimane"
},
"milestone": {
"metric": {
@@ -10993,6 +11441,68 @@
"body": "Un mese intero con la tua {metric} nel tuo intervallo abituale. Meravigliosamente costante."
}
}
+ },
+ "lead": {
+ "vitalOutside": {
+ "above": "{metric} è a {value}, sopra il tuo intervallo abituale di {range}.",
+ "below": "{metric} è a {value}, sotto il tuo intervallo abituale di {range}."
+ },
+ "vitalsOutside": {
+ "aboveOne": "{metric} è a {value}, sopra il tuo intervallo abituale di {range}, e anche {count} altro parametro vitale è fuori dal suo intervallo.",
+ "aboveFew": "{metric} è a {value}, sopra il tuo intervallo abituale di {range}, e anche altri {count} parametri vitali sono fuori dal loro intervallo.",
+ "aboveOther": "{metric} è a {value}, sopra il tuo intervallo abituale di {range}, e anche altri {count} parametri vitali sono fuori dal loro intervallo.",
+ "belowOne": "{metric} è a {value}, sotto il tuo intervallo abituale di {range}, e anche {count} altro parametro vitale è fuori dal suo intervallo.",
+ "belowFew": "{metric} è a {value}, sotto il tuo intervallo abituale di {range}, e anche altri {count} parametri vitali sono fuori dal loro intervallo.",
+ "belowOther": "{metric} è a {value}, sotto il tuo intervallo abituale di {range}, e anche altri {count} parametri vitali sono fuori dal loro intervallo."
+ },
+ "sleep": {
+ "more": "Stanotte hai dormito {duration}, {diff} in più del solito.",
+ "less": "Stanotte hai dormito {duration}, {diff} in meno del solito.",
+ "usual": "Stanotte hai dormito {duration}, vicino al tuo solito.",
+ "plain": "Stanotte hai dormito {duration}."
+ },
+ "vitalsInRangeOne": "Il tuo ultimo parametro vitale è nel suo intervallo abituale.",
+ "vitalsInRangeFew": "Tutti i tuoi {count} ultimi parametri vitali sono nel loro intervallo abituale.",
+ "vitalsInRangeOther": "Tutti i tuoi {count} ultimi parametri vitali sono nel loro intervallo abituale.",
+ "vitalInRange": "{metric} è a {value}, nel tuo intervallo abituale di {range}."
+ },
+ "todayFact": {
+ "restMode": {
+ "label": "Modalità riposo",
+ "value": "Giorno {day}"
+ },
+ "medications": {
+ "label": "Farmaci",
+ "value": "{taken} di {scheduled} presi"
+ },
+ "appointment": {
+ "label": "Appuntamento",
+ "today": "Oggi {time}, {what}",
+ "tomorrow": "Domani {time}, {what}"
+ },
+ "sleep": {
+ "label": "Stanotte",
+ "plain": "{duration}",
+ "usual": "{duration}, vicino al solito",
+ "more": "{duration}, {diff} in più del solito",
+ "less": "{duration}, {diff} in meno del solito"
+ },
+ "vitals": {
+ "label": "Parametri vitali",
+ "allInOne": "{count} controllato, nel tuo intervallo",
+ "allInFew": "{count} controllati, tutti nel tuo intervallo",
+ "allInOther": "{count} controllati, tutti nel tuo intervallo",
+ "oneOut": {
+ "above": "{metric} sopra il tuo intervallo",
+ "below": "{metric} sotto il tuo intervallo"
+ },
+ "someOut": "{count} su {total} fuori dal tuo intervallo"
+ },
+ "cycle": {
+ "label": "Ciclo",
+ "value": "{phase}, giorno {day}",
+ "dayOnly": "Giorno {day}"
+ }
}
},
"reportSelection": {
@@ -11177,7 +11687,7 @@
"managed": {
"title": "Profili di cui ti occupi",
"description": "Cartelle sanitarie senza credenziali proprie.",
- "explainer": "Un profilo gestito è una cartella sanitaria per qualcuno che non accede da sé — un bambino, o una persona di cui ti prendi cura. Non ha credenziali né indirizzo e-mail, e tu ne diventi la prima persona responsabile. Più avanti puoi aggiungerne altre, e ognuna può entrare nella cartella per tenerla aggiornata.",
+ "explainer": "Un profilo gestito è una cartella sanitaria per qualcuno che non accede da sé, come un bambino o una persona di cui ti prendi cura. Non ha credenziali né indirizzo e-mail, e tu ne diventi la prima persona responsabile. Più avanti puoi aggiungerne altre, e ognuna può entrare nella cartella per tenerla aggiornata.",
"nameLabel": "Nome",
"namePlaceholder": "Il nome con cui riconoscerai questa cartella",
"nameHint": "Fino a 80 caratteri. Compare ovunque appaia questa cartella.",
@@ -11199,7 +11709,7 @@
"creating": "Creazione in corso…",
"created": "{name} è pronto. Puoi entrare in quella cartella dal menu dell'account.",
"errorStepUp": "Conferma il tuo secondo fattore e riprova.",
- "errorOffline": "Non è arrivato al server. Non è cambiato nulla — controlla la connessione e riprova.",
+ "errorOffline": "Non è arrivato al server. Non è cambiato nulla. Controlla la connessione e riprova.",
"errorInvalid": "Il server ha rifiutato questi dati. Controlla nome, data e fuso orario.",
"errorFailed": "Non è stato possibile creare il profilo.",
"edit": "Modifica",
@@ -11210,7 +11720,7 @@
"editErrorFailed": "Non è stato possibile salvare le modifiche.",
"delete": "Elimina profilo",
"deleteTitle": "Eliminare {name}?",
- "deleteBody": "Tutto ciò che è stato registrato per {name} sparisce insieme al profilo — misurazioni, farmaci, documenti e note. Non si può annullare.",
+ "deleteBody": "Tutto ciò che è stato registrato per {name} sparisce insieme al profilo: misurazioni, farmaci, documenti e note. Non si può annullare.",
"deleteGuardiansOne": "{count} persona si occupa oggi di questa cartella e la perderà.",
"deleteGuardiansFew": "{count} persone si occupano oggi di questa cartella e la perderanno.",
"deleteGuardiansOther": "{count} persone si occupano oggi di questa cartella e la perderanno.",
@@ -11239,7 +11749,47 @@
"guardianErrorFailed": "Non è stato possibile inviare l'invito.",
"errorMfaRequired": "Serve un secondo fattore e il tuo account non ne ha ancora uno. Impostane uno in Impostazioni → Sicurezza e riprova.",
"rosterLoadError": "Non è stato possibile caricare chi si occupa di questa cartella.",
- "errorRateLimit": "Troppi profili creati nell'ultima ora. Aspetta un po' e riprova."
+ "errorRateLimit": "Troppi profili creati nell'ultima ora. Aspetta un po' e riprova.",
+ "handover": {
+ "action": "Consegna",
+ "title": "Consegna a {name}",
+ "description": "Dai a {name} un accesso proprio. Nulla viene copiato o spostato: la cartella diventa il suo account.",
+ "guardiansLabel": "Accesso dopo la consegna",
+ "guardiansHint": "La tua proposta. {name} la vede e decide dopo l'accesso.",
+ "access": {
+ "end": "Nessun accesso",
+ "read": "Solo lettura",
+ "manage": "Gestire"
+ },
+ "expiryLabel": "Link valido per",
+ "expiry1": "1 giorno",
+ "expiry7": "7 giorni",
+ "expiry14": "14 giorni",
+ "minorHint": "{name} ha meno di 16 anni. Vale la pena mantenere l'accesso in lettura e parlare insieme di questo passo.",
+ "irreversible": "Appena {name} usa il link, la consegna non si può annullare. La cartella sarà sua e deciderà chi mantiene l'accesso.",
+ "afterwards": "{name} conferma di persona l'avviso e ogni consenso all'IA. I promemoria di questa cartella non ti raggiungeranno più.",
+ "replaceNote": "Creare un nuovo link ritira quello aperto.",
+ "create": "Crea link",
+ "creating": "Creazione…",
+ "linkOnce": "Questo link viene mostrato solo ora. Consegnalo a {name} di persona o tramite un canale di fiducia, oppure fai scansionare il codice.",
+ "qrAlt": "Codice QR del link di consegna per {name}",
+ "urlLabel": "Link di consegna",
+ "copy": "Copia",
+ "copied": "Link copiato",
+ "copyError": "Impossibile copiare il link. Selezionalo e copialo a mano.",
+ "linkExpires": "Valido fino al {date}.",
+ "done": "Fatto",
+ "pending": "Un link di consegna è aperto fino al {date}.",
+ "replace": "Nuovo link",
+ "revoke": "Ritira link",
+ "revoked": "Link ritirato",
+ "revokeFailed": "Impossibile ritirare il link. Riprova.",
+ "errorFailed": "Impossibile creare il link di consegna.",
+ "errorRosterChanged": "Nel frattempo è cambiato chi si occupa della cartella. Chiudi e riapri.",
+ "oidcOnly": "Non è possibile consegnare un profilo finché questo server consente solo il single sign-on.",
+ "loadError": "Impossibile caricare lo stato della consegna.",
+ "entryHint": "Quando {name} vuole tenere questa cartella in autonomia."
+ }
},
"toast": {
"savedTo": "Salvato nella cartella di {name}"
@@ -11358,7 +11908,13 @@
"symptomEventDelete": "{name} ha eliminato una voce di sintomo",
"symptomDefinitionCreate": "{name} ha aggiunto un sintomo",
"symptomDefinitionUpdate": "{name} ha modificato un sintomo",
- "symptomDefinitionDelete": "{name} ha rimosso un sintomo"
+ "symptomDefinitionDelete": "{name} ha rimosso un sintomo",
+ "customVaccineCreate": "{name} ha aggiunto un vaccino personale",
+ "customVaccineUpdate": "{name} ha modificato un vaccino personale",
+ "customVaccineDelete": "{name} ha rimosso un vaccino personale",
+ "lifeEventCreate": "{name} ha aggiunto un evento della vita",
+ "lifeEventUpdate": "{name} ha modificato un evento della vita",
+ "lifeEventDelete": "{name} ha rimosso un evento della vita"
},
"section": {
"measurements": {
@@ -11375,7 +11931,7 @@
},
"profile": {
"label": "Anamnesi",
- "detail": "Allergie, anamnesi familiare, i dati del tuo profilo sanitario, le tue visite mediche e studi, e il tuo libretto vaccinale."
+ "detail": "Allergie, anamnesi familiare, i dati del tuo profilo sanitario, le tue visite mediche e studi e il tuo libretto vaccinale."
},
"illness": {
"label": "Malattie",
@@ -11408,12 +11964,28 @@
"notFound": "Questo accesso non esiste più.",
"revokeFailed": "Non è stato possibile terminare l'accesso.",
"renounceFailed": "Non è stato possibile restituire l'accesso.",
- "profileNotFound": "Quel profilo non c'è più — potrebbe essere già stato eliminato, o il tuo accesso è terminato.",
+ "profileNotFound": "Quel profilo non c'è più: potrebbe essere già stato eliminato, o il tuo accesso è terminato.",
"deleteProfileFailed": "Non è stato possibile eliminare il profilo.",
"removeGuardianFailed": "Non è stato possibile terminare quell'accesso."
},
"anamnesis": {
"relativeNote": "Una voce di anamnesi familiare descrive i parenti di questa persona. Aggiungila solo se vorrebbe vederla registrata qui."
+ },
+ "handoverDecision": {
+ "title": "Chi mantiene l'accesso alla tua cartella",
+ "description": "Decidi cosa possono fare d'ora in poi le persone che hanno tenuto la tua cartella.",
+ "proposed": "Proposto: {access}",
+ "settled": "Già modificato in Accesso condiviso",
+ "empty": "Nessun altro aveva accesso alla tua cartella.",
+ "later": "Puoi cambiarlo in qualsiasi momento in Impostazioni, Accesso condiviso.",
+ "save": "Conferma",
+ "saving": "Salvataggio…",
+ "decideLater": "Più tardi",
+ "errorFailed": "Impossibile salvare la tua decisione. Riprova.",
+ "loadError": "Impossibile caricare chi teneva la tua cartella.",
+ "notificationsTitle": "Imposta i tuoi promemoria",
+ "notificationsBody": "Finora i promemoria di questa cartella arrivavano alle persone che la tenevano. D'ora in poi arrivano a te, appena aggiungi un canale di notifica.",
+ "notificationsAction": "Apri le notifiche"
}
},
"encounters": {
@@ -11434,7 +12006,7 @@
"both": "Entrambi i lati",
"none": "Non indicato"
},
- "bodySiteWithSide": "{site} · {side}",
+ "bodySiteWithSide": "{site} ({side})",
"sectionTitle": "Appuntamenti",
"sectionDescription": "Visite mediche e appuntamenti fissati in un unico posto.",
"add": "Appuntamento",
@@ -11526,7 +12098,6 @@
"title": "Studi e medici",
"description": "La tua rubrica: scrivi uno studio una volta e selezionalo ovunque.",
"linkLabel": "Rubrica",
- "add": "Voce",
"searchPlaceholder": "Cerca nome, studio o specializzazione",
"loadError": "Impossibile caricare la rubrica.",
"emptyTitle": "Nessuna voce",
@@ -11623,7 +12194,6 @@
"unreadableLabel": "Sintomo illeggibile",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Registra sintomo",
"sheetDescription": "Uno dei tuoi sintomi, quanto forte e quando.",
"symptom": "Sintomo",
"symptomRequired": "Scegli un sintomo.",
@@ -11672,5 +12242,426 @@
"title": "Sintomi registrati",
"filed": "registrato con questa malattia"
}
+ },
+ "day": {
+ "scoreOfMax": "su {max}",
+ "cycleDay": "Giorno {n} del ciclo",
+ "cyclePhaseLine": {
+ "MENSTRUAL": "Giorno {n} del ciclo, fase mestruale",
+ "FOLLICULAR": "Giorno {n} del ciclo, fase follicolare",
+ "OVULATORY": "Giorno {n} del ciclo, fase ovulatoria",
+ "LUTEAL": "Giorno {n} del ciclo, fase luteale"
+ },
+ "previousDay": "Giorno precedente",
+ "nextDay": "Giorno successivo",
+ "close": "Chiudi giorno",
+ "hidePanel": "Nascondi giorno",
+ "showPanel": "Mostra {date}",
+ "openDay": "Apri giorno",
+ "viewWholeDay": "Vedi l’intera giornata",
+ "captureForDay": "Registra per questo giorno",
+ "empty": "Non ci sono voci per questo giorno.",
+ "notShared": "Alcune aree non sono condivise con te.",
+ "inTimeline": "Nella cronologia",
+ "askCoach": "Chiedi al Coach di questo giorno",
+ "loadFailed": "Impossibile caricare questo giorno.",
+ "dayOf": "Giorno {n} di {m}",
+ "dayN": "Giorno {n}",
+ "usualRange": "Di solito da {lo} a {hi}",
+ "groups": {
+ "scores": "Punteggi",
+ "running": "In corso quel giorno",
+ "values": "Valori",
+ "happened": "Cosa è successo"
+ },
+ "notable": {
+ "extremeHigh": "Valore giornaliero più alto dal {since}",
+ "extremeLow": "Valore giornaliero più basso dal {since}",
+ "firstValue": "Prima misurazione di questo tipo",
+ "gap": "{days} giorni senza misurazione"
+ },
+ "previousDayShortcut": "Giorno precedente (Alt + freccia sinistra)",
+ "nextDayShortcut": "Giorno successivo (Alt + freccia destra)",
+ "captureShort": "Registra",
+ "inTimelineShort": "Cronologia",
+ "askCoachPrompt": "Cosa è successo il {date}?",
+ "coachChips": "Giorni in questa risposta",
+ "description": "Tutto ciò che le tue voci contengono per questo giorno.",
+ "openDocument": "Apri documento",
+ "previousReading": "Prima, {date}",
+ "sheetExpand": "Espandi a tutta altezza",
+ "sheetShrink": "Riduci",
+ "valuesLegend": "Punto per questo giorno, barra per i 30 giorni precedenti",
+ "valuesAllOne": "Tutti i valori",
+ "valuesAllFew": "Tutti i {count} valori",
+ "valuesAllOther": "Tutti i {count} valori",
+ "valuesFewer": "Meno valori",
+ "countEntriesOne": "1 voce",
+ "countEntriesFew": "{count} voci",
+ "countEntriesOther": "{count} voci",
+ "meanOfReadingsOne": "Media di 1 misurazione",
+ "meanOfReadingsFew": "Media di {count} misurazioni",
+ "meanOfReadingsOther": "Media di {count} misurazioni",
+ "sinceLastVisit": {
+ "title": "Dall’ultima visita del {date}",
+ "countOne": "1 voce",
+ "countFew": "{count} voci",
+ "countOther": "{count} voci",
+ "allOne": "Mostra tutto",
+ "allFew": "Mostra tutte le {count}",
+ "allOther": "Mostra tutte le {count}",
+ "fewer": "Mostra meno",
+ "note": "Raccolto dalle tue voci, senza valutazioni. Ogni data apre l’intera giornata.",
+ "report": "Referto medico dal {date} a oggi"
+ },
+ "travel": "In viaggio",
+ "event": {
+ "started": "Inizio: {label}",
+ "ended": "Fine: {label}",
+ "paused": "Pausa: {label}",
+ "resumed": "Ripresa: {label}",
+ "illnessBegan": "Inizio: {label}",
+ "illnessResolved": "Guarigione: {label}"
+ },
+ "pickDate": "Scegli una data",
+ "pickerTitle": "Vai a un giorno",
+ "today": "Oggi",
+ "hasEntries": "con voci",
+ "dayNSince": "Giorno {n}, dal {date}",
+ "sinceDate": "Dal {date}"
+ },
+ "timeline": {
+ "title": "Cronologia",
+ "subtitle": "Cosa è successo e quando, accanto ai tuoi valori.",
+ "standing": "Permanente",
+ "startUnknown": "inizio sconosciuto",
+ "zoom": {
+ "all": "Tutto",
+ "year": "12 mesi",
+ "quarter": "3 mesi",
+ "range": "Periodo"
+ },
+ "lanes": {
+ "life": "Vita",
+ "illness": "Malattie",
+ "allergies": "Allergie",
+ "medications": "Farmaci",
+ "vaccinations": "Vaccinazioni",
+ "visits": "Medico e interventi",
+ "labs": "Esami",
+ "documents": "Documenti",
+ "cycle": "Ciclo"
+ },
+ "readiness": {
+ "title": "Cosa contiene già la tua cronologia",
+ "subtitle": "Un inventario onesto prima di aprirla.",
+ "menuEntry": "Copertura dei dati",
+ "hint": "Le corsie vuote restano nascoste finché non c’è qualcosa. I moduli disattivati non compaiono mai. Ritroverai questa panoramica più avanti in Copertura dei dati, nella cronologia.",
+ "later": "Più tardi",
+ "open": "Apri cronologia",
+ "verdict": {
+ "carries": "È già ben fornita.",
+ "thin": "Ancora scarna. Per ora la cronologia mostra soprattutto i tuoi valori."
+ },
+ "status": {
+ "carries": "fornita",
+ "thin": "scarna",
+ "empty": "vuota"
+ },
+ "lanes": {
+ "values": "Valori",
+ "mood": "Umore",
+ "environment": "Ambiente",
+ "life": "Eventi di vita"
+ },
+ "meter": "{carrying} corsie su {total} hanno contenuto",
+ "carriesDetail": "{carrying} corsie su {total} danno una panoramica da {since}.",
+ "loadFailed": "Impossibile caricare l’inventario.",
+ "card": {
+ "title": "Cosa contiene già la cronologia",
+ "summary": "Risale fino a {since}; {carrying} corsie su {total} hanno già contenuto.",
+ "dismiss": "Nascondi"
+ },
+ "detail": {
+ "valuesOne": "{count} misura da {since}",
+ "valuesFew": "{count} misure da {since}",
+ "valuesOther": "{count} misure da {since}",
+ "illnessOne": "{count} episodio; cronici: {chronic}",
+ "illnessFew": "{count} episodi; cronici: {chronic}",
+ "illnessOther": "{count} episodi; cronici: {chronic}",
+ "allergiesOne": "{count} allergia",
+ "allergiesFew": "{count} allergie",
+ "allergiesOther": "{count} allergie",
+ "medicationsOne": "{count} farmaco; senza data di inizio: {missingStart}",
+ "medicationsFew": "{count} farmaci; senza data di inizio: {missingStart}",
+ "medicationsOther": "{count} farmaci; senza data di inizio: {missingStart}",
+ "medicationsDatedOne": "{count} farmaco con data di inizio",
+ "medicationsDatedFew": "{count} farmaci con data di inizio",
+ "medicationsDatedOther": "{count} farmaci con data di inizio",
+ "vaccinationsOne": "{count} vaccinazione",
+ "vaccinationsFew": "{count} vaccinazioni",
+ "vaccinationsOther": "{count} vaccinazioni",
+ "visitsOne": "{count} visita, {procedures} con intervento",
+ "visitsFew": "{count} visite, {procedures} con intervento",
+ "visitsOther": "{count} visite, {procedures} con intervento",
+ "visitsPlannedOne": "{count} visita, solo pianificata",
+ "visitsPlannedFew": "{count} visite, solo pianificate",
+ "visitsPlannedOther": "{count} visite, solo pianificate",
+ "labsOne": "{count} giorno con referti",
+ "labsFew": "{count} giorni con referti",
+ "labsOther": "{count} giorni con referti",
+ "documentsOne": "{count} documento con data",
+ "documentsFew": "{count} documenti con data",
+ "documentsOther": "{count} documenti con data",
+ "lifeOne": "{count} evento di vita",
+ "lifeFew": "{count} eventi di vita",
+ "lifeOther": "{count} eventi di vita",
+ "cycleOne": "{count} ciclo",
+ "cycleFew": "{count} cicli",
+ "cycleOther": "{count} cicli",
+ "moodOne": "{count} voce da {since}",
+ "moodFew": "{count} voci da {since}",
+ "moodOther": "{count} voci da {since}",
+ "environmentOne": "{count} giorno con dati ambientali",
+ "environmentFew": "{count} giorni con dati ambientali",
+ "environmentOther": "{count} giorni con dati ambientali",
+ "empty": "Ancora nulla"
+ },
+ "gap": {
+ "valuesEmpty": "Collega le fonti",
+ "illnessEmpty": "Aggiungi episodio",
+ "medicationsWithoutStart": "Completa",
+ "visitsEmpty": "Aggiungi visita",
+ "documentsUndated": "Controlla",
+ "labsEmpty": "Aggiungi referto",
+ "vaccinationsEmpty": "Aggiungi vaccinazioni",
+ "lifeEventsEmpty": "Aggiungi"
+ },
+ "gapLine": {
+ "medicationsWithoutStartOne": "{count} farmaco senza data di inizio",
+ "medicationsWithoutStartFew": "{count} farmaci senza data di inizio",
+ "medicationsWithoutStartOther": "{count} farmaci senza data di inizio",
+ "documentsUndatedOne": "{count} documento senza data",
+ "documentsUndatedFew": "{count} documenti senza data",
+ "documentsUndatedOther": "{count} documenti senza data",
+ "valuesEmpty": "Ancora nessun valore su un periodo lungo",
+ "illnessEmpty": "Ancora nessuna malattia con data di inizio",
+ "visitsEmpty": "Ancora nessuna visita medica svolta",
+ "labsEmpty": "Ancora nessun referto di laboratorio",
+ "vaccinationsEmpty": "Ancora nessuna vaccinazione",
+ "lifeEventsEmpty": "Ancora nessun evento di vita"
+ }
+ },
+ "legendSpan": "Periodo",
+ "legendEvent": "Evento",
+ "legendMeanQuarter": "Media trimestrale",
+ "legendMeanMonth": "Media mensile",
+ "legendMeanWeek": "Media settimanale",
+ "legendThin": "Meno di 3 misurazioni",
+ "legendGap": "Breve pausa senza misurazioni",
+ "today": "Oggi",
+ "noDataYet": "Ancora nessun dato",
+ "layers": "Livelli",
+ "layersLanes": "Corsie",
+ "zoomLabel": "Intervallo visualizzato",
+ "chartLabel": "Cronologia da {from} a {to}",
+ "keyboardHint": "Le frecce spostano la selezione, Inizio e Fine vanno agli estremi, Invio apre il giorno.",
+ "loadFailed": "Impossibile caricare la cronologia.",
+ "empty": {
+ "title": "Ancora niente nella cronologia",
+ "description": "Le voci con data compariranno qui appena ce ne saranno."
+ },
+ "values": {
+ "menuTitle": "Valori sotto le corsie",
+ "trigger": "Valori ({count})",
+ "noneInWindow": "Nessun valore in questo periodo",
+ "series": {
+ "BLOOD_PRESSURE_SYS": "Pressione sistolica",
+ "BLOOD_PRESSURE_DIA": "Pressione diastolica"
+ },
+ "noValue": "nessun valore",
+ "bloodPressure": "Pressione arteriosa",
+ "readingsOne": "1 misurazione",
+ "readingsFew": "media di {count} misurazioni",
+ "readingsOther": "media di {count} misurazioni"
+ },
+ "table": {
+ "caption": "Voci della cronologia",
+ "lane": "Corsia",
+ "entry": "Voce",
+ "start": "Inizio",
+ "end": "Fine",
+ "seriesCaption": "Medie per periodo",
+ "period": "Periodo"
+ },
+ "selection": {
+ "empty": "Nessuna voce in questo periodo",
+ "since": "dal {date}",
+ "range": "dal {from} al {to}",
+ "mean": "Ø {values}",
+ "week": "Settimana del {date}",
+ "hintClick": "Fai clic su un momento per vederlo qui",
+ "hintTap": "Tocca un momento per vederlo qui"
+ },
+ "chronicle": {
+ "months": "Mesi",
+ "years": "Anni",
+ "groupingLabel": "Raggruppamento",
+ "empty": "Nessuna voce da {from} a {to}",
+ "emptySingle": "{period}: nessuna voce",
+ "ended": "Fine: {label}",
+ "afterDaysOne": "dopo 1 giorno",
+ "afterDaysFew": "dopo {count} giorni",
+ "afterDaysOther": "dopo {count} giorni",
+ "notable": {
+ "extremeHigh": "Valore più alto da molto tempo",
+ "extremeLow": "Valore più basso da molto tempo",
+ "firstValue": "Primo valore di una misura",
+ "gap": "Una lunga pausa nei valori"
+ },
+ "notableMeta": "Apri il giorno per i dettagli"
+ },
+ "standingAllergiesOne": "{count} allergia",
+ "standingAllergiesFew": "{count} allergie",
+ "standingAllergiesOther": "{count} allergie",
+ "legendMeanDay": "Media giornaliera",
+ "range": {
+ "from": "Dal",
+ "to": "Al",
+ "label": "Periodo scelto",
+ "open": "Scegli un periodo",
+ "clear": "Mostra tutto",
+ "apply": "Applica"
+ },
+ "item": {
+ "labValuesOne": "1 valore di laboratorio",
+ "labValuesFew": "{count} valori di laboratorio",
+ "labValuesOther": "{count} valori di laboratorio"
+ }
+ },
+ "lifeEvents": {
+ "title": "Evento di vita",
+ "add": "Evento di vita",
+ "privacy": "Titolo e nota sono salvati cifrati. Non vanno a nessuna IA.",
+ "emptyHint": "Un trasloco, una nascita o un nuovo lavoro danno agli anni un punto di riferimento.",
+ "save": "Salva",
+ "cancel": "Annulla",
+ "delete": "Elimina evento di vita",
+ "saved": "Evento di vita salvato",
+ "deleted": "Evento di vita eliminato",
+ "fields": {
+ "title": "Cosa è successo?",
+ "category": "Tipo",
+ "when": "Quando",
+ "precision": "Precisione",
+ "addEnd": "Data di fine, se è stato un periodo",
+ "endDate": "Fino al",
+ "note": "Nota",
+ "optionalTag": "facoltativo",
+ "notePlaceholder": "Qualcosa che vorrai sapere più avanti",
+ "removeEnd": "Rimuovi data di fine"
+ },
+ "category": {
+ "FAMILY": "Famiglia",
+ "HOME": "Casa",
+ "WORK": "Lavoro",
+ "LOSS": "Lutto",
+ "OTHER": "Altro"
+ },
+ "precision": {
+ "DAY": "Giorno",
+ "MONTH": "Mese",
+ "YEAR": "Anno"
+ },
+ "edit": "Modifica evento di vita",
+ "errors": {
+ "titleRequired": "Dai un nome all’evento.",
+ "titleTooLong": "Al massimo 120 caratteri.",
+ "categoryRequired": "Scegli un tipo.",
+ "dateRequired": "Inserisci una data valida.",
+ "endBeforeStart": "La fine è prima dell’inizio.",
+ "noteTooLong": "Al massimo 2000 caratteri."
+ },
+ "saveFailed": "Salvataggio non riuscito.",
+ "deleteFailed": "Eliminazione non riuscita.",
+ "deleteConfirm": {
+ "title": "Eliminare questo evento di vita?",
+ "body": "Titolo e nota non ci saranno più."
+ }
+ },
+ "palette": {
+ "title": "Cerca",
+ "description": "Trova una pagina, un’impostazione, un modulo, un’azione o una voce dei tuoi dati.",
+ "placeholder": "Cerca pagine, impostazioni, i tuoi dati…",
+ "results": "Risultati",
+ "noResults": "Nessun risultato per «{query}».",
+ "moduleOn": "Attivo",
+ "moduleOff": "Disattivo",
+ "hintMove": "Sposta",
+ "hintOpen": "Apri",
+ "hintClose": "Chiudi",
+ "groups": {
+ "recent": "Recenti",
+ "pages": "Pagine",
+ "insights": "Insights",
+ "settings": "Impostazioni",
+ "modules": "Moduli",
+ "actions": "Azioni",
+ "records": "Nei tuoi dati"
+ },
+ "actions": {
+ "capture": "Nuova voce",
+ "coach": "Chiedi al Coach",
+ "backup": "Crea un backup",
+ "today": "Apri oggi",
+ "shortcuts": "Mostra scorciatoie da tastiera"
+ },
+ "records": {
+ "loading": "Ricerca nei tuoi dati…",
+ "empty": "Niente nei tuoi dati per «{query}».",
+ "error": "Non è stato possibile cercare nei tuoi dati. Riprova tra un momento.",
+ "truncated": "Sono state cercate solo le voci più recenti.",
+ "kind": {
+ "visit": "Visita",
+ "condition": "Malattia",
+ "document": "Documento",
+ "vaccination": "Vaccinazione",
+ "lab": "Esame di laboratorio",
+ "medication": "Farmaco",
+ "symptom": "Sintomo",
+ "workout": "Allenamento",
+ "note": "Nota"
+ },
+ "source": {
+ "mood": "Nota sull’umore",
+ "measurement": "Nota su una misurazione",
+ "illness": "Diario di malattia"
+ }
+ }
+ },
+ "shortcuts": {
+ "title": "Scorciatoie da tastiera",
+ "description": "Tasti che funzionano ovunque nell’app, tranne mentre scrivi in un campo.",
+ "menuEntry": "Scorciatoie da tastiera",
+ "then": "poi",
+ "search": "Cerca",
+ "help": "Mostra questo elenco",
+ "close": "Chiudi una finestra o un pannello",
+ "previousDay": "Giorno precedente",
+ "nextDay": "Giorno successivo",
+ "toggleDay": "Apri o chiudi il giorno",
+ "dayHint": "Giorno precedente e successivo funzionano mentre un giorno è aperto.",
+ "groups": {
+ "navigation": "Navigazione",
+ "actions": "Azioni",
+ "day": "Giorno"
+ }
+ },
+ "aboutPage": {
+ "eyebrow": "Informazioni",
+ "title": "Informazioni su HealthLog",
+ "lastUpdated": "Ultimo aggiornamento: {date}",
+ "project": "Progetto",
+ "credits": "Riconoscimenti"
}
}
diff --git a/messages/ko.json b/messages/ko.json
index e51af7017..d05ccb066 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -49,7 +49,9 @@
"dismiss": "닫기",
"moreInfo": "추가 정보",
"durationHoursMinutes": "{hours}시간 {minutes}분",
- "durationMinutes": "{minutes}분"
+ "durationMinutes": "{minutes}분",
+ "durationClockMinutes": "{time}분",
+ "durationClockHours": "{time}시간"
},
"apiErrors": {
"measurement": {
@@ -165,7 +167,7 @@
"anamnesisShiftSchedule": "근무 일정",
"anamnesisColFact": "프로필 항목",
"anamnesisColValue": "기록된 값",
- "subtitle": "HealthLog — 개인 건강 리포트",
+ "subtitle": "HealthLog: 개인 건강 리포트",
"patient": "환자",
"dateOfBirth": "생년월일",
"gender": "성별",
@@ -211,18 +213,18 @@
"contacts": "비상 연락처",
"notes": "추가 메모"
},
- "vitalsTitle": "활력징후 — 요약",
+ "vitalsTitle": "활력징후: 요약",
"colParameter": "항목",
"colCurrent": "현재",
"colAvgPeriod": "기간 평균",
"colMin": "최소",
"colMax": "최대",
"colN": "건수",
- "bpClassificationTitle": "혈압 — ESH 분류 (2023)",
+ "bpClassificationTitle": "혈압: ESH 분류 (2023)",
"avgBp": "평균 혈압",
"classification": "분류",
"bmiTitle": "체질량지수 (BMI)",
- "bmiRow": "BMI: {bmi} kg/m² — 분류: {class} (WHO)",
+ "bmiRow": "BMI: {bmi} kg/m², 분류: {class} (WHO)",
"complianceTitle": "복약 순응도",
"glp1Title": "GLP-1 치료",
"glp1WeightSummary": "리포트 기간 체중: {start} kg → {end} kg ({delta} kg)",
@@ -240,7 +242,7 @@
"colExpected": "예정",
"colComplianceRate": "순응률",
"moodTitle": "기분 추이",
- "moodSummary": "평균: {avg} / 5.0 · 기록: {count}건 · 범위: {min}–{max}",
+ "moodSummary": "평균 {avg} / 5.0, 기록 {count}건, 범위 {min}–{max}",
"colMood": "기분",
"colCount": "횟수",
"colShare": "비율",
@@ -270,8 +272,8 @@
"typeGlucoseRandom": "혈당 (임의)",
"typeGlucoseBedtime": "혈당 (취침 전)",
"typeGlucoseUnspecified": "혈당 (미지정)",
- "glucoseClassificationTitle": "혈당 — 참고 범위 (ADA 2024 / DDG)",
- "glucoseRow": "{label}: 평균 {avg} {unit} (목표 {rangeMin}–{rangeMax}) — {class}",
+ "glucoseClassificationTitle": "혈당: 참고 범위 (ADA 2024 / DDG)",
+ "glucoseRow": "{label}: 평균 {avg} {unit} (목표 {rangeMin}–{rangeMax}), {class}",
"glucoseInTarget": "목표 범위 내",
"glucoseBelowTarget": "목표 미만",
"glucoseAboveTarget": "목표 초과",
@@ -376,12 +378,12 @@
"backToDashboard": "대시보드로 돌아가기"
},
"offlineBanner": {
- "message": "연결 없음 — 온라인으로 돌아올 때까지 저장할 수 없어요.",
- "saveFailed": "저장하지 못했어요 — 오프라인 상태예요. 온라인으로 돌아온 뒤 다시 시도해 주세요.",
+ "message": "연결이 없어요. 온라인으로 돌아올 때까지 저장할 수 없어요.",
+ "saveFailed": "저장하지 못했어요. 오프라인 상태예요. 온라인으로 돌아온 뒤 다시 시도해 주세요.",
"showingCached": "마지막으로 동기화한 데이터를 보여주고 있어요."
},
"demoBanner": {
- "message": "데모 모드 — 변경 내용은 저장되지 않아요."
+ "message": "데모 모드: 변경 내용은 저장되지 않아요."
},
"nav": {
"dashboard": "대시보드",
@@ -407,6 +409,7 @@
"mood": "기분",
"coach": "코치",
"skipToContent": "본문으로 건너뛰기",
+ "breadcrumb": "현재 위치",
"sidebar": "사이드바",
"mainNavigation": "주 메뉴",
"mobileNavigation": "모바일 메뉴",
@@ -426,7 +429,9 @@
"workout": "운동",
"workoutDescription": "기기가 기록하지 못한 운동",
"symptom": "증상",
- "symptomDescription": "직접 정한 증상과 강도"
+ "symptomDescription": "직접 정한 증상과 강도",
+ "lifeEventDescription": "이사, 출산, 새 직장",
+ "lifeEvent": "삶의 사건"
},
"cycle": "생리 주기",
"labs": "검사",
@@ -435,7 +440,8 @@
"illness": "아픈 날",
"documents": "문서",
"vaccinations": "예방접종",
- "mentalWellbeing": "정신 건강"
+ "mentalWellbeing": "정신 건강",
+ "timeline": "타임라인"
},
"auth": {
"login": "로그인",
@@ -452,14 +458,13 @@
"hasAccount": "이미 계정이 있나요?",
"loginFailed": "로그인 실패",
"registerFailed": "회원가입 실패",
- "invitedBanner": "초대를 받았어요 — 회원가입이 닫혀 있어도 계정을 만들 수 있어요.",
+ "invitedBanner": "초대를 받았어요. 회원가입이 닫혀 있어도 계정을 만들 수 있어요.",
"alreadySignedIn": {
"title": "이미 로그인되어 있습니다",
"body": "{username} 계정으로 로그인되어 있습니다. 초대는 새 계정을 만듭니다. 초대를 수락하려면 로그아웃하세요.",
"signOut": "로그아웃하고 초대 수락하기"
},
"createAccount": "계정 만들기",
- "newAccount": "새 계정 만들기",
"backToPasskey": "패스키로 돌아가기",
"passkeySetupLater": "패스키는 설정에서 언제든 등록할 수 있어요.",
"passwordMinLength": "최소 12자",
@@ -512,6 +517,37 @@
"fixLan": "TLS 없이 홈 네트워크에서 쓰는 서버: 설정에서 SESSION_COOKIE_SECURE=false로 지정하고 다시 시작하세요.",
"fixHttps": "HTTPS 프록시 뒤의 서버: 대신 https:// 주소를 열고, 프록시가 X-Forwarded-Proto: https를 전달하는지 확인하세요.",
"localhostNote": "이 페이지는 http://localhost에 있지만 서버는 로그인 쿠키를 https://에만 설정합니다. 대부분의 브라우저는 localhost에서 이를 허용합니다. 로그인 후 계속 여기로 돌아온다면 SESSION_COOKIE_SECURE=false로 지정하세요."
+ },
+ "claim": {
+ "title": "내 건강 기록 넘겨받기",
+ "titleNamed": "{name} 님, 환영합니다",
+ "description": "지금까지는 다른 사람이 건강 기록을 대신 관리했습니다. 로그인 방법을 정하면 이 기록이 내 계정이 됩니다. 아무것도 복사되거나 옮겨지지 않습니다.",
+ "loading": "링크를 확인하는 중…",
+ "invalid": "이 링크는 더 이상 작동하지 않습니다",
+ "invalidHint": "알 수 없거나, 만료되었거나, 철회되었거나, 이미 사용된 링크입니다. 보낸 사람에게 새 링크를 요청하세요.",
+ "oidcOnly": "이 서버는 통합 로그인(SSO)만 허용하므로 아직 여기서 프로필을 넘겨받을 수 없습니다.",
+ "signedInTitle": "먼저 로그아웃하세요",
+ "signedInBody": "{username} 계정으로 로그인되어 있습니다. 이 링크는 다른 사람의 로그인을 만드는 링크이므로 사용하기 전에 로그아웃하세요.",
+ "signOut": "로그아웃하고 계속",
+ "guardiansTitle": "당분간 접근 권한을 유지하는 사람",
+ "guardiansHint": "기록을 관리해 온 사람들이 제안한 내용입니다. 로그인한 직후에 직접 결정합니다.",
+ "guardianUnnamed": "보호자",
+ "usernameHint": "영문자, 숫자, _ 또는 - 로 3~30자.",
+ "usernameReserved": "managed- 로 시작하는 사용자 이름은 예약되어 있습니다.",
+ "emailHint": "비밀번호를 잊었을 때 다시 들어올 수 있도록 필요합니다.",
+ "consentNote": "안내 사항과 AI 사용은 직접 확인합니다. 그 전까지는 기록의 어떤 내용도 AI 제공자에게 전송되지 않습니다.",
+ "expires": "이 링크는 {date}까지 유효합니다.",
+ "submit": "넘겨받기",
+ "submitting": "넘겨받는 중…",
+ "errorTaken": "이미 사용 중인 사용자 이름 또는 이메일 주소입니다.",
+ "errorRateLimit": "시도 횟수가 너무 많습니다. 몇 분 후 다시 시도하세요.",
+ "errorFailed": "처리하지 못했습니다. 잠시 후 다시 시도하세요.",
+ "errorOffline": "서버에 연결하지 못했습니다. 연결을 확인하고 다시 시도하세요."
+ },
+ "inviteInvalid": {
+ "title": "유효하지 않은 초대예요",
+ "description": "이 초대 링크가 불완전하거나 잘못 복사되었어요. 초대한 분께 새 링크를 요청해 주세요.",
+ "toLogin": "로그인으로 이동"
}
},
"dashboard": {
@@ -543,17 +579,17 @@
"sleepShort": "수면",
"stepsShort": "걸음 수",
"vo2MaxShort": "VO₂ max",
- "bpInTargetWindow90": "목표 내 혈압 · 90일",
- "bpInTargetWindowDynamic": "목표 내 혈압 · {days}일",
+ "bpInTargetWindow90": "목표 내 혈압 (90일)",
+ "bpInTargetWindowDynamic": "목표 내 혈압 ({days}일)",
"bpInTargetCollecting": "데이터 모으는 중",
"medications": "복약",
"quickAddMeasurement": "측정 기록",
"quickAddMood": "기분 기록",
"quickAddMedicationIntake": "복용 기록",
"quickAddWorkout": "운동 기록",
+ "quickAddLifeEvent": "삶의 사건 기록",
"customizeDashboard": "대시보드 맞춤 설정",
"medicationIntakeQuickAdd": {
- "sheetTitle": "복용 기록",
"sheetDescription": "몇 번의 탭으로 약과 용량, 시각을 기록해요.",
"medicationLabel": "약",
"medicationPlaceholder": "약 선택",
@@ -666,7 +702,8 @@
"body": "키 사본이 없으면 다시 설치한 뒤 데이터를 복구할 수 없습니다. 1분이면 됩니다.",
"action": "단계 열기"
},
- "quickAddSymptom": "증상 기록"
+ "quickAddSymptom": "증상 기록",
+ "todayCardVisible": "상단 카드 표시"
},
"measurements": {
"title": "측정",
@@ -738,7 +775,7 @@
"formReset": "양식 초기화",
"saveError": "저장하지 못했어요",
"duplicateTimestamp": "선택한 시각에 같은 유형의 측정이 이미 있어요.",
- "serverOwnedSource": "연동된 출처의 값은 수정할 수 없어요 — 시간과 메모는 수정할 수 있어요.",
+ "serverOwnedSource": "연동된 출처의 값은 수정할 수 없어요. 시간과 메모는 수정할 수 있어요.",
"deleteError": "삭제하지 못했어요",
"deletedToast": "측정을 삭제했어요",
"restoredToast": "측정을 복원했어요",
@@ -833,7 +870,8 @@
"durationHoursOnly": "{hours}시간",
"durationMinutesOnly": "{minutes}분",
"durationInvalid": "7.5, 7:30, 7h 30m처럼 시간을 입력해 주세요.",
- "durationTooLong": "한 번에 24시간보다 길게 입력할 수 없어요."
+ "durationTooLong": "한 번에 24시간보다 길게 입력할 수 없어요.",
+ "sourceHealthConnect": "Health Connect"
},
"metricCatalog": {
"title": "HealthLog가 기록할 수 있는 것",
@@ -849,7 +887,7 @@
},
"watchdog": {
"title": "자동 감시",
- "description": "HealthLog는 백그라운드에서도 조용히 지켜봐요 — 연결된 워치의 불규칙 리듬·호흡 장애 알림, 기준값 이탈 건강 상태 경고, 최근 두 번의 검사 패널 사이의 눈에 띄는 변화를 보고, 특별한 게 있을 때만 카드를 보여줘요. 이런 원본이 하나라도 연결돼 있다면 카드가 없는 건 기능 누락이 아니라 좋은 신호예요. 아직 아무것도 연결하지 않았다면 감시할 대상 자체가 없어요."
+ "description": "HealthLog는 백그라운드에서도 조용히 지켜봐요. 연결된 워치의 불규칙 리듬·호흡 장애 알림, 기준값 이탈 건강 상태 경고, 최근 두 번의 검사 패널 사이의 눈에 띄는 변화를 보고, 특별한 게 있을 때만 카드를 보여줘요. 이런 원본이 하나라도 연결돼 있다면 카드가 없는 건 기능 누락이 아니라 좋은 신호예요. 아직 아무것도 연결하지 않았다면 감시할 대상 자체가 없어요."
}
},
"mood": {
@@ -1031,7 +1069,7 @@
},
"sectionMoreTags": "태그 더 보기",
"sectionNoteDetails": "메모·상세",
- "quickTagsHelp": "자주 쓰는 태그 — 한 번만 누르면 돼요",
+ "quickTagsHelp": "자주 쓰는 태그: 한 번만 누르면 돼요",
"quickTagsExpand": "더 보기",
"loadError": "기분 기록을 불러오지 못했어요.",
"sectionLevelA": "마음 상태",
@@ -1191,7 +1229,8 @@
"bodyLogged": "아픈 날 일지에 증상 {count}개를 기록했어요",
"bodyNotLogged": "이 날은 아픈 날 일지에 기록된 게 없어요",
"openIllness": "열기"
- }
+ },
+ "sourceNotification": "알림"
},
"medications": {
"title": "복약",
@@ -1209,8 +1248,8 @@
"dialogDescription": "지금 복용할 약이에요. 확인하면 모두 복용으로 기록해요.",
"confirm": "모두 복용 ({count})",
"successToast": "{count}건 복용을 기록했어요",
- "partialToast": "{taken}건 기록, {failed}건 실패 – 다시 시도해 주세요",
- "failedToast": "복용을 기록하지 못했어요 – 다시 시도해 주세요"
+ "partialToast": "{taken}건 기록, {failed}건 실패. 다시 시도해 주세요",
+ "failedToast": "복용을 기록하지 못했어요. 다시 시도해 주세요"
},
"logIntake": {
"dialogTitle": "복용 기록",
@@ -1284,6 +1323,14 @@
"disabledToast": "복용 기록 꺼짐",
"toggleFailed": "복용 기록 설정을 바꾸지 못했어요."
},
+ "autoMarkTaken": {
+ "label": "복용 자동 기록",
+ "helper": "HealthLog가 예정된 각 복용을 해당 시간에 복용한 것으로 기록합니다. 복용은 언제든 직접 변경하거나 건너뛸 수 있습니다. 알림은 보내지 않습니다.",
+ "cardHint": "자동으로 기록됨",
+ "enabledToast": "이제 복용이 자동으로 기록됩니다",
+ "disabledToast": "더 이상 복용이 자동으로 기록되지 않습니다",
+ "toggleFailed": "자동 기록을 변경하지 못했습니다."
+ },
"withoutNotification": "알림 없음",
"pausedSince": "일시중지 시작",
"takeNow": "지금 복용",
@@ -1299,7 +1346,7 @@
"complianceInfoLabel": "복약 순응도란?",
"lastIntake": "마지막 복용:",
"nextIntake": "다음 복용:",
- "nextIntakeOverdue": "시간 지남 · {time} — 지금도 복용할 수 있어요",
+ "nextIntakeOverdue": "{time}부터 지남, 지금도 복용할 수 있어요",
"dayStreak": "일 연속 기록",
"dayStreakOne": "일 연속 기록",
"cycleDueToday": "오늘 예정",
@@ -1308,10 +1355,10 @@
"skipped": "건너뜀",
"intakeToastTaken": "복용으로 기록함: {name}",
"intakeToastSkipped": "건너뜀: {name}",
- "intakeToastFailed": "기록하지 못했어요: {name} — 다시 시도해 주세요",
+ "intakeToastFailed": "기록하지 못했어요: {name}. 다시 시도해 주세요",
"intakeUndo": "실행 취소",
"intakeUndone": "기록을 되돌렸어요",
- "intakeUndoFailed": "되돌리지 못했어요 — 기록에서 수정해 주세요",
+ "intakeUndoFailed": "되돌리지 못했어요. 기록에서 수정해 주세요",
"glp1LastInjection": "{label}",
"glp1NextInjectionDays": "{label} ({days}일 후)",
"glp1NextInjectionToday": "오늘",
@@ -1393,10 +1440,10 @@
"error": {
"empty": "설명을 먼저 입력해 주세요.",
"tooLong": "설명은 2000자 이내로 적어 주세요.",
- "rateLimit": "요청이 너무 많아요 — 잠시 후 다시 시도해 주세요.",
- "noProvider": "AI 제공자가 설정되지 않았어요 — 설정에서 등록해 주세요.",
- "budget": "오늘 AI 사용 한도를 다 썼어요 — 내일 다시 시도해 주세요.",
- "network": "AI 제공자에 연결하지 못했어요 — 다시 시도해 주세요.",
+ "rateLimit": "요청이 너무 많아요. 잠시 후 다시 시도해 주세요.",
+ "noProvider": "AI 제공자가 설정되지 않았어요. 설정에서 등록해 주세요.",
+ "budget": "오늘 AI 사용 한도를 다 썼어요. 내일 다시 시도해 주세요.",
+ "network": "AI 제공자에 연결하지 못했어요. 다시 시도해 주세요.",
"consent": "AI로 설명을 읽으려면 먼저 동의가 필요합니다.",
"unavailable": "지금 여기에서는 AI로 설명을 읽을 수 없습니다."
}
@@ -1470,7 +1517,7 @@
"night": "밤"
},
"max": {
- "reached": "최대 개수에 도달했어요 — 다른 시각을 삭제한 뒤 추가하세요."
+ "reached": "최대 개수에 도달했어요. 다른 시각을 삭제한 뒤 추가하세요."
}
},
"courseWindow": {
@@ -1546,7 +1593,7 @@
},
"dosesPerUnitLabel": "단위당 투여 횟수",
"dosesPerUnitPlaceholder": "예: 4",
- "dosesPerUnitHint": "펜/바이알당 투여 횟수 — 재고 추적이 필요 없으면 비워 두세요.",
+ "dosesPerUnitHint": "펜/바이알당 투여 횟수예요. 재고 추적이 필요 없으면 비워 두세요.",
"short": "용량",
"unitsPerDoseLabel": "1회 복용 단위 수",
"unitsPerDoseHint": "예: 4mg 용량이면 2mg 2정, 반으로 나눠 먹으면 ½정"
@@ -1558,7 +1605,7 @@
},
"step5": {
"title": "얼마나 자주 복용하나요?",
- "subline": "주기를 고르세요 — 세부 설정은 다음 단계에서 해요.",
+ "subline": "주기를 고르세요. 세부 설정은 다음 단계에서 해요.",
"short": "주기"
},
"step6": {
@@ -1580,7 +1627,7 @@
},
"step7": {
"title": "하루 중 언제 복용하나요?",
- "subline": "하루 한 번 이상 — 알림은 이 목록을 따라요.",
+ "subline": "하루 한 번 이상이에요. 알림은 이 목록을 따라요.",
"presetsLabel": "시간대 프리셋",
"presets": {
"morning": "아침",
@@ -1588,7 +1635,7 @@
"evening": "저녁",
"night": "밤"
},
- "overrideHint": "선택 사항 — 이 시간대에만 다른 용량이나 단위 수를 지정해요. 비워 두면 약의 기본값을 써요.",
+ "overrideHint": "선택 사항: 이 시간대에만 다른 용량이나 단위 수를 지정해요. 비워 두면 약의 기본값을 써요.",
"doseOverrideLabel": "이 시간대 용량 (선택)",
"doseOverridePlaceholder": "예: 반 알",
"unitsOverrideLabel": "이 시간대에 복용하는 단위 수",
@@ -1596,10 +1643,10 @@
"short": "시간"
},
"step8": {
- "title": "다 됐어요 — HealthLog가 알림을 보낼까요?",
+ "title": "다 됐어요. HealthLog가 알림을 보낼까요?",
"subline": "저장하기 전에 아래 내용을 확인해 주세요.",
"remindersLabel": "알림 켜기",
- "remindersDescription": "푸시, Telegram, ntfy — 설정에 따라 보내요.",
+ "remindersDescription": "푸시, Telegram, ntfy 중 설정에 따라 보내요.",
"noChannelHint": "알림을 보내려면 알림 채널이 필요한데, 지금은 켜진 채널이 없어요.",
"noChannelHintLink": "알림 설정하기",
"short": "완료"
@@ -1684,7 +1731,7 @@
}
},
"errors": {
- "submitFailed": "약을 저장하지 못했어요 — 다시 시도해 주세요."
+ "submitFailed": "약을 저장하지 못했어요. 다시 시도해 주세요."
}
},
"doseStrength": {
@@ -1700,11 +1747,11 @@
"empty": "아직 증량 단계가 기록되지 않았어요. 날짜가 있는 용량 변경을 추가하면 계획을 그려 줘요.",
"youAreHere": "현재 위치",
"doseStep": "{dose} {unit}",
- "plannedFor": "예정 · {date}"
+ "plannedFor": "{date} 예정"
},
"injectionSiteBodyOutlineAriaLabel": "신체 윤곽",
"injectionSitePickerAriaLabel": "주사 부위 선택기",
- "injectionSiteRecommendedAriaLabel": "{site} — 다음 권장 부위",
+ "injectionSiteRecommendedAriaLabel": "{site}: 다음 권장 부위",
"injectionSiteLegendAriaLabel": "마커 범례",
"injectionSiteLegendRecommended": "다음 권장 부위",
"injectionSiteLegendLastUsed": "마지막 사용 부위",
@@ -1740,6 +1787,7 @@
"sourceImport": "불러오기",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "{origin} 경유",
+ "intakeRecordedAutomatically": "자동으로 기록됨",
"intakeHistoryEmptyTitle": "아직 복용 기록이 없어요",
"intakeHistoryEmptyDescription": "일일 복용 페이지에서 용량을 기록하면 이력이 쌓여요.",
"intakeHistoryEmptyAction": "일일 복용 열기",
@@ -1751,7 +1799,7 @@
"intakeHistoryStatusSkipped": "건너뜀",
"intakeHistorySortByTaken": "복용 시각순 정렬",
"intakeHistorySortByScheduled": "예정 시각순 정렬",
- "intakeHistoryPageInfo": "{page}/{total}페이지 · {count}개 기록",
+ "intakeHistoryPageInfo": "{page}/{total}페이지, 기록 {count}개",
"intakeHistoryPrev": "이전",
"intakeHistoryNext": "다음",
"glp1": {
@@ -1780,7 +1828,7 @@
"emptyState": "아직 복용 기록이 없어요.",
"emptyStateCta": "용량을 기록하면 예상 농도 곡선이 그려져요.",
"estimateNote": "EMA가 공개한 집단 약동학 자료를 바탕으로 한 참고용 추정치예요. 실제 측정값이 아니에요.",
- "unknownDrug": "{name} — EMA 승인 GLP-1 목록에 없어서 HealthLog가 집단 약동학 곡선을 그릴 수 없어요."
+ "unknownDrug": "{name}: EMA 승인 GLP-1 목록에 없어서 HealthLog가 집단 약동학 곡선을 그릴 수 없어요."
}
},
"detail": {
@@ -1792,7 +1840,7 @@
"cadence": {
"oneShotOn": "{date}에 1회 복용해요.",
"oneShotPending": "1회 복용 예정이에요.",
- "asNeeded": "필요시 복용 — 정해진 일정 없어요."
+ "asNeeded": "필요시 복용: 정해진 일정 없어요."
},
"intake": {
"title": "복용 기록",
@@ -1819,7 +1867,7 @@
"dialogTitle": "복용 기록 편집",
"takenAtLabel": "복용 시각",
"scheduledForHint": "예정: {dateTime}",
- "farFromScheduledWarning": "예정 시각과 많이 떨어져 있어요 — 날짜를 확인해 주세요.",
+ "farFromScheduledWarning": "예정 시각과 많이 떨어져 있어요. 날짜를 확인해 주세요.",
"skippedLabel": "건너뜀",
"save": "저장",
"cancel": "취소",
@@ -1851,7 +1899,7 @@
"markAsTaken": "복용함으로 표시",
"markAsSkipped": "건너뜀으로 표시",
"adHocTag": "일정 외",
- "takenDetail": "예정 {planned} · 복용 {taken}",
+ "takenDetail": "예정 {planned}, 복용 {taken}",
"doseTaken": "용량: {dose}",
"status": {
"taken_on_time": "정시",
@@ -1863,10 +1911,10 @@
},
"addDialog": {
"title": "복용 기록 추가",
- "description": "복용을 기록해요 — 지난 날짜나 일정 외 복용도 괜찮아요.",
+ "description": "복용을 기록해요. 지난 날짜나 일정 외 복용도 괜찮아요.",
"takenAtLabel": "복용 시각",
"skippedLabel": "건너뜀",
- "nudgeHint": "{slot} 시간대와 가까워요 — 저장할 때 그 시간대에 배정할지 물어볼게요.",
+ "nudgeHint": "{slot} 시간대와 가까워요. 저장할 때 그 시간대에 배정할지 물어볼게요.",
"cancel": "취소",
"submit": "저장"
},
@@ -1935,7 +1983,7 @@
"title": "복약 종료",
"helper": "알림을 끄고 이 약을 종료 상태로 표시해요. 기록은 계속 볼 수 있어요.",
"button": "종료",
- "dialogTitle": "종료: {name} — 알림을 끄고, 기록은 계속 볼 수 있어요",
+ "dialogTitle": "종료: {name} (알림을 끄고, 기록은 계속 볼 수 있어요)",
"dialogBody": "알림이 더 이상 오지 않아요. 이전 기록은 타임라인에 남아요.",
"toast": "복약을 종료했어요",
"failed": "복약을 종료하지 못했어요."
@@ -1953,7 +2001,7 @@
"title": "약 삭제",
"helper": "약과 관련된 모든 기록을 삭제해요.",
"button": "삭제",
- "dialogTitle": "{name} — 삭제할까요?",
+ "dialogTitle": "{name}: 삭제할까요?",
"dialogBody": "약과 일정, 복용 기록, API 토큰, 단계 설정, 알림이 모두 영구히 삭제돼요.",
"toast": "약을 삭제했어요",
"failed": "약을 삭제하지 못했어요."
@@ -1973,7 +2021,7 @@
"copyFailed": "복사하지 못했어요.",
"tokenCopied": "토큰을 복사했어요",
"copyToken": "토큰 복사",
- "mintedHint": "지금 토큰을 복사해 주세요 — 한 번만 보여줘요."
+ "mintedHint": "지금 토큰을 복사해 주세요. 한 번만 보여줘요."
},
"advanced": {
"dataPortability": {
@@ -2002,7 +2050,6 @@
"bestand": "재고",
"verlauf": "기록",
"injektion": "주사",
- "api": "API",
"erweitert": "고급",
"wirkung": "효과"
},
@@ -2014,9 +2061,9 @@
"statusTitle": "상태",
"nextDoseLabel": "다음 복용",
"nextDoseNone": "예정된 복용이 없어요.",
- "nextDoseAsNeeded": "필요시 복용 — 예정된 복용이 없어요.",
+ "nextDoseAsNeeded": "필요시 복용: 예정된 복용이 없어요.",
"nextDoseNotTracked": "복용을 기록하지 않아요. 기록으로만 남겨요.",
- "pausedHint": "일시중지 — 알림이 오지 않아요.",
+ "pausedHint": "일시중지: 알림이 오지 않아요.",
"reminderLabel": "알림",
"reminderOn": "켜짐",
"reminderOff": "꺼짐",
@@ -2087,7 +2134,7 @@
"packagingDefaultPack": "기본 포장 크기: {units}개",
"packagingNoDefaultPack": "기본 포장 크기가 설정되지 않았어요",
"packagingEdit": "편집",
- "packagingDescription": "1회분에 몇 개를 쓰는지, 새 포장이 보통 몇 개인지 설정해요 — 예를 들어 제조사가 바뀌어 포장 크기가 달라졌을 때요.",
+ "packagingDescription": "1회분에 몇 개를 쓰는지, 새 포장이 보통 몇 개인지 설정해요. 예를 들어 제조사가 바뀌어 포장 크기가 달라졌을 때요.",
"packagingUnitsPerDoseLabel": "1회분당 개수",
"packagingUnitsPerDoseHelper": "예: 4mg 복용에 2mg 정제 2개, 한 알 반이면 1.5 (0 초과, 100 이하). 이미 기록된 복용은 그대로예요.",
"packagingDefaultPackLabel": "기본 포장 크기 (개)",
@@ -2138,11 +2185,11 @@
"hideEras": "이전 구간 숨기기",
"addEra": "이전 구간 추가",
"addEraTitle": "이전 구간 추가",
- "addEraDescription": "예전에 이 약을 복용하던 시각을 기록해요 — 예를 들어 여기서 일정을 바꾸기 전 시각이요.",
+ "addEraDescription": "예전에 이 약을 복용하던 시각을 기록해요. 예를 들어 여기서 일정을 바꾸기 전 시각이요.",
"editAria": "구간 편집",
"editEraTitle": "구간 편집",
"editEraDescription": "이 구간의 복용 시각과 기간을 조정해요.",
- "archivedEditHint": "이 구간은 자동으로 기록됐어요 — 수정하면 기록된 이력이 바로잡히고, 원본은 내부에 기록으로 남아요.",
+ "archivedEditHint": "이 구간은 자동으로 기록됐어요. 수정하면 기록된 이력이 바로잡히고, 원본은 내부에 기록으로 남아요.",
"updatedToast": "구간을 수정했어요",
"updateError": "구간을 수정하지 못했어요. 다른 구간과 겹치지 않고, 현재 일정보다 먼저 끝나야 해요.",
"timesLabel": "복용 시각",
@@ -2168,7 +2215,7 @@
"saved": "일정을 저장했어요",
"failed": "일정을 저장하지 못했어요",
"window": {
- "intro": "복용마다 정시로 인정되는 범위를 정해요 — 정확한 시각(±1h)이나 07:00–09:00 같은 직접 지정 범위로요.",
+ "intro": "복용마다 정시로 인정되는 범위를 정해요. 정확한 시각(±1h)이나 07:00–09:00 같은 직접 지정 범위로요.",
"explicitToggle": "직접 범위 지정",
"startLabel": "범위 시작",
"endLabel": "범위 종료",
@@ -2181,10 +2228,10 @@
}
},
"logInjectionSiteTitle": "주사 부위 기록",
- "logInjectionSiteDescription": "{name} — 어디에 주사했나요? 선택 사항이라 건너뛰어도 돼요.",
+ "logInjectionSiteDescription": "{name}: 어디에 주사했나요? 선택 사항이라 건너뛰어도 돼요.",
"logInjectionSiteSkip": "건너뛰기",
"logInjectionSiteConfirm": "부위 저장",
- "logInjectionSiteNoneAvailable": "쓸 수 있는 주사 부위가 없어요 — 설정에서 모든 부위를 제외했어요.",
+ "logInjectionSiteNoneAvailable": "쓸 수 있는 주사 부위가 없어요. 설정에서 모든 부위를 제외했어요.",
"logInjectionSiteSaveFailed": "주사 부위를 저장하지 못했어요. 다시 시도해 주세요.",
"perSlotUnits": "1회분당 {units}",
"trackInjectionSitesToggle": "주사 부위 기록하기",
@@ -2209,10 +2256,9 @@
"tableStockDoses": "{count}회분",
"tableStockRunway": "약 {days}일 남음",
"cardLowStockRunway": "재고: 약 {days}일",
- "takeActionFor": "복용 – {name}",
- "skipActionFor": "건너뛰기 – {name}",
- "reorderTitle": "순서 조정",
- "reorderDescription": "약의 순서를 정하세요 – 카드 보기와 표 보기 모두에 적용돼요.",
+ "takeActionFor": "복용: {name}",
+ "skipActionFor": "건너뛰기: {name}",
+ "reorderDescription": "카드 보기와 표 보기 모두에 적용되는 약의 순서를 정하세요.",
"reorderSaved": "순서를 저장했어요",
"reorderSaveFailed": "순서를 저장하지 못했어요",
"customize": "복약 맞춤 설정",
@@ -2231,7 +2277,7 @@
"levelShift": "{date} 무렵에 수준 변화가 감지됐어요.",
"plateau": {
"title": "현재 용량에서 체중 정체",
- "evidence": "최근 {window}일 동안 체중이 {delta}{unit} 움직였어요({readings}개 측정값) — {dose}로 {days}일."
+ "evidence": "최근 {window}일 동안 체중이 {delta}{unit} 움직였어요({readings}개 측정값). {dose}로 {days}일째예요."
},
"retarget": {
"label": "맞는 측정값이 아닌가요?",
@@ -2241,16 +2287,16 @@
"error": "변경 내용을 저장하지 못했어요. 다시 시도해 주세요."
},
"beforeAfter": {
- "summary": "시작 전 평균 {before} → 이후 평균 {after} — {delta} 변화.",
+ "summary": "시작 전 평균 {before} → 이후 평균 {after}, {delta} 변화.",
"counts": "시작 전 {beforeCount}개, 이후 {afterCount}개 측정값을 기준으로 했어요.",
- "needBefore": "시작 전 측정값이 아직 부족해요 — 비교하려면 전후로 약 {weeks}주씩 필요해요.",
- "needAfter": "시작 이후 측정값이 아직 부족해요 — 비교하려면 전후로 약 {weeks}주씩 필요해요.",
+ "needBefore": "시작 전 측정값이 아직 부족해요. 비교하려면 전후로 약 {weeks}주씩 필요해요.",
+ "needAfter": "시작 이후 측정값이 아직 부족해요. 비교하려면 전후로 약 {weeks}주씩 필요해요.",
"needStart": "시작일을 설정하면 복용 전후 측정값을 비교할 수 있어요.",
- "needData": "측정값이 아직 부족해요 — 비교하려면 시작 전후로 약 {weeks}주씩 필요해요."
+ "needData": "측정값이 아직 부족해요. 비교하려면 시작 전후로 약 {weeks}주씩 필요해요."
},
"insights": {
"title": "약 효과",
- "subtitle": "약이 목표로 하는 측정값과 무엇이 함께 움직였는지 보여줘요 — 연관성일 뿐 판단이 아니에요.",
+ "subtitle": "약이 목표로 하는 측정값과 무엇이 함께 움직였는지 보여줘요. 연관성일 뿐 판단이 아니에요.",
"link": "효과 보기 열기",
"empty": "약과 목표 측정값을 연결하기에는 데이터가 아직 부족해요."
},
@@ -2284,7 +2330,7 @@
"reasonMissingMedication": "행에 약 이름이 없어요",
"reasonUnreadableDosage": "복용량이 숫자가 아니에요",
"reasonUnreadableRow": "행의 열 개수가 머리글보다 적어요",
- "reasonStatusNoDoseInformation": "파일에 그 복용에 대한 기록이 없어요 — 응답도 없었고 기록된 것도 없어요",
+ "reasonStatusNoDoseInformation": "파일에 그 복용에 대한 기록이 없어요. 응답도 없었고 기록된 것도 없어요",
"reasonStatusReminderEvent": "행이 미뤄 둔 알림에 관한 내용이라 복용에 대해 알려주는 게 없어요",
"reasonStatusNotificationNotSent": "행은 알림이 발송되지 않았다는 기록이라 복용이 아니라 앱에 관한 내용이에요"
},
@@ -2356,7 +2402,7 @@
},
"charts": {
"loadingLabel": "차트 불러오는 중…",
- "loadingSlowHint": "분석을 계산하는 중이에요 — 잠시 걸릴 수 있어요.",
+ "loadingSlowHint": "분석을 계산하는 중이에요. 잠시 걸릴 수 있어요.",
"weight": "체중",
"bloodPressure": "혈압",
"pulse": "맥박",
@@ -2429,13 +2475,15 @@
"loadErrorTitle": "차트를 불러오지 못했어요",
"loadErrorReload": "다시 불러오기",
"sparseDataCaption": "날이 쌓이면 추이가 채워져요.",
+ "tooltipHourlyMean": "{metric} (시간별 평균)",
"dataTable": {
"toggle": "데이터 포인트 ({count})",
"columnDate": "날짜",
"noValue": "값 없음",
"captionDay": "{metric} 하루당 한 행, 차트가 그리는 값과 같아요.",
- "captionWeek": "{metric} 한 주당 한 행, 그 주 일별 평균 — 차트가 그리는 값과 같아요.",
- "captionMonth": "{metric} 한 달당 한 행, 그달 일별 평균 — 차트가 그리는 값과 같아요."
+ "captionWeek": "{metric} 한 주당 한 행, 그 주 일별 평균이에요. 차트가 그리는 값과 같아요.",
+ "captionMonth": "{metric} 한 달당 한 행, 그달 일별 평균이에요. 차트가 그리는 값과 같아요.",
+ "hourlyMeanNote": "{metric}은(는) 하루의 평균이며 각 시간을 한 번씩만 반영하므로, 운동이 하루의 나머지보다 더 크게 반영되지 않습니다."
},
"personalBaseline": "내 평소값",
"deltaVsBaseline": "평소값 대비 {delta}",
@@ -2467,11 +2515,13 @@
"days7Label": "7일",
"days7Title": "최근 7일",
"days30Label": "30일",
+ "daysNLabel": "{days}일",
"days30Title": "최근 30일",
"days90Label": "90일",
"days90Title": "최근 90일",
"daysAllLabel": "전체",
- "daysAllTitle": "전체 기간"
+ "daysAllTitle": "전체 기간",
+ "medicationsEmptyAction": "약 추가하기"
},
"chart": {
"overlay": {
@@ -2502,8 +2552,8 @@
},
"tooltipPrior": "이전 기간",
"unavailable": {
- "lastMonth": "비교 불가 — 지난달 데이터가 아직 없어요",
- "lastYear": "비교 불가 — 작년 데이터가 아직 없어요"
+ "lastMonth": "비교 불가: 지난달 데이터가 아직 없어요",
+ "lastYear": "비교 불가: 작년 데이터가 아직 없어요"
},
"notAvailableForCompliance": "복약 순응도는 비교 불가"
},
@@ -2515,7 +2565,9 @@
"dismissError": "이 패턴을 숨기지 못했어요.",
"restoreError": "이 패턴을 다시 표시하지 못했어요.",
"loadError": "패턴을 불러오지 못했어요.",
- "empty": "근거 기준선을 넘은 패턴이 아직 없어요."
+ "empty": "근거 기준선을 넘은 패턴이 아직 없어요.",
+ "seasonalNote": "버전 1.42부터 HealthLog는 값을 비교하기 전에 계절과 장기 추세를 먼저 걸러 냅니다. 보이는 관계는 줄어들지만, 남은 관계는 더 믿을 만합니다. 예전에 보이던 패턴 중 상당수는 그저 달력이었습니다.",
+ "seasonalNoteDismiss": "안내 닫기"
},
"correlation": {
"direction": {
@@ -2547,29 +2599,37 @@
"ENV_DAYLIGHT": "낮 길이",
"ENV_PRECIP": "강수량",
"ENV_PRESSURE_MEAN": "기압",
- "ENV_PRESSURE_DELTA": "하루 중 기압 변화"
+ "ENV_PRESSURE_DELTA": "하루 중 기압 변화",
+ "ENV_PM25": "초미세먼지(PM2.5)",
+ "ENV_OZONE_8H": "오존",
+ "ENV_POLLEN_MAX": "꽃가루"
},
"daily": {
- "high": "{behaviour} 수치가 높은 날에는 다음 날 {outcome} 수치가 {direction} 경향이 있어요 — 원인이 아니라 지켜볼 만한 패턴이에요.",
- "moderate": "지금까지의 근거로는 {behaviour} 수치가 높은 날에 다음 날 {outcome} 수치가 {direction} 것처럼 보여요 — 원인이 아니라 지켜볼 만한 패턴이에요.",
- "faint": "{behaviour} 수치가 높은 날에 다음 날 {outcome} 수치가 {direction} 아주 희미한 기미만 보여요 — 기대기엔 너무 작고, 원인은 더더욱 아니에요."
+ "high": "{behaviour} 수치가 높은 날에는 다음 날 {outcome} 수치가 {direction} 경향이 있어요. 원인이 아니라 지켜볼 만한 패턴이에요.",
+ "moderate": "지금까지의 근거로는 {behaviour} 수치가 높은 날에 다음 날 {outcome} 수치가 {direction} 것처럼 보여요. 원인이 아니라 지켜볼 만한 패턴이에요.",
+ "faint": "{behaviour} 수치가 높은 날에 다음 날 {outcome} 수치가 {direction} 아주 희미한 기미만 보여요. 기대기엔 너무 작고, 원인은 더더욱 아니에요."
},
"lab": {
- "default": "{lab} 측정값이 높은 시기에는 같은 기간의 {outcome} 수치도 {direction} 편이에요 — 원인이 아니라 임상의와 함께 지켜볼 만한 연관이에요.",
- "faint": "{lab} 측정값이 높은 시기에 같은 기간의 {outcome} 수치가 {direction} 아주 희미한 기미만 보여요 — 기대기엔 너무 작고, 원인은 아니지만 임상의에게 이야기해 볼 만한 연관이에요."
+ "default": "{lab} 측정값이 높은 시기에는 같은 기간의 {outcome} 수치도 {direction} 편이에요. 원인이 아니라 임상의와 함께 지켜볼 만한 연관이에요.",
+ "faint": "{lab} 측정값이 높은 시기에 같은 기간의 {outcome} 수치가 {direction} 아주 희미한 기미만 보여요. 기대기엔 너무 작고 원인도 아니지만, 임상의에게 이야기해 볼 만해요."
},
"hypothesis": {
"bpCompliance": {
- "support": "복약 순응도가 높은 날에 수축기 측정값이 낮은 편이에요 — 지켜볼 만한 패턴이에요.",
- "surprising": "기록에서는 순응도와 수축기 혈압이 같은 방향으로 움직여요 — 뜻밖이에요. 무언가 조정하기 전에 의사와 상의해 주세요.",
+ "support": "복약 순응도가 높은 날에 수축기 측정값이 낮은 편이에요. 지켜볼 만한 패턴이에요.",
+ "surprising": "기록에서는 순응도와 수축기 혈압이 같은 방향으로 움직여요. 뜻밖이에요. 무언가 조정하기 전에 의사와 상의해 주세요.",
"none": "이 기간에는 순응도와 수축기 혈압이 뚜렷하게 함께 움직이지 않아요."
},
"moodPulse": {
- "support": "기분이 낮은 날에 안정 시 맥박이 높은 편이에요 — 지켜볼 만한 패턴이에요.",
- "surprising": "기록에서는 기분이 좋은 날에 안정 시 맥박도 높은 편이에요 — 뜻밖이에요.",
+ "support": "기분이 낮은 날에 안정 시 맥박이 높은 편이에요. 지켜볼 만한 패턴이에요.",
+ "surprising": "기록에서는 기분이 좋은 날에 안정 시 맥박도 높은 편이에요. 뜻밖이에요.",
"none": "이 기간에는 기분과 안정 시 맥박이 뚜렷하게 함께 움직이지 않아요."
},
- "weightWeekday": "{weekday} 체중이 다른 요일 평균보다 {delta}kg {direction} 편이에요 — 지켜볼 만한 패턴이에요."
+ "weightWeekday": "{weekday} 체중이 다른 요일 평균보다 {delta}kg {direction} 편이에요. 지켜볼 만한 패턴이에요."
+ },
+ "dailySameDay": {
+ "high": "당일과 전날의 {behaviour}이(가) 높을수록 내 데이터에서는 {outcome}이(가) {direction} 경향이 있습니다. 지켜볼 만한 패턴이지 원인은 아닙니다.",
+ "moderate": "지금까지의 데이터로 보면 당일과 전날의 {behaviour}이(가) 높을수록 {outcome}이(가) {direction} 것으로 보입니다. 지켜볼 만한 패턴이지 원인은 아닙니다.",
+ "faint": "당일과 전날의 {behaviour}이(가) 높을 때 내 데이터에서 {outcome}이(가) {direction} 기미가 아주 약하게 보일 뿐입니다. 의지하기엔 너무 작고, 원인은 아닙니다."
}
},
"noAnalysisYet": "아직 평가가 없어요.",
@@ -2681,7 +2741,7 @@
"referenceSleep": "하룻밤 최소 {low}분, 더 자는 건 감점하지 않아요",
"referenceAdiposity": "허리-키 비율 {high} 미만",
"referenceHba1c": "HbA1c {low}~{high}%",
- "referenceDetail": "{label} · {source} · {kind} · {low}~{high}",
+ "referenceDetail": "{label}: {low}~{high} ({kind}, {source})",
"openBound": "제한 없음",
"laboratoryReferenceSource": "검사 기관 참고 범위",
"personalTargetSource": "개인 목표",
@@ -2701,7 +2761,7 @@
"unavailable": "이 항목은 지금 반영 대상이 아니에요"
},
"tension": "컨디션 신호가 엇갈려요. 좋은 쪽: {positive}. 부담이 큰 쪽: {negative}.",
- "returnToBand": "{metric} — {days}일째 평소 범위 안으로 돌아와 있어요.",
+ "returnToBand": "{metric}: {days}일째 평소 범위 안으로 돌아와 있어요.",
"weightGoal": {
"title": "개인 체중 목표",
"value": "지금 {current}{unit}. 목표 {min}~{max}{unit}. 목표까지 {distance}{unit}.",
@@ -2716,7 +2776,8 @@
"coverage": {
"recommendedMet": "점수가 권장하는 세 영역을 모두 채웠어요",
"areas": "건강 영역 {required}개 중 {present}개"
- }
+ },
+ "historyLink": "추이"
},
"dailyBriefing": {
"title": "데일리 브리핑",
@@ -2775,7 +2836,7 @@
"experimentCta": "7일 실험 해보기",
"experimentTooltip": "곧 제공돼요",
"emptyTitle": "이 관계를 보려면 데이터가 더 필요해요",
- "sourceChip": "짝지은 측정값 {n}개 기준 · {window}",
+ "sourceChip": "짝지은 측정값 {n}개 기준 ({window})",
"sourceWindowLast30": "최근 30일",
"confidenceHigh": "신뢰도 높음",
"confidenceModerate": "신뢰도 보통",
@@ -2790,13 +2851,14 @@
}
},
"coach": {
- "refusalOutOfScope": "기록에 있는 건강 지표 — 측정값, 약, 기분, 추이 — 만 도와드릴 수 있어요. 그 밖의 질문에는 답할 수 없어요.",
- "refusalInjection": "제 지시를 덮어쓰려는 표현이 보였어요. 저는 HealthLog 코치이고 회원님의 건강 데이터만 정리해 드려요 — 그 범위로 질문을 다시 적어 주세요.",
- "errorBudget": "오늘 코치 사용 한도에 도달했어요. 사용량은 UTC 자정에 초기화돼요.",
+ "refusalOutOfScope": "기록에 있는 건강 지표(측정값, 약, 기분, 추이)만 도와드릴 수 있어요. 그 밖의 질문에는 답할 수 없어요.",
+ "refusalInjection": "제 지시를 덮어쓰려는 표현이 보였어요. 저는 HealthLog 코치이고 회원님의 건강 데이터만 정리해 드려요. 그 범위로 질문을 다시 적어 주세요.",
+ "errorBudget": "오늘 코치 사용 한도에 도달했어요. 사용량은 내 시간대의 자정에 초기화돼요.",
"errorProvider": "지금은 코치가 인사이트 제공자에 연결하지 못했어요. 잠시 후 다시 시도해 주세요.",
- "errorNoProvider": "코치는 기록된 건강 데이터를 읽고 그에 대한 질문에 답해요(의학적 조언은 아니에요). 그러려면 HealthLog에 AI 제공자가 필요해요 — 직접 발급한 키나 로컬 모델을 연결해 주세요.",
+ "errorDemo": "데모에서는 코치가 답하지 않아요.",
+ "errorNoProvider": "코치는 기록된 건강 데이터를 읽고 그에 대한 질문에 답해요(의학적 조언은 아니에요). 그러려면 HealthLog에 AI 제공자가 필요해요. 직접 발급한 키나 로컬 모델을 연결해 주세요.",
"errorNoProviderAction": "AI 제공자 연결",
- "errorNetwork": "인터넷에 연결되어 있지 않아요 — 다시 연결되면 시도해 주세요.",
+ "errorNetwork": "인터넷에 연결되어 있지 않아요. 다시 연결되면 시도해 주세요.",
"prefsSaveError": "코치 설정을 저장하지 못했어요. 다시 시도해 주세요.",
"errorCredentialExpired": "AI 제공자 연결이 만료됐어요. 코치를 계속 쓰려면 설정에서 다시 연결해 주세요.",
"tagline": "내 데이터에 기반한 개인 건강 코치.",
@@ -2815,7 +2877,7 @@
"thinking": "생각 중…",
"unverifiedFiguresNotice": "일부 수치는 데이터와 대조하지 못해 제외했어요.",
"composerPlaceholder": "무엇이든 물어보세요…",
- "nudgeBubbleLabel": "코치의 새 메시지 — 대화 열기",
+ "nudgeBubbleLabel": "코치의 새 메시지: 대화 열기",
"fabLabel": "코치 열기",
"readAloud": "소리내어 읽기",
"readAloudStop": "읽기 중지",
@@ -2833,14 +2895,13 @@
},
"rememberMessage": {
"action": "기억하기",
- "done": "셀프 컨텍스트에 저장했어요",
- "duplicate": "이미 셀프 컨텍스트에 있어요",
- "failed": "저장하지 못했어요. 다시 시도해 주세요."
+ "failed": "저장하지 못했어요. 다시 시도해 주세요.",
+ "alreadyKept": "코치가 이미 기억하고 있어요."
},
"guided": {
"offerTitle": "코치가 물어볼 후속 질문이 {count}개 있어요",
"offerTitleOne": "코치가 물어볼 후속 질문이 있어요",
- "offerBody": "짧은 답변이 셀프 컨텍스트를 더 정확하게 만들어요 — 여기 대화에서 한 번에 하나씩 물어볼게요.",
+ "offerBody": "짧은 답변이 셀프 컨텍스트를 더 정확하게 만들어요. 여기 대화에서 한 번에 하나씩 물어볼게요.",
"offerStart": "지금 답하기",
"offerLater": "나중에",
"offerDismiss": "다시 묻지 않기",
@@ -2850,7 +2911,7 @@
"later": "나중에",
"dismissRemaining": "다시 묻지 않기",
"answerPlaceholder": "답변을 입력하세요…",
- "summaryTitle": "다 됐어요 — 답변 고마워요!",
+ "summaryTitle": "다 됐어요. 답변 고마워요!",
"summaryAnswered": "{total}개 중 {answered}개 답변",
"summaryAdopted": "셀프 컨텍스트에 {adopted}개 추가",
"summaryAdoptedNone": "셀프 컨텍스트에 새로 추가된 건 없어요",
@@ -2862,6 +2923,7 @@
"historyEmpty": "대화가 여기에 표시돼요. 무엇이든 물어보며 시작하세요.",
"historyDeleteAria": "대화 삭제",
"historyDeleted": "대화를 삭제했어요",
+ "historyDeleteFailed": "대화를 삭제하지 못했어요.",
"rename": {
"action": "대화 이름 바꾸기",
"input": "대화 제목",
@@ -2875,7 +2937,6 @@
"sourcesFresh": "최신",
"sourcesStale": "오래됨",
"sourcesFooter": "코치는 연결한 데이터만 읽어요.",
- "evidenceLabel": "내가 보고 있는 것",
"metric": {
"bp": "혈압",
"weight": "체중",
@@ -2921,14 +2982,6 @@
"skin_temp": "피부 온도",
"workouts": "운동"
},
- "window": {
- "last7days": "최근 7일",
- "last30days": "최근 30일",
- "last90days": "최근 90일",
- "lastYear": "최근 12개월",
- "allTime": "전체 기간(상세 12개월)"
- },
- "windowLabel": "기간",
"settingsAriaLabel": "코치 설정",
"settingsTitle": "코치 설정",
"settingsDescription": "코치가 대화하는 방식을 조절해요.",
@@ -2940,9 +2993,10 @@
"settingsVerbosityBrief": "짧게",
"settingsVerbosityDefault": "기본",
"settingsVerbosityDetailed": "자세히",
- "settingsVerbosityConciseHint": "간결한 말투는 이미 답변을 짧게 유지해요 — 켜 두는 동안 답변 길이는 '짧게'로 고정돼요.",
- "settingsSourcesPointer": "코치가 참고할 데이터 묶음과 기본 분석 기간은 코치 대화의 '내가 참고하는 것' 레일에서 바로 관리해요.",
- "settingsSourcesPointerLink": "코치 열기",
+ "settingsVerbosityConciseHint": "간결한 말투는 이미 답변을 짧게 유지해요. 켜 두는 동안 답변 길이는 '짧게'로 고정돼요.",
+ "settingsSourcesSummary": "코치는 데이터 영역 {total}개 중 {count}개를 {window} 전까지 봐요.",
+ "settingsSourcesSummaryAll": "코치는 데이터 영역 {total}개 중 {count}개를 처음 기록부터 봐요.",
+ "settingsSourcesPointerLink": "코치 설정 열기",
"settingsExcludeLabel": "제외할 지표",
"settingsExcludeHint": "선택한 지표의 데이터는 코치가 보지 않아요.",
"settingsContextLabel": "포함할 맥락",
@@ -2954,34 +3008,30 @@
"settingsSave": "저장",
"settingsSaved": "저장됨",
"settingsCancel": "취소",
- "feedbackHelpful": "도움됨",
- "feedbackUnhelpful": "아쉬움",
- "feedbackThanks": "알려줘서 고마워요.",
- "feedbackError": "피드백을 저장하지 못했어요",
"dailyLimitTitle": "하루 한도 도달",
- "dailyLimitBody": "하루 한도에 도달했어요. 사용량은 UTC 자정에 초기화돼요.",
+ "dailyLimitBody": "하루 한도에 도달했어요. 사용량은 내 시간대의 자정에 초기화돼요.",
"providerRateLimitTitle": "제공자 요청 제한",
"providerRateLimitBody": "제공자가 일시적으로 요청을 제한하고 있어요. 5분쯤 뒤에 다시 시도해 주세요.",
"cluster": {
"cardio": {
"label": "심혈관",
- "hint": "혈압 · 맥박 · 심박변이도 · 안정 시와 보행 심박수"
+ "hint": "혈압, 맥박, 심박변이도, 안정 시와 보행 심박수"
},
"body": {
"label": "체성분",
- "hint": "체중 · 체지방 · 근육 · BMI"
+ "hint": "체중, 체지방, 근육, BMI"
},
"activity": {
"label": "활동",
- "hint": "걸음 수 · 에너지 · 거리 · 최대산소섭취량"
+ "hint": "걸음 수, 에너지, 거리, 최대산소섭취량"
},
"workouts": {
"label": "운동",
- "hint": "러닝 · 라이딩 · 세션"
+ "hint": "러닝, 라이딩, 세션"
},
"sleep": {
"label": "수면",
- "hint": "수면 시간 · 단계"
+ "hint": "수면 시간과 단계"
},
"mood": {
"label": "기분",
@@ -2989,60 +3039,72 @@
},
"glucose": {
"label": "혈당",
- "hint": "공복 · 식후 측정값"
+ "hint": "공복 및 식후 측정값"
},
"medication": {
"label": "복약",
- "hint": "복약 순응도 · GLP-1 맥락"
+ "hint": "복약 순응도와 GLP-1 맥락"
},
"mobility": {
"label": "이동성과 보행",
- "hint": "안정성 · 비대칭 · 보행"
+ "hint": "안정성, 비대칭, 보행"
},
"environment": {
"label": "환경",
- "hint": "소음 · 햇빛 · 피부 온도"
+ "hint": "소음, 햇빛, 피부 온도"
}
},
"settingsClustersLabel": "코치가 보는 데이터",
"settingsClustersHint": "코치가 읽을 수 있는 데이터 묶음을 고르세요. 데이터가 많을수록 답변이 풍부해지고 프롬프트도 커져요.",
"settingsClusterNoData": "아직 데이터 없음",
- "sourcesMemberCount": "지표 {count}개",
+ "sourcesMemberCountOne": "지표 1개",
+ "sourcesMemberCountFew": "지표 {count}개",
+ "sourcesMemberCountOther": "지표 {count}개",
+ "lookback": {
+ "label": "코치가 돌아볼 수 있는 기간",
+ "option": {
+ "last7days": "{days}일",
+ "last30days": "{days}일",
+ "last90days": "{days}일",
+ "lastYear": "{months}개월",
+ "allTime": "전체"
+ },
+ "detailDays": "코치는 어떤 답변에서도 {days}일보다 오래된 기록은 읽지 않아요.",
+ "detailMonths": "코치는 어떤 답변에서도 {months}개월보다 오래된 기록은 읽지 않아요.",
+ "detailAll": "기록한 모든 것: 최근 {dailyMonths}개월은 하루 단위로, 그 이전 측정값은 최대 {years}년 전까지 월별 값으로, 검사 결과는 최근 {labMonths}개월 치를 읽어요.",
+ "note": "프로필, 현재 복용 중인 약, 진행 중인 질병, 예정된 진료는 계속 보여요. 더 긴 기록이 필요한 요약은 줄이거나 빼요. 예전 대화에서 코치가 이미 한 답변에는 더 오래된 값이 남아 있을 수 있어요."
+ },
"showConversations": "대화 보기",
"hideConversations": "대화 숨기기",
"heroGreeting": "내 데이터에 대해 무엇이든 물어보세요",
"heroSubline": "추이, 약, 측정값에 대해 물어보세요.",
- "thinkingElapsed": "{seconds}초",
"thinkingDuration": "{seconds}초 동안 생각했어요",
"thinkingExpandAria": "코치가 참고한 내용 보기",
"thinkingDetail": "건강 스냅샷을 바탕으로 답했어요.",
"tokensUsed": "{count} 토큰",
- "tokensUsedWithModel": "{count} 토큰 · {model}",
"askAboutThis": "코치에게 물어보기",
"readStrip": {
"label": "평소 범위",
- "baselineWithin": "평소 범위: {low}–{high} {unit}. 오늘 값 {value} — 범위 안이에요.",
- "baselineAbove": "평소 범위: {low}–{high} {unit}. 오늘 값 {value} — 범위 위예요.",
- "baselineBelow": "평소 범위: {low}–{high} {unit}. 오늘 값 {value} — 범위 아래예요.",
- "insufficient": "아직 평소 범위를 배우는 중이에요 — 며칠 더 기록해 주세요.",
+ "baselineWithin": "평소 범위: {low}–{high} {unit}. 오늘 값 {value}, 범위 안이에요.",
+ "baselineAbove": "평소 범위: {low}–{high} {unit}. 오늘 값 {value}, 범위 위예요.",
+ "baselineBelow": "평소 범위: {low}–{high} {unit}. 오늘 값 {value}, 범위 아래예요.",
+ "sameHoursWithin": "이 시간대까지 평소 범위: {low}–{high} {unit}. 오늘 지금까지 {value}, 범위 안이에요.",
+ "sameHoursAbove": "이 시간대까지 평소 범위: {low}–{high} {unit}. 오늘 지금까지 {value}, 범위 위예요.",
+ "sameHoursBelow": "이 시간대까지 평소 범위: {low}–{high} {unit}. 오늘 지금까지 {value}, 범위 아래예요.",
+ "steadyWithin": "평소 값: {low} {unit}. 오늘 값 {value}, 평소와 같아요.",
+ "steadyAbove": "평소 값: {low} {unit}. 오늘 값 {value}, 평소보다 높아요.",
+ "steadyBelow": "평소 값: {low} {unit}. 오늘 값 {value}, 평소보다 낮아요.",
+ "latestWithin": "평소 범위: {low}–{high} {unit}. 마지막 측정({date}) 값 {value}, 범위 안이었어요.",
+ "latestAbove": "평소 범위: {low}–{high} {unit}. 마지막 측정({date}) 값 {value}, 범위보다 높았어요.",
+ "latestBelow": "평소 범위: {low}–{high} {unit}. 마지막 측정({date}) 값 {value}, 범위 아래였어요.",
+ "steadyLatestWithin": "평소 값: {low} {unit}. 마지막 측정({date}) 값 {value}, 평소와 같았어요.",
+ "steadyLatestAbove": "평소 값: {low} {unit}. 마지막 측정({date}) 값 {value}, 평소보다 높았어요.",
+ "steadyLatestBelow": "평소 값: {low} {unit}. 마지막 측정({date}) 값 {value}, 평소보다 낮았어요.",
+ "insufficient": "아직 평소 범위를 배우는 중이에요. 며칠 더 기록해 주세요.",
"driver": "{note}"
},
"scope": {
- "prefix": "코치가 이미 보고 있어요: {metric}",
- "tap": "탭해서 물어보기",
- "question": "이 지표를 설명해 주세요 — 눈여겨볼 게 있나요?"
- },
- "seeded": {
- "prefix": "오늘 살펴볼 만한 것: {signal}",
- "signal": {
- "readiness": "준비도",
- "recovery": "회복"
- },
- "question": {
- "readiness": "오늘 준비도가 평소와 달라요 — 무엇 때문일까요?",
- "recovery": "회복 상태가 달라졌어요 — 이유가 뭘까요?"
- },
- "dismiss": "닫기"
+ "prefix": "코치가 이미 보고 있어요: {metric}"
},
"settings": "설정",
"history": {
@@ -3052,39 +3114,39 @@
"groupEarlier": "이전"
},
"seed": {
- "bloodPressure": "최근 30일 혈압 추이를 설명해 주세요 — 눈여겨볼 게 있나요?",
+ "bloodPressure": "최근 30일 혈압 추이를 설명해 주세요. 눈여겨볼 게 있나요?",
"weight": "요즘 체중 추이가 어떤가요? 그 방향이 신경 쓸 만한가요?",
"bmi": "제 BMI 추이는 무엇을 말해 주나요? 어떻게 읽어야 하나요?",
- "pulse": "제 맥박 측정값을 설명해 주세요 — 평소와 다른 점이 있나요?",
+ "pulse": "제 맥박 측정값을 설명해 주세요. 평소와 다른 점이 있나요?",
"restingHr": "안정 시 심박수 추이는 어떤가요? 제 체력에 대해 뭘 알려 주나요?",
"hrv": "요즘 제 심박변이도는 회복과 스트레스에 대해 뭘 말해 주나요?",
- "sleep": "최근 수면을 설명해 주세요 — 수면 시간, 규칙성, 바꿀 만한 점까지.",
+ "sleep": "최근 수면을 설명해 주세요. 수면 시간, 규칙성, 바꿀 만한 점까지.",
"mood": "최근 몇 주 동안 제 기분에서 어떤 패턴이 보이나요?",
"medications": "제 복약 순응도는 어떤가요? 요즘 컨디션과 맞물리나요?",
"steps": "걸음 수로 볼 때 요즘 얼마나 활동적이었나요? 건강한 기준값과 비교하면 어떤가요?",
"activeEnergy": "활동 에너지 추이는 요즘 제 활동량에 대해 뭘 말해 주나요?",
"cardioFitness": "심폐 체력(VO₂ max) 추이는 무슨 뜻인가요? 어떻게 하면 좋은 방향으로 올릴 수 있나요?",
- "bloodGlucose": "최근 혈당 측정값을 설명해 주세요 — 건강한 범위 안인가요?",
+ "bloodGlucose": "최근 혈당 측정값을 설명해 주세요. 건강한 범위 안인가요?",
"oxygenSaturation": "제 산소포화도(SpO₂) 추이는 무엇을 말해 주나요?",
"respiratoryRate": "제 호흡수 추이는 건강에 대해 뭘 말해 주나요?",
- "workouts": "최근 운동을 설명해 주세요 — 운동량, 빈도, 사이사이 회복까지.",
+ "workouts": "최근 운동을 설명해 주세요. 운동량, 빈도, 사이사이 회복까지.",
"recoveryPage": "지금 제 회복 상태가 이런 이유는 뭔가요? 무엇이 영향을 주고 있나요?",
- "briefing": "오늘 브리핑을 설명해 주세요 — 무엇에 집중해야 하고, 지금 할 만한 일이 있나요?",
+ "briefing": "오늘 브리핑을 설명해 주세요. 무엇에 집중해야 하고, 지금 할 만한 일이 있나요?",
"healthScore": "제 건강 점수가 100점 중 {score}점인 이유는 뭔가요? 무엇을 바꾸면 가장 크게 오를까요?",
- "periodWeek": "지난주 건강 지표들이 어땠는지 설명해 주세요 — 눈에 띄는 게 있나요?",
- "periodMonth": "지난달 건강 지표들이 어땠는지 설명해 주세요 — 눈에 띄는 게 있나요?",
+ "periodWeek": "지난주 건강 지표들이 어땠는지 설명해 주세요. 눈에 띄는 게 있나요?",
+ "periodMonth": "지난달 건강 지표들이 어땠는지 설명해 주세요. 눈에 띄는 게 있나요?",
"correlationBpCompliance": "제 복약 순응도가 혈압과 어떤 관계인가요? 이 상관관계가 저에게 어떤 의미인지 설명해 주세요.",
"correlationMoodPulse": "제 기분과 맥박 사이에 실제로 연관이 있나요? 이 상관관계를 읽는 법을 알려 주세요.",
"correlationWeightWeekday": "제 체중이 주간 패턴을 따르나요? 이 요일별 상관관계는 무엇을 말해 주나요?",
- "recommendation": "이 추천에 대해 더 알려 주세요: '{text}' — 왜 저에게 해당하고, 무엇을 하면 되나요?"
+ "recommendation": "이 추천에 대해 더 알려 주세요: '{text}'. 왜 저에게 해당하고, 무엇을 하면 되나요?"
},
- "assessmentPrompt": "제 {metric} 평가를 설명해 주세요 — 무슨 뜻이고 무엇을 하면 되나요?",
+ "assessmentPrompt": "제 {metric} 평가를 설명해 주세요. 무슨 뜻이고 무엇을 하면 되나요?",
"attach": {
"menuChooseFromDocuments": "문서에서 선택",
"menuUploadNew": "새 문서 업로드",
"trigger": "문서 첨부",
"pickerTitle": "문서 첨부",
- "pickerDescription": "코치는 첨부한 문서만 읽어요 — 건강 데이터는 읽지 않아요.",
+ "pickerDescription": "코치는 첨부한 문서만 읽어요. 건강 데이터는 읽지 않아요.",
"pickerSearchPlaceholder": "문서 검색…",
"pickerEmpty": "보관함에 문서가 아직 없어요.",
"pickerNotIndexed": "아직 색인되지 않음",
@@ -3092,13 +3154,13 @@
"pickerConfirm": "첨부 ({count})",
"pillRemove": "제거: {title}",
"indexing": "색인 중…",
- "indexingHint": "아직 색인 중인 문서가 있어요 — 준비되면 보낼 수 있어요.",
- "fencedHint": "이 대화는 건강 데이터에 접근하지 않아요 — 코치는 첨부한 문서만 읽어요.",
+ "indexingHint": "아직 색인 중인 문서가 있어요. 준비되면 보낼 수 있어요.",
+ "fencedHint": "이 대화는 건강 데이터에 접근하지 않아요. 코치는 첨부한 문서만 읽어요.",
"documentsAttachedOne": "문서 1개 첨부됨",
"documentsAttachedOther": "문서 {count}개 첨부됨",
"railBadge": "문서 전용 대화",
"uploadError": "업로드하지 못했어요. 다시 시도해 주세요.",
- "notFoundError": "그 문서 링크는 더 이상 유효하지 않아요 — 일반 대화로 시작할게요."
+ "notFoundError": "그 문서 링크는 더 이상 유효하지 않아요. 일반 대화로 시작할게요."
},
"interrupted": {
"note": "이 답변은 끝나기 전에 중단됐어요.",
@@ -3111,7 +3173,102 @@
"errorNotPermitted": "다른 사람이 공유한 기록 안에서는 코치를 사용할 수 없습니다.",
"errorUnavailable": "코치가 지금 설정을 확인하지 못했습니다. 잠시 후 다시 시도해 주세요.",
"errorOperatorOff": "이 서버에서 AI가 꺼져 있어 코치가 답변할 수 없습니다.",
- "errorCoachOff": "코치가 꺼져 있습니다. 설정의 모듈에서 다시 켤 수 있습니다."
+ "errorCoachOff": "코치가 꺼져 있습니다. 설정의 모듈에서 다시 켤 수 있습니다.",
+ "frame": {
+ "showPanel": "대화 목록 보기",
+ "hidePanel": "대화 목록 숨기기",
+ "panelDescription": "코치와 나눈 대화, 최신순이에요.",
+ "rowActions": "{title} 작업",
+ "rename": "이름 바꾸기",
+ "delete": "삭제",
+ "modelSection": "모델",
+ "provider": "응답 제공자",
+ "modelManaged": "이 제공자의 모델은 AI 설정에서 정해요.",
+ "noProvider": "사용할 수 있는 제공자가 없어요",
+ "saving": "저장 중…",
+ "saveFailed": "변경 사항을 저장하지 못했어요.",
+ "allSettings": "모든 AI 설정",
+ "providerNotActive": "{provider}(으)로 전환하지 못했습니다. 작동하는 로그인이나 키가 없습니다. AI 설정에서 설정하세요.",
+ "operatorCodex": "ChatGPT (운영자 공유)",
+ "operatorManaged": "이 연결은 서버 운영자가 관리합니다. 내 공급자를 하나라도 설정하면 그 공급자가 먼저 답합니다."
+ },
+ "answer": {
+ "scrollToBottom": "최신 메시지로 이동",
+ "rememberDone": "기억함",
+ "timeYesterday": "어제 {time}",
+ "timeEarlier": "{date} {time}",
+ "info": "답변 정보",
+ "infoModel": "모델: {model}"
+ },
+ "activity": {
+ "thinking": "생각하는 중…",
+ "fetching": "{domain} 가져오는 중, {window}…",
+ "digestOne": "값 {count}개 요약 중…",
+ "digestFew": "값 {count}개 요약 중…",
+ "digestOther": "값 {count}개 요약 중…",
+ "digestDoneOne": "{areas}에서 값 {count}개",
+ "digestDoneFew": "{areas}에서 값 {count}개",
+ "digestDoneOther": "{areas}에서 값 {count}개",
+ "areasOne": "{count}개 영역",
+ "areasFew": "{count}개 영역",
+ "areasOther": "{count}개 영역",
+ "memoryOne": "알려 준 내용 {count}개를 떠올리는 중",
+ "memoryFew": "알려 준 내용 {count}개를 떠올리는 중",
+ "memoryOther": "알려 준 내용 {count}개를 떠올리는 중",
+ "remember": "기억해 두는 중…",
+ "plan": "계획을 제안하는 중…",
+ "asking": "질문하는 중…",
+ "answer": "답변을 쓰는 중…",
+ "stop": {
+ "budget": "예산 도달, 지금까지의 내용으로 답변",
+ "time": "시간 제한 도달, 지금까지의 내용으로 답변",
+ "cap": "단계가 많아 이제 정리하는 중",
+ "noProgress": "새 데이터가 없어 정리하는 중"
+ },
+ "thoughtProcessOne": "사고 과정 ({count}단계)",
+ "thoughtProcessFew": "사고 과정 ({count}단계)",
+ "thoughtProcessOther": "사고 과정 ({count}단계)",
+ "awaitingAnswer": "답변이 필요해요"
+ },
+ "memory": {
+ "saved": "기억함: {fact}",
+ "undo": "실행 취소",
+ "accept": "네, 기억해 줘",
+ "decline": "아니요",
+ "confirmed": "알겠어요, 기억해 둘게요.",
+ "declined": "알겠어요, 저장하지 않을게요."
+ },
+ "plan": {
+ "accept": "이 계획 시작하기",
+ "decline": "나중에",
+ "confirmed": "기록했어요. {days}일 뒤에 함께 살펴볼게요.",
+ "declined": "알겠어요, 지금은 하지 않을게요."
+ },
+ "assumption": {
+ "line": "가정: {value}",
+ "change": "변경"
+ },
+ "suggestedReplies": {
+ "groupLabel": "추천 답변"
+ },
+ "chart": {
+ "compare": {
+ "periods": "{a} 대 {b}"
+ }
+ },
+ "reasoning": {
+ "label": "생각 깊이",
+ "hint": "더 깊이 생각할수록 답변이 정확해지고 조금 더 기다리게 돼요.",
+ "off": "끄기",
+ "minimal": "최소",
+ "low": "낮음",
+ "medium": "중간",
+ "high": "높음",
+ "capped": "(관리자가 제한함)",
+ "disabled": "관리자가 깊은 생각을 꺼 두었어요."
+ },
+ "memoryLink": "기억 ({count})",
+ "newConversation": "새 대화"
},
"relativeJustNow": "방금",
"relativeMinutesAgoOne": "{count}분 전",
@@ -3174,7 +3331,7 @@
},
"hrv": {
"title": "심박변이도",
- "description": "시간에 따른 심박변이도 — 신경계 회복 신호로, Apple Watch 등 호환 웨어러블이 수면 중에 측정해요.",
+ "description": "시간에 따른 심박변이도예요. 신경계 회복 신호로, Apple Watch 등 호환 웨어러블이 수면 중에 측정해요.",
"chartTitle": "심박변이도",
"emptyState": {
"title": "아직 심박변이도 기록이 없어요",
@@ -3183,17 +3340,17 @@
},
"restingHr": {
"title": "안정 시 심박수",
- "description": "심장이 하루 중 내려앉는 최저치예요 — 맥박 페이지의 단발 측정값과는 달라요. Apple Health는 하루에 한 값을 제공해요.",
+ "description": "심장이 하루 중 내려앉는 최저치예요. 맥박 페이지의 단발 측정값과는 달라요. Apple Health는 하루에 한 값을 제공해요.",
"chartTitle": "안정 시 심박수",
"emptyState": {
"title": "아직 안정 시 심박수가 없어요",
"description": "Apple Watch나 호환 Withings 기기를 조용한 하루 동안 착용하면 안정 시 심박수가 들어와요."
},
- "coachPrefill": "아직 안정 시 심박수를 기록한 적이 없어요 — 하루 안정 시 값이 단발 맥박과 왜 다른가요? 어떻게 개선하나요?"
+ "coachPrefill": "아직 안정 시 심박수를 기록한 적이 없어요. 하루 안정 시 값이 단발 맥박과 왜 다른가요? 어떻게 개선하나요?"
},
"oxygenSaturation": {
"title": "산소포화도",
- "description": "혈중 산소(SpO₂) — 임상 정상 범위는 95–100 %이고, 목표 구간을 차트에 음영으로 표시해요.",
+ "description": "혈중 산소(SpO₂)예요. 임상 정상 범위는 95–100 %이고, 목표 구간을 차트에 음영으로 표시해요.",
"chartTitle": "산소포화도",
"emptyState": {
"title": "아직 SpO₂ 기록이 없어요",
@@ -3202,7 +3359,7 @@
},
"bodyTemperature": {
"title": "체온",
- "description": "심부 체온 추이 — 섭씨. Withings의 피부 온도 측정값은 별도 지표로 따로 있어요.",
+ "description": "심부 체온 추이(섭씨)예요. Withings의 피부 온도 측정값은 별도 지표로 따로 있어요.",
"chartTitle": "체온",
"emptyState": {
"title": "아직 체온 기록이 없어요",
@@ -3212,7 +3369,7 @@
},
"steps": {
"title": "걸음 수",
- "description": "하루 걸음 수 — 하루에 한 줄씩, Apple Health나 연결된 활동 기기에서 가져와요.",
+ "description": "하루 걸음 수예요. 하루에 한 줄씩, Apple Health나 연결된 활동 기기에서 가져와요.",
"chartTitle": "하루 걸음 수",
"emptyState": {
"title": "아직 걸음 수 데이터가 없어요",
@@ -3221,7 +3378,7 @@
},
"activeEnergy": {
"title": "활동 에너지",
- "description": "하루 활동 에너지 합계(kcal) — 하루에 한 줄씩, Apple Health나 Withings 활동에서 가져와요.",
+ "description": "하루 활동 에너지 합계(kcal)예요. 하루에 한 줄씩, Apple Health나 Withings 활동에서 가져와요.",
"chartTitle": "활동 에너지",
"emptyState": {
"title": "아직 활동 에너지 데이터가 없어요",
@@ -3241,27 +3398,27 @@
},
"explainer": {
"bloodPressureBody": "혈압은 혈액이 동맥 벽에 가하는 힘이고, 수축기 분의 이완기로 적어요(심박 중 최고치와 박동 사이의 안정값). 높은 측정값이 계속되면 시간이 지나며 심장과 혈관에 부담이 쌓여요.",
- "pulseBody": "맥박은 심장이 1분에 몇 번 뛰는지예요. 한 번의 측정값은 그 순간 무엇을 하고 있었는지를 반영해요 — 안정 시에는 보통 분당 60~100회이고, 움직임·스트레스·카페인에 따라 자연스럽게 올라가요.",
+ "pulseBody": "맥박은 심장이 1분에 몇 번 뛰는지예요. 한 번의 측정값은 그 순간 무엇을 하고 있었는지를 반영해요. 안정 시에는 보통 분당 60~100회이고, 움직임·스트레스·카페인에 따라 자연스럽게 올라가요.",
"weightBody": "체중은 장기적인 에너지 균형과 전반적인 건강을 보여주는 가장 단순한 신호 중 하나예요. 수분·음식·시간대에 따라 흔들리는 한 번의 측정값보다 몇 주에 걸친 추이가 훨씬 중요해요.",
"bmiBody": "BMI는 체중을 키의 제곱으로 나눈 값으로, 넓은 체중 범주를 알려주는 하나의 숫자예요. 진단이 아니라 인구 집단 수준의 선별 지표이고, 근육과 지방을 구분하지 못해요.",
"sleepBody": "수면 시간은 밤새 실제로 잠든 시간이고, 얕은 수면·깊은 수면·REM 단계로 나뉘어요. 대부분의 성인은 대략 7~9시간이 필요하고, 꾸준한 양과 질 모두가 회복·기분·대사를 뒷받침해요.",
"moodBody": "기분 기록은 간단한 척도로 시간에 따른 기분을 남겨요. 꾸준히 기록하면 수면·활동·약이 기분과 어떻게 맞물리는지, 하루만 봐서는 절대 보이지 않는 패턴이 드러나요.",
"medicationsBody": "복약 순응도는 처방대로 약을 얼마나 꾸준히 복용하는지이고, 여기서는 예정된 복용 중 실제로 기록한 비율로 보여줘요. 치료가 의도대로 효과를 내려면 꾸준한 복용이 관건인 경우가 많아요.",
- "restingHrBody": "안정 시 심박수는 완전히 쉬고 있을 때의 심박수로, 보통 밤사이에 측정해요 — 활동 중 맥박과는 달라요. 안정 시 심박수가 낮을수록 대체로 심폐 체력이 좋다는 뜻이고, 올라가는 추이는 스트레스나 질병의 초기 신호일 수 있어요. 일반적인 기준으로 성인의 정상 안정 시 심박수는 분당 60~100회예요(AHA 2024). 단련된 심장은 40~60회에 머물러도 문제없어요.",
+ "restingHrBody": "안정 시 심박수는 완전히 쉬고 있을 때의 심박수로, 보통 밤사이에 측정해요. 활동 중 맥박과는 달라요. 안정 시 심박수가 낮을수록 대체로 심폐 체력이 좋다는 뜻이고, 올라가는 추이는 스트레스나 질병의 초기 신호일 수 있어요. 일반적인 기준으로 성인의 정상 안정 시 심박수는 분당 60~100회예요(AHA 2024). 단련된 심장은 40~60회에 머물러도 문제없어요.",
"hrvBody": "심박변이도는 연속된 심장 박동 사이 시간의 미세한 변동이에요. 변동이 클수록 대체로 잘 회복되고 적응력 있는 신경계를 뜻하고, 지속적으로 떨어지면 피로·스트레스·회복 부족의 신호일 수 있어요.",
"oxygenSaturationBody": "SpO₂는 산소를 실어 나르는 적혈구의 비율이에요. 안정 시 95~100%는 정상이고, 90% 아래에 계속 머무르면 살펴볼 필요가 있어요. 다만 밤중에 잠깐 떨어지는 건 흔해요.",
"bodyTemperatureBody": "체온은 몸의 심부 열 상태를 반영해요. 일반적인 기준으로 인구 평균은 37.0°C가 아니라 약 36.6°C이고, 구강 정상 범위는 대략 35.7~37.4°C, 발열은 38.0°C부터예요(J Gen Intern Med 2019). 하루 사이 약 0.5°C 오르내리고 측정 부위에 따라 최대 1°C가량 차이 나니 어디서 쟀는지 태그해 두세요. 밤사이 손목 측정값은 임상적 발열 수치가 아니라 상대적인 변화를 따라가요.",
"activeEnergyBody": "활동 에너지는 몸이 쉴 때 쓰는 기본량 위에, 움직임으로 태운 칼로리예요. 하루를 얼마나 몸을 움직였는지 직접 보여주는 지표예요.",
"workoutsBody": "운동은 기기가 기록한 구조화된 운동 세션으로, 종류·시간·에너지가 함께 남아요. 기록해 두면 꾸준한 훈련이 몇 주에 걸쳐 어떻게 쌓이는지, 다른 건강 신호와 어떻게 연결되는지 보여요.",
- "respiratoryRateBody": "호흡수는 1분에 숨을 몇 번 쉬는지예요. 일반적인 기준으로 안정 시 대부분의 성인은 분당 12~20회 호흡해요(American Lung Association). 정말 쉬고 있거나 잠든 상태에서만 의미가 있어요. 본인의 밤사이 기준값보다 +3 정도 지속해서 올라가면 질병의 초기 신호일 수 있어요 — 임상 경보 기준치는 병원용 도구이지 자가 경보용이 아니에요.",
+ "respiratoryRateBody": "호흡수는 1분에 숨을 몇 번 쉬는지예요. 일반적인 기준으로 안정 시 대부분의 성인은 분당 12~20회 호흡해요(American Lung Association). 정말 쉬고 있거나 잠든 상태에서만 의미가 있어요. 본인의 밤사이 기준값보다 +3 정도 지속해서 올라가면 질병의 초기 신호일 수 있어요. 임상 경보 기준치는 병원용 도구이지 자가 경보용이 아니에요.",
"bodyWaterBody": "체수분 총량은 세포 안팎의 수분이 체중에서 차지하는 비율이에요. 수분 섭취·음식·활동에 따라 움직이니 한 번의 측정값보다 몇 주 추이가 더 많은 걸 알려줘요.",
"boneMassBody": "골량은 뼈에 들어 있는 무기질의 추정 무게예요. 체중계 기반 추정은 대략적이지만, 유지되거나 올라가는 추이는 장기적인 뼈 건강에 도움이 돼요.",
- "fatFreeMassBody": "제지방량은 몸에서 지방이 아닌 전부 — 근육·뼈·장기·수분이에요. 체중을 줄이면서도 이걸 유지하거나 늘리면 대체로 더 건강한 체성분을 뜻해요.",
+ "fatFreeMassBody": "제지방량은 몸에서 지방이 아닌 전부(근육·뼈·장기·수분)예요. 체중을 줄이면서도 이걸 유지하거나 늘리면 대체로 더 건강한 체성분을 뜻해요.",
"fatMassBody": "체지방량은 몸속 지방의 총 무게예요. 지방은 호르몬과 에너지 저장에 어느 정도 꼭 필요하지만, 특히 복부에 너무 많으면 장기적인 건강 위험이 올라가요(WHO 2024).",
"bodyFatBody": "체지방률은 체중 중 지방이 차지하는 비율이에요. 건강한 범위는 성별과 나이에 따라 다르고, 체중계는 생체 전기 임피던스로 추정하므로 한 번의 측정값보다 몇 주간의 추이가 더 많은 걸 알려줘요.",
"muscleMassBody": "근육량은 몸속 근육의 추정 무게예요. 근육이 많을수록 근력·대사·활동성에 도움이 되고, 나이가 들수록 유지하려면 적극적인 노력이 필요해요.",
"visceralFatBody": "내장지방은 복부 깊숙이 장기 주변에 쌓이는 지방이에요. 피부 밑 지방과 달리 수치가 높으면 심장병과 2형 당뇨병에 밀접하게 연결되니 대체로 낮을수록 좋아요.",
- "leanBodyMassBody": "제지방 체중은 전체 체중에서 체지방을 뺀 값 — 근육·뼈·장기·수분을 합친 거예요. 체중과 함께 보면 변화가 근육에서 왔는지 지방에서 왔는지 알 수 있어요.",
+ "leanBodyMassBody": "제지방 체중은 전체 체중에서 체지방을 뺀 값으로, 근육·뼈·장기·수분을 합친 거예요. 체중과 함께 보면 변화가 근육에서 왔는지 지방에서 왔는지 알 수 있어요.",
"flightsClimbedBody": "오른 층수는 하루에 올라간 층을 세고, 한 층은 대략 3m의 고도 상승이에요. 일상 속 활동량과 하체 사용을 보여주는 단순한 지표예요.",
"walkingDistanceBody": "걷기·달리기 거리는 하루에 두 발로 이동한 거리예요. 얼마나 자주 걸었는지뿐 아니라 얼마나 멀리 움직였는지를 보여줘서 걸음 수를 보완해요.",
"walkingSteadinessBody": "보행 안정성은 걸음걸이가 얼마나 안정적이고 균형 잡혔는지를 양호부터 낮음까지로 추정해요. 등급이 낮을수록 낙상 가능성이 높다는 뜻이니 임상의와 상의해 볼 만해요.",
@@ -3276,30 +3433,30 @@
"headphoneAudioBody": "헤드폰 음량은 이어폰이나 헤드폰으로 얼마나 크게 듣는지를 dB로 기록해요. 볼륨을 낮추고 중간중간 쉬어 주면 오래도록 청력을 지킬 수 있어요.",
"audioEventsBody": "큰 소리 이벤트는 주변 소음이 충분히 노출될 경우 청력을 해칠 수 있는 수준에 도달한 순간을 표시해요. 자주 발생한다면 더 조용한 환경을 찾거나 청력 보호구를 쓰라는 신호예요.",
"daylightBody": "햇빛 노출 시간은 하루에 야외 빛 속에서 보낸 시간이에요. 규칙적인 햇빛은 건강한 수면-각성 리듬과 기분에 도움이 되고(CDC 2024), 야외에서 보내는 시간은 눈 건강과도 연결돼요.",
- "bloodGlucoseBody": "혈당은 몸의 주된 에너지원인 혈액 속 당의 양이에요. 식후에 올라가고 식사 사이에 가라앉아요. 당뇨가 없는 성인의 일반적인 기준으로 공복 혈당 {prediabetesFrom} {unit} 미만이 정상, {prediabetesFrom}~{prediabetesTo}는 당뇨 전 단계, {diabetesFrom} 이상은 당뇨 범위예요(ADA 2024). 한 번 높게 나왔다고 진단되지는 않아요 — 임상의가 재검으로 확인하고, 당뇨를 관리하는 사람은 임상의가 정한 다른 목표를 써요.",
+ "bloodGlucoseBody": "혈당은 몸의 주된 에너지원인 혈액 속 당의 양이에요. 식후에 올라가고 식사 사이에 가라앉아요. 당뇨가 없는 성인의 일반적인 기준으로 공복 혈당 {prediabetesFrom} {unit} 미만이 정상, {prediabetesFrom}~{prediabetesTo}는 당뇨 전 단계, {diabetesFrom} 이상은 당뇨 범위예요(ADA 2024). 한 번 높게 나왔다고 진단되지는 않아요. 임상의가 재검으로 확인하고, 당뇨를 관리하는 사람은 임상의가 정한 다른 목표를 써요.",
"skinTemperatureBody": "여기서 피부 온도는 절대적인 발열 수치가 아니라 본인 기준값 대비 밤사이 손목 온도의 작은 변화예요. 이 변화는 수면·환경·질병·생리 주기 단계를 반영할 수 있어요.",
"cardioFitnessBody": "VO₂ max는 최대 노력에서 몸이 쓸 수 있는 산소량으로, 유산소 체력의 가장 좋은 실험실 지표이고 웨어러블은 이를 간접 추정해요. 값이 높거나 올라가는 추이는 심혈관 건강의 강력한 신호예요.",
"cardioRecoveryBody": "심박 회복은 격한 운동을 멈춘 뒤 1분 동안 심박수가 분당 몇 회 떨어지는지예요. 많이 떨어질수록 심장이 더 빨리 안정된다는 뜻이고, 체력이 좋다는 신호예요.",
"wristTemperatureBody": "손목 온도는 자는 동안 측정하고 절대값이 아니라 본인 기준값과의 차이로 보여줘요. 값이 일정하면 정상이고, 지속적으로 올라가면 질병·고강도 훈련·생리 주기 변화를 암시할 수 있어요.",
"fallsBody": "웨어러블이 감지한 심한 낙상 횟수를 세요. 0이 목표이고, 횟수가 늘어나면 활동성 위험 신호이니 특히 고령자라면 임상의와 이야기해 볼 만해요(CDC STEADI 2024).",
- "sixMinuteWalkBody": "6분 동안 걸을 수 있는 거리의 추정치예요 — 활동성과 심폐 지구력을 보는 오래된 임상 검사죠. 웨어러블 수치는 감독하에 진행하는 임상 검사가 아니라 대리 지표이니, 유지되거나 올라가는 추이를 보는 게 쓸모 있어요.",
+ "sixMinuteWalkBody": "6분 동안 걸을 수 있는 거리의 추정치예요. 활동성과 심폐 지구력을 보는 오래된 임상 검사죠. 웨어러블 수치는 감독하에 진행하는 임상 검사가 아니라 대리 지표이니, 유지되거나 올라가는 추이를 보는 게 쓸모 있어요.",
"stairAscentSpeedBody": "계단 한 층을 얼마나 빨리 올라가는지예요. 다리 근력과 활동성을 민감하게 보여주는 지표라, 속도가 유지되거나 빨라지면 좋은 신호이고 느려지면 지켜볼 만해요.",
- "stairDescentSpeedBody": "계단 한 층을 얼마나 빨리 내려가는지예요. 오르는 속도와 함께 보면 다리 조절력과 균형을 알 수 있어요 — 속도가 일정하면 좋은 신호이고 느려지면 지켜볼 만해요.",
+ "stairDescentSpeedBody": "계단 한 층을 얼마나 빨리 내려가는지예요. 오르는 속도와 함께 보면 다리 조절력과 균형을 알 수 있어요. 속도가 일정하면 좋은 신호이고 느려지면 지켜볼 만해요.",
"breathingDisturbancesBody": "웨어러블은 자는 동안 호흡이 얼마나 자주 끊겼는지 추정할 수 있어요. 지수가 낮을수록 좋고, 여러 밤에 걸쳐 높게 표시되면 임상의와 이야기해 볼 만해요. 수면무호흡 진단이 아니라 선별용 대리 지표예요.",
"stepsBody": "걸음 수는 하루에 걷는 총 걸음으로, 일상 활동량을 보는 가장 단순한 지표예요. 하루 크게 걷는 것보다 꾸준한 습관이 더 중요하고, 딱 떨어지는 1만 걸음 목표는 연구가 아니라 마케팅 문구예요.",
- "readinessBody": "준비도는 안정 시 심박수·심박변이도·수면을 묶어 하루를 감당할 몸 상태가 얼마나 준비됐는지 하나로 보여줘요. 임상 지표가 아니라 본인 기준값에 맞춘 기기 대리 지표예요 — 점수가 높으면 회복됐다는 뜻이고, 낮으면 조금 쉬어 가라는 부드러운 신호예요.",
- "sleepScoreBody": "수면 점수는 하룻밤 전체를 하나의 숫자로 압축해요 — 시간·효율·규칙성, 그리고 얕은 수면·깊은 수면·REM의 균형이에요. 하룻밤의 완벽한 점수를 쫓기보다 몇 주에 걸친 패턴을 보세요.",
+ "readinessBody": "준비도는 안정 시 심박수·심박변이도·수면을 묶어 하루를 감당할 몸 상태가 얼마나 준비됐는지 하나로 보여줘요. 임상 지표가 아니라 본인 기준값에 맞춘 기기 대리 지표예요. 점수가 높으면 회복됐다는 뜻이고, 낮으면 조금 쉬어 가라는 부드러운 신호예요.",
+ "sleepScoreBody": "수면 점수는 하룻밤 전체를 하나의 숫자로 압축해요: 시간·효율·규칙성, 그리고 얕은 수면·깊은 수면·REM의 균형이에요. 하룻밤의 완벽한 점수를 쫓기보다 몇 주에 걸친 패턴을 보세요.",
"recoveryBody": "회복도는 심박변이도와 안정 시 심박수 같은 밤사이 신호를 바탕으로 몸이 얼마나 잘 회복했는지 보여줘요. 임상 지표가 아니라 본인 기준값에 견줘 읽는 기기 유래 대리 지표이고, 기기끼리 비교할 수 없어요. 점수가 높으면 밀어붙여도 좋다는 신호이고, 낮으면 몸이 아직 회복 중이라는 뜻이에요.",
"strainBody": "부하는 운동부터 종일 서서 활동한 하루까지, 몸에 실린 심혈관·신체 부담이 얼마나 되는지 담아요. 임상 지표가 아니라 본인의 평소 노력에 견줘 판단하는 기기 유래 지수이고, 대표 숫자는 기기끼리 비교할 수 없어요.",
"stressBody": "여기서 스트레스는 코르티솔 측정이나 스스로 느끼는 감정이 아니라 심박변이도 패턴에서 끌어낸 기기 추정값이고, 임상 스트레스 점수도 아니에요. 본인 기준값에 견줘 읽으세요. 기기끼리 비교할 수 없어요. 점수가 낮으면 차분하고 잘 조절된 상태이고, 높으면 회복 시간을 넣으라는 신호예요.",
"recoveryPageBody": "회복도는 웨어러블이 기록한 밤사이 신호(재충전, 부하, 누적 부담, 그날의 심박·에너지 합계)를 모아 몸이 얼마나 잘 회복했는지 보여줘요. 하나의 숫자보다 며칠에 걸친 패턴을 보세요. 낮은 값은 경보가 아니라 조금 쉬어 가라는 신호예요.",
"pulsePressureBody": "맥압은 수축기와 이완기 측정값의 차이예요(수축기 빼기 이완기). 일반적인 기준으로 안정 시 40mmHg 정도가 보통이고, 중년에서 약 60mmHg가 넘게 벌어지면 동맥이 뻣뻣해지고 있음을 반영할 수 있어요. 반대로 약 25mmHg 아래로 좁으면 심박출량이 적은 상태와 함께 나타날 수 있어요(StatPearls/NIH 2023). 진단이 아니라 이야기를 시작해 볼 거리예요.",
"meanArterialPressureBody": "평균 동맥압은 심장 박동 한 주기 동안 장기가 받는 평균 압력으로, 대략 이완기에 수축기까지 차이의 3분의 1을 더한 값이에요. 일반적인 기준으로 안정 시 값은 70~100mmHg 정도예요(StatPearls/NIH 2023). 흔히 인용되는 60mmHg 하한선은 급성기 진료 개념이지 소비자용 자가 경보가 아니에요.",
- "cycleEducationBody": "생리 주기는 호르몬 변화가 이끄는 네 단계로 흘러가고, 주기 길이와 규칙성은 전반적인 건강을 들여다보는 손쉬운 창이에요. 일반적인 기준으로 성인의 주기는 보통 21~35일(임상 자료는 24~38일)이고 출혈은 5일 안팎이에요(ACOG 2015). 황체기가 일정한 기준점이라 배란은 다음 예정 생리에서 거꾸로 세어 추정하는 게 가장 좋아요 — 고정된 '14일째'는 절대 아니에요. 예측된 가임기는 여러 주기가 기록돼야 나오는 추정치이고 여전히 대략적이에요. 기초 체온은 배란을 예측하는 게 아니라 지나간 뒤에 확인해 줘요. 이건 피임 방법이 아니에요.",
+ "cycleEducationBody": "생리 주기는 호르몬 변화가 이끄는 네 단계로 흘러가고, 주기 길이와 규칙성은 전반적인 건강을 들여다보는 손쉬운 창이에요. 일반적인 기준으로 성인의 주기는 보통 21~35일(임상 자료는 24~38일)이고 출혈은 5일 안팎이에요(ACOG 2015). 황체기가 일정한 기준점이라 배란은 다음 예정 생리에서 거꾸로 세어 추정하는 게 가장 좋아요. 고정된 '14일째'는 절대 아니에요. 예측된 가임기는 여러 주기가 기록돼야 나오는 추정치이고 여전히 대략적이에요. 기초 체온은 배란을 예측하는 게 아니라 지나간 뒤에 확인해 줘요. 이건 피임 방법이 아니에요.",
"sleepHygieneBody": "수면에 도움이 되는 근거 기반 습관 몇 가지예요. 주말을 포함해 잠자리에 드는 시각과 일어나는 시각을 일정하게 유지하고, 아침에는 밝은 햇빛을 쬐고 저녁에는 조명을 낮추고, 방은 시원하고 어둡고 조용하게 하고, 오후 카페인·늦은 술·늦은 과식을 피하세요(CDC 2024). 규칙성은 수면 시간만큼이나 건강을 잘 예측하고, 웨어러블의 총 수면 시간은 깊은 수면·얕은 수면·REM 구분보다 훨씬 믿을 만해요."
},
"bpDerived": {
- "pulsePressure": "맥압(측정값 사이의 차이)은 약 {value}mmHg예요 — 안정 시에는 40 정도가 보통이에요.",
+ "pulsePressure": "맥압(측정값 사이의 차이)은 약 {value}mmHg예요. 안정 시에는 40 정도가 보통이에요.",
"meanArterialPressure": "평균 동맥압(장기가 받는 평균)은 약 {value}mmHg로, 보통 70~100 정도예요.",
"caveat": "이 값들은 가장 최근 측정값에서 계산한 일반적인 참고 정보이고, 진단이 아니에요."
},
@@ -3311,7 +3468,7 @@
"mean": "평균"
},
"showAllValues": "모든 측정값 보기",
- "valuesPageTitle": "{metric} — 전체 측정값",
+ "valuesPageTitle": "{metric}: 전체 측정값",
"valuesPageDescription": "이 지표를 이루는 모든 측정값이에요. 기록을 바로 편집하거나 삭제할 수 있어요.",
"diversity": {
"trigger": "측정 팁",
@@ -3351,7 +3508,7 @@
"inBed": "침대에 있음",
"nap": "낮잠",
"napCount": "낮잠 {count}회",
- "unavailable": "아직 단계 데이터가 없어요 — 연결된 소스가 보내주는 대로 수면 단계가 여기에 나타나요."
+ "unavailable": "아직 단계 데이터가 없어요. 연결된 소스가 보내주는 대로 수면 단계가 여기에 나타나요."
},
"empty": {
"title": "아직 수면 데이터가 없어요",
@@ -3364,21 +3521,21 @@
"caption": "최근 {nights}일 밤 동안 부족한 수면",
"clearValue": "모두 채웠어요",
"clearCaption": "최근 {nights}일 밤 동안 부족한 수면이 없어요",
- "learning": "아직 수면을 익히는 중이에요 — 지금까지 {counted}일 밤, 믿을 만한 부족분을 읽으려면 {remaining}일 밤이 더 필요해요.",
+ "learning": "아직 수면을 익히는 중이에요. 지금까지 {counted}일 밤, 믿을 만한 부족분을 읽으려면 {remaining}일 밤이 더 필요해요.",
"computedInfo": "이건 HealthLog가 자체적으로 낸 추정치예요. 최근 밤들의 필요 수면에서 실제 수면을 뺀 누적 잔액이고, 보충 수면이 이를 갚아 나가요. 자체 필요 수면 모델을 쓰는 웨어러블의 수면 부채 수치와는 다를 수 있어요."
},
"average": {
"title": "밤 평균",
"value": "{hours}시간 {minutes}분",
"caption": "최근 {nights}일 밤의 평균 수면",
- "learning": "아직 수면을 익히는 중이에요 — 지금까지 {counted}일 밤, 믿을 만한 평균을 내려면 {remaining}일 밤이 더 필요해요."
+ "learning": "아직 수면을 익히는 중이에요. 지금까지 {counted}일 밤, 믿을 만한 평균을 내려면 {remaining}일 밤이 더 필요해요."
},
"chronotype": {
"title": "크로노타입",
- "learning": "아직 리듬을 익히는 중이에요 — 자유로운 밤 {total}일 중 {counted}일이에요. 크로노타입은 알람 없이 맞은 아침이 몇 번은 쌓여야 의미가 생겨요.",
+ "learning": "아직 리듬을 익히는 중이에요. 자유로운 밤 {total}일 중 {counted}일이에요. 크로노타입은 알람 없이 맞은 아침이 몇 번은 쌓여야 의미가 생겨요.",
"advancedToggle": "고급",
"socialJetlagValue": "사회적 시차 {hours}시간 {minutes}분",
- "socialJetlagCaption": "일하는 날과 쉬는 날의 수면 시각 차이예요 — 매주 작은 시차를 겪는 셈이죠.",
+ "socialJetlagCaption": "일하는 날과 쉬는 날의 수면 시각 차이예요. 매주 작은 시차를 겪는 셈이죠.",
"msfScValue": "보정 수면 중간 시각 {clock}",
"msfScCaption": "주말에 보충한 수면을 반영해 보정한 값이에요(MSFsc).",
"dayTypeNote": "날짜 유형 기록이 추가되기 전까지는 쉬는 날을 주말로 가정해요.",
@@ -3567,7 +3724,10 @@
"endInFuture": "종료 시간이 미래가 됩니다. 시작 시간을 운동을 시작한 시각으로 설정하세요.",
"distanceInvalid": "거리를 현실적인 범위의 숫자로 입력하세요.",
"energyInvalid": "활동 에너지는 20,000kcal 이하의 숫자로 입력하세요."
- }
+ },
+ "editTitle": "운동 편집",
+ "editAction": "편집",
+ "updated": "운동을 업데이트했습니다"
}
},
"bloodGlucose": {
@@ -3582,10 +3742,10 @@
"clinical": {
"title": "혈당 개요",
"subtitle": "최근 {days}일, 측정값 {count}개를 바탕으로 읽은 결과예요.",
- "spotCaveat": "연속 혈당 측정 프로필이 아니라 개별 측정에서 낸 단발 추정치예요 — 임상적 판정이 아니라 흐름의 방향으로 읽어 주세요.",
+ "spotCaveat": "연속 혈당 측정 프로필이 아니라 개별 측정에서 낸 단발 추정치예요. 임상적 판정이 아니라 흐름의 방향으로 읽어 주세요.",
"labsLink": "검사 결과가 있나요? 채혈 HbA1c나 공복 혈당은 검사에 기록해 주세요.",
"learningTitle": "아직 혈당을 익히는 중이에요",
- "learningBody": "지금까지 {days} 동안 {count} — 기록이 조금만 더 쌓이면 더 온전한 그림이 잡혀요. 계속 기록하거나 연결된 기기가 채우게 두세요.",
+ "learningBody": "지금까지 {days} 동안 {count}. 기록이 조금만 더 쌓이면 더 온전한 그림이 잡혀요. 계속 기록하거나 연결된 기기가 채우게 두세요.",
"learningBodyEmpty": "직접 기록하든 기기에서 동기화되든 측정값이 몇 개 들어오면, 목표 범위 내 시간·평균·변동성이 여기에 자리를 잡아요.",
"readingsCount": "측정값 {count}개",
"readingsCountOne": "측정값 1개",
@@ -3624,7 +3784,7 @@
"intro": "더 깊이 들여다보는 연구 수준의 복합 지표예요. 각각 측정값을 위험 가중치를 매긴 하나의 숫자로 압축해요.",
"jIndex": {
"label": "J 지수",
- "help": "평균과 그 퍼짐 정도를 합친 하나의 점수예요 — 낮을수록 더 촘촘한 관리로 읽혀요."
+ "help": "평균과 그 퍼짐 정도를 합친 하나의 점수예요. 낮을수록 더 촘촘한 관리로 읽혀요."
},
"lbgi": {
"label": "저혈당 위험(LBGI)",
@@ -3741,7 +3901,7 @@
"navSkinTemperature": "피부 온도",
"pulseWaveVelocity": {
"title": "맥파 전파 속도",
- "description": "시간에 따른 동맥 맥파 전파 속도예요(m/s) — 값이 높을수록 동맥이 뻣뻣하다는 뜻이에요.",
+ "description": "시간에 따른 동맥 맥파 전파 속도예요(m/s), 값이 높을수록 동맥이 뻣뻣하다는 뜻이에요.",
"chartTitle": "맥파 전파 속도",
"emptyState": {
"title": "아직 맥파 전파 속도 데이터가 없어요",
@@ -3891,7 +4051,6 @@
"navWalkingSpeed": "보행 속도",
"mood": {
"logLink": "기분 기록하기",
- "mentalWellbeingLink": "정신 건강 체크인 하기",
"heatmapTitle": "기분 캘린더",
"heatmapNoEntry": "기록 없음",
"legendGreat": "아주 좋음",
@@ -3902,7 +4061,7 @@
"distributionDaysUnit": "일",
"weekdayTitle": "요일별 평균",
"weekdayNoData": "데이터 없음",
- "weekdayBestWorst": "가장 좋은 요일: {best} · 가장 힘든 요일: {worst}",
+ "weekdayBestWorst": "가장 좋은 요일: {best}, 가장 힘든 요일: {worst}",
"tagsTitle": "태그와 유발 요인",
"tagAvgMood": "태그가 있는 날의 평균 기분",
"correlationsTitle": "기분과 다른 지표",
@@ -3918,7 +4077,7 @@
"strengthModerate": "보통",
"strengthWeak": "약함",
"strengthNone": "없음",
- "source": "짝지은 {n}일 · r = {r}",
+ "source": "짝지은 {n}일, r = {r}",
"sourceLabel": "계산 방법",
"emptyTitle": "아직 짝지을 데이터가 부족해요",
"emptyDescription": "같은 날에 기분과 이 지표를 함께 기록하면 상관관계를 볼 수 있어요.",
@@ -3935,7 +4094,7 @@
"inTargetCaption": "최근 날들 중 기분 좋음 범위에 든 비율",
"narrative": {
"title": "눈에 띄는 점",
- "weekdayDip": "{weekday}에 기분이 가장 많이 떨어져요 — 한 주 평균보다 {delta} 낮아요.",
+ "weekdayDip": "{weekday}에 기분이 가장 많이 떨어져요. 한 주 평균보다 {delta} 낮아요.",
"weekdayPeak": "꾸준히 가장 좋은 요일은 {weekday}, 평균 {value}.",
"trendUp": "최근 {days}일 동안 기분이 올라가는 추세예요(이전 기간 대비 +{delta}).",
"trendDown": "최근 {days}일 동안 기분이 내려가는 추세예요(이전 기간 대비 -{delta}).",
@@ -3944,8 +4103,8 @@
"tagLift": "'{tag}' 태그가 붙은 날은 전체 평균보다 {delta} 높아요.",
"tagDrop": "'{tag}' 태그가 붙은 날은 평균보다 {delta} 낮아요.",
"inTarget": "최근 30일 중 {pct}%는 기분 좋음 범위에 있었어요.",
- "streak": "{days}일 연속으로 기분을 기록했어요 — 계속 이어가 보세요.",
- "timeOfDay": "기분이 가장 좋은 시간대는 {bucket} — 평균 {value}."
+ "streak": "{days}일 연속으로 기분을 기록했어요. 계속 이어가 보세요.",
+ "timeOfDay": "기분이 가장 좋은 시간대는 {bucket}, 평균 {value}."
},
"timeOfDay": {
"title": "시간대별 기분",
@@ -3977,7 +4136,7 @@
"crosstab": {
"title": "태그와 건강 지표",
"description": "각 활동을 태그한 날과 그렇지 않은 날에 건강 지표가 어떻게 다른지 보여줘요.",
- "pairLabel": "{tag} · {metric}",
+ "pairLabel": "{tag} 있는 날의 {metric}",
"metricActiveEnergy": "활동 에너지",
"metricSleepDuration": "수면",
"metricNextDayRecovery": "다음 날 회복도",
@@ -3994,7 +4153,7 @@
"factorCrosstab": {
"title": "직접 평가한 요인과 생체 지표",
"description": "각 요인을 낮게 평가한 날과 높게 평가한 날에 생체 지표가 어떻게 다른지 보여줘요.",
- "pairLabel": "{factor} · {metric}",
+ "pairLabel": "{factor} 있는 날의 {metric}",
"metricSleepDuration": "수면",
"metricSteps": "걸음 수",
"metricRestingHeartRate": "안정 시 심박수",
@@ -4015,12 +4174,12 @@
"title": "눈에 띄는 점",
"subheading": "발견된 관계",
"footer": "모든 지표에서 검정한 일자별 조합 {tested}개 중 허위 발견 보정을 거쳐 추려낸 결과예요.",
- "disclaimer": "통계로 걸러냈지만 여전히 관찰 결과예요 — 원인이 아니라 지켜볼 만한 패턴이에요.",
+ "disclaimer": "통계로 걸러냈지만 여전히 관찰 결과예요. 원인이 아니라 지켜볼 만한 패턴이에요.",
"moodUp": "{factor} 값이 높으면 다음 날 기분도 높은 편이에요.",
"moodDown": "{factor} 값이 높으면 다음 날 기분은 낮은 편이에요.",
"outcomeUp": "기분이 좋으면 다음 날 {factor} 값도 높은 편이에요.",
"outcomeDown": "기분이 좋으면 다음 날 {factor} 값은 낮은 편이에요.",
- "stat": "짝지은 {n}일 · r = {r} · q = {q}",
+ "stat": "짝지은 {n}일, r = {r}, q = {q}",
"statLabel": "계산 방법",
"explainerLabel": "계산 방법"
},
@@ -4054,8 +4213,8 @@
"deviationAbove": "내 평가는 그 범위보다 {amount}점 높아요.",
"deviationBelow": "내 평가는 그 범위보다 {amount}점 낮아요.",
"weightedTitle": "이날 비교에서 가장 크게 작용한 것",
- "weightedUp": "{label} — 예상값을 높이는 쪽으로 작용했어요",
- "weightedDown": "{label} — 예상값을 낮추는 쪽으로 작용했어요",
+ "weightedUp": "{label}: 예상값을 높이는 쪽으로 작용했어요",
+ "weightedDown": "{label}: 예상값을 낮추는 쪽으로 작용했어요",
"caveat": "이건 내 지난 날들과의 비교일 뿐, 그날이 왜 그렇게 흘러갔는지에 대한 설명은 아니에요.",
"learning": "학습 중이에요: {threshold}일 중 {entries}일 기록했어요. 충분히 모이면 비교가 나타나요.",
"noPattern": "지금까지 기록한 {entries}일 중에는 하루를 견줄 만큼 일정한 패턴이 없어요. 하루의 맥락까지 함께 남기면 비교할 수 있는 게 늘어나요.",
@@ -4064,15 +4223,15 @@
},
"derived": {
"coverage": {
- "summary": "입력 {required}개 중 {present}개 · {percent}%",
+ "summary": "입력 {required}개 중 {present}개 ({percent}%)",
"ratioLabel": "{present}/{required}",
"historyDays": "최근 기록 {count}일 기준",
"missing": "더 정확해지려면 기록해 보세요: {list}",
- "summaryLabelled": "{label} · {percent}%"
+ "summaryLabelled": "{label} ({percent}%)"
},
"scoreRing": {
"aria": "점수 100점 만점에 {score}점, {band}",
- "ariaProvisional": "아직 점수를 낼 수 없어요 — 데이터가 부족해요",
+ "ariaProvisional": "아직 점수를 낼 수 없어요. 데이터가 부족해요",
"provisionalCaption": "아직 데이터가 부족해요",
"band": {
"green": "좋음",
@@ -4086,7 +4245,7 @@
"inputsLabel": "입력값",
"noInputs": "아직 이 값을 뒷받침한 입력값이 없어요.",
"methodLabel": "방법",
- "window": "최근 {days}일 기준 · {source} 데이터",
+ "window": "최근 {days}일간의 {source} 데이터 기준",
"asOf": "{date} 기준",
"standardLink": "기준: {name}"
},
@@ -4096,7 +4255,7 @@
"loadingLabel": "생체 지표 불러오는 중",
"loadError": "지금은 생체 지표를 불러오지 못했어요.",
"infoLabel": "어떻게 계산하나요?",
- "building": "내 일반 범위를 만드는 중 — {target}일 중 {count}일",
+ "building": "내 일반 범위를 만드는 중: {target}일 중 {count}일",
"typicalRange": "내 일반 범위: {low}–{high}",
"fitnessYounger": "약 {years}세 젊은 사람 수준의 심폐 체력",
"fitnessOlder": "실제 나이보다 약 {years}세 높은 심폐 체력",
@@ -4128,9 +4287,8 @@
"anatomy": {
"outOf": "/100",
"contributorsLabel": "구성 요소",
- "contributorValue": "{value} · 가중치 {weight}%",
+ "contributorValue": "{value}, 가중치 {weight}%",
"contributorMissing": "데이터 없음",
- "disclaimer": "참고용이에요 — 임상 평가가 아니에요.",
"insufficient": "아직 점수를 낼 데이터가 부족해요. 계속 기록하면 여기에 표시돼요."
},
"composite": {
@@ -4156,11 +4314,10 @@
"sleep": "수면",
"respiratory": "호흡수",
"mood": "기분 안정성"
- },
- "caveat": "회복도나 임상 점수가 아니라 컨디션을 가늠하는 지표예요."
+ }
},
"VITALS_BASELINE": {
- "method": "개인 일반 범위는 해당 기간 일별 값의 중앙값에 견고한 산포(중앙절대편차를 보정한 값)를 더하고 뺀 구간이에요. 인구 평균이 아니라 내 측정값을 따라가요 — 그래서 이 범위 안이면 나에게는 평소 수준이에요."
+ "method": "개인 일반 범위는 해당 기간 일별 값의 중앙값에 견고한 산포(중앙절대편차를 보정한 값)를 더하고 뺀 구간이에요. 인구 평균이 아니라 내 측정값을 따라가요. 그래서 이 범위 안이면 나에게는 평소 수준이에요."
},
"HRV_BALANCE": {
"method": "최근 심박변이도(SDNN) 평균을 내 이동 기준값(중앙값 ± 견고한 산포)과 비교해요. 균형은 개인 범위 안에 있다는 뜻이고, 이 범위는 인구 기준선이 아니라 내 데이터로 만들어요."
@@ -4173,49 +4330,48 @@
},
"VASCULAR_AGE_DELTA": {
"method": "기기가 추정한 혈관 나이 / 맥파 속도를 실제 나이와 비교해 연 단위 차이로 보여줘요. 추정은 기기가 하고, HealthLog는 다시 보여줄 뿐이에요.",
- "caveat": "기기가 추정한 값을 다시 보여주는 것뿐이에요 — 설명용이고 임상 혈관 평가가 아니에요.",
+ "caveat": "기기가 추정한 값을 다시 보여주는 것뿐이에요. 설명용이고 임상 혈관 평가가 아니에요.",
"description": "기기가 추정한 혈관 나이가 실제 나이와 어떻게 비교되는지 보여줘요."
},
"COINCIDENT_DEVIATION": {
"method": "같은 날 생체 지표 두 개 이상이 개인 일반 범위를 벗어나면 표시해요. 각 범위는 내 기록의 중앙값 ± k·MAD로 만들어서 인구 기준선이 아니라 나에게 맞춰져요.",
- "caveat": "내 기준값에서 나온 알아차림 신호예요 — 설명용이고 진단이나 임상 경고가 아니에요."
+ "caveat": "내 기준값에서 나온 알아차림 신호예요. 설명용이고 진단이나 임상 경고가 아니에요."
},
"RECOVERY_SCORE": {
"method": "심박변이도, 안정 시 심박수, 수면을 내 기준값과 비교해 섞은 밤마다의 0–100 회복 지표예요. 높을수록 더 잘 회복된 거예요. 매일 밤 한 번 계산해 두고 여기서 그대로 읽어와요.",
- "caveat": "설명용 컨디션 지표예요. 훈련용이나 임상용 회복 측정값이 아니에요.",
"description": "심박변이도, 안정 시 심박수, 수면으로 매일 밤 계산하는 회복 지표예요."
},
"STRESS_SCORE": {
"method": "하루 중 심박변이도의 흐름을 기준값과 비교해 매일 밤 내는 0–100 스트레스 지표예요. 높을수록 생리적 부담이 큰 거예요.",
- "caveat": "심박변이도에서 뽑은 지표이고 EDA / 피부 전기 반응 측정이 아니에요 — 시계에 EDA 센서가 없어요. 설명용이고 임상용이 아니에요.",
"description": "하루 중 심박변이도의 흐름으로 매일 밤 계산하는 스트레스 지표예요."
},
"STRAIN_SCORE": {
"method": "심박수로 가중한 활동 부하(TRIMP 방식 훈련 자극 모델)로 매일 밤 내는 0–100 부하 지표예요. 높을수록 힘든 하루였다는 뜻이에요.",
- "anchorPersonal": "최근 운동한 날의 부하를 기준으로 삼아요 — 평소 강도와 비교해 점수를 매기니 나에게 정말 힘든 날이 높게 나와요.",
+ "anchorPersonal": "최근 운동한 날의 부하를 기준으로 삼아요. 평소 강도와 비교해 점수를 매기니 나에게 정말 힘든 날이 높게 나와요.",
"anchorPopulation": "운동 기록이 쌓이는 동안에는 일반 기준으로 점수를 매기고, 충분히 모이면 내 평소 강도 기준으로 바뀌어요.",
- "caveat": "설명용 활동 부하 지표예요. 임상용이나 훈련용 부하 측정값이 아니에요.",
- "description": "심박수로 가중한 운동 강도를 평소 강도와 비교해 매일 밤 계산하는 활동 부하 지표예요."
+ "description": "심박수로 가중한 운동 강도를 평소 강도와 비교해 매일 밤 계산하는 활동 부하 지표예요.",
+ "deviceCaption": "기기의 일일 부하: {max} 중 {value}",
+ "deviceSource": "HealthLog가 밤사이 자체 값을 계산하기 전까지는 기기의 일일 부하를 0–100으로 환산해 보여 줘요."
},
"WRIST_TEMPERATURE_BASELINE": {
"method": "밤사이 손목 온도의 일반 범위는 해당 기간 야간 측정값의 중앙값에 견고한 산포(중앙절대편차를 보정한 값)를 더하고 뺀 구간이에요. 내 측정값을 따라가기 때문에, 지난밤은 인구 평균이 아니라 내 기준값에서 얼마나 벗어났는지로 보여줘요.",
- "caveat": "개인 편차 범위일 뿐이에요 — 설명용이고 질병, 발열, 생리 주기 신호가 아니에요. 해석은 직접 해 주세요."
+ "caveat": "개인 편차 범위일 뿐이에요. 설명용이고 질병, 발열, 생리 주기 신호가 아니에요. 해석은 직접 해 주세요."
},
"STAIR_ASCENT_SPEED_BASELINE": {
"method": "계단 오르는 속도의 일반 범위는 해당 기간 일별 값의 중앙값에 견고한 산포(중앙절대편차를 보정한 값)를 더하고 뺀 구간이에요. 계단 모양과 다리 길이에 따라 속도가 달라져 인구 기준선이 없기 때문에, 범위는 내 데이터로만 만들어요.",
- "caveat": "개인 이동성 추이 범위예요 — 설명용이고 노쇠나 낙상 위험 평가가 아니에요."
+ "caveat": "개인 이동성 추이 범위예요. 설명용이고 노쇠나 낙상 위험 평가가 아니에요."
},
"STAIR_DESCENT_SPEED_BASELINE": {
"method": "계단 내려가는 속도의 일반 범위는 해당 기간 일별 값의 중앙값에 견고한 산포(중앙절대편차를 보정한 값)를 더하고 뺀 구간이에요. 계단 모양과 다리 길이에 따라 속도가 달라져 인구 기준선이 없기 때문에, 범위는 내 데이터로만 만들어요.",
- "caveat": "개인 이동성 추이 범위예요 — 설명용이고 노쇠나 낙상 위험 평가가 아니에요."
+ "caveat": "개인 이동성 추이 범위예요. 설명용이고 노쇠나 낙상 위험 평가가 아니에요."
},
"SIX_MINUTE_WALK_BAND": {
"method": "기기가 추정한 6분 보행 거리를 나이, 키, 체중, 성별에 맞춘 Enright & Sherrill 기준과 비교해 예측치 대비 백분율로 보여줘요. 거리는 기기의 추정값이고 여기서 다시 계산하지 않아요. 나이, 키, 체중, 성별이 없으면 백분율은 숨기고 거리와 추이만 보여줘요.",
- "caveat": "발표된 기준과 비교해 다시 보여준 추정값이에요 — 신체 기능을 알아차리기 위한 신호이고 임상 6분 보행 검사나 진단이 아니에요. 이 기준은 40–80세를 대상으로 만들어졌고, 그 범위를 벗어나면 예측치는 외삽한 값이에요."
+ "caveat": "발표된 기준과 비교해 다시 보여준 추정값이에요. 신체 기능을 알아차리기 위한 신호이고 임상 6분 보행 검사나 진단이 아니에요. 이 기준은 40–80세를 대상으로 만들어졌고, 그 범위를 벗어나면 예측치는 외삽한 값이에요."
},
"TRAJECTORY": {
"method": "최근 일별 값에 최소제곱 직선을 맞춰 짧은 기간 앞으로 이어 그리고, 멀어질수록 넓어지는 표준 예측 구간을 함께 보여줘요. 최근 추이가 충분히 뚜렷하고 기록이 길고 최신일 때만 계산하고, 그렇지 않으면 선을 그리지 않아요.",
- "caveat": "최근 추이를 그대로 이어 그린 것이에요 — 예측이나 목표, 임상 전망이 아니에요. 최근 패턴이 이어질 때만 맞고, 음영 구간은 그 확신이 얼마나 빨리 흐려지는지 보여줘요."
+ "caveat": "최근 추이를 그대로 이어 그린 것이에요. 예측이나 목표, 임상 전망이 아니에요. 최근 패턴이 이어질 때만 맞고, 음영 구간은 그 확신이 얼마나 빨리 흐려지는지 보여줘요."
},
"SAME_TIME_BASELINE": {
"method": "오늘 누적값을 최근 4주 동안 같은 시각의 중앙값과 비교하고, 그 주위에 중앙값 ± MAD 범위를 함께 보여줘요. 기록이 충분한 날만 계산에 넣고, 그런 날이 14일 이상 모이기 전에는 비교를 보여주지 않아요.",
@@ -4230,7 +4386,9 @@
"recovery": "회복 점수",
"recoveryCrossLink": "웨어러블의 회복·부하 데이터 보기",
"stress": "스트레스",
- "strain": "부하"
+ "strain": "부하",
+ "historyTitle": "추이",
+ "historyWindow": "최근 {days}일"
},
"coincident": {
"label": "여러 지표가 기준값 밖",
@@ -4242,29 +4400,29 @@
"watch": "오늘 지표 하나가 평소 범위를 벗어났어요.",
"watchVital": "범위 밖: {vital}. 대개는 한 번뿐이지만 며칠 지켜볼 만해요.",
"firedHeadline": "오늘 생체 지표 {count}개가 개인 범위를 벗어났어요.",
- "factors": "가능한 요인 — 원인은 아니에요: 힘든 운동, 부족한 수면, 음주, 고도, 스트레스, 아픈 날. 내 기준값에서 나온 알아차림 신호이고 진단이 아니에요.",
- "building": "개인 기준값을 만드는 중이에요 — 며칠 더 기록하면 여기에 표시돼요.",
+ "factors": "가능한 요인(원인은 아니에요): 힘든 운동, 부족한 수면, 음주, 고도, 스트레스, 아픈 날. 내 기준값에서 나온 알아차림 신호이고 진단이 아니에요.",
+ "building": "개인 기준값을 만드는 중이에요. 며칠 더 기록하면 여기에 표시돼요.",
"illnessExplained": "이 변화는 기록해 둔 상태 때문일 가능성이 커요."
},
"trajectory": {
"cardTitle": "단기 전망",
- "headline": "이 패턴이 이어진다면 — 앞으로 {days}일",
+ "headline": "이 패턴이 이어진다면: 앞으로 {days}일",
"range": "{days}일 뒤 예상 범위: 약 {low}–{high} {unit}.",
"caveat": "최근 추이를 그대로 이어 그린 것이고 예측이 아니에요. 음영 구간은 불확실성이 얼마나 빨리 커지는지 보여줘요.",
- "insufficient": "아직 전망을 그릴 만큼 추이가 뚜렷하지 않아요 — 계속 기록해서 최근 패턴이 뚜렷해지면 전망이 표시돼요.",
+ "insufficient": "아직 전망을 그릴 만큼 추이가 뚜렷하지 않아요. 계속 기록해서 최근 패턴이 뚜렷해지면 전망이 표시돼요.",
"daysNeeded": "전망이 나오려면 {count}일쯤 더 기록해야 해요."
},
"sameTime": {
"sectionTitle": "오늘 지금까지",
"sectionSubtitle": "이 시각의 평소 수준과 비교한 오늘 누적값",
- "learning": "평소 하루를 익히는 중 — {target}일 중 {count}일",
+ "learning": "평소 하루를 익히는 중: {target}일 중 {count}일",
"framing": "{time}까지 평소: {typical} {unit}",
"comparisonLabel": "평소 하루"
}
},
"cardioFitness": {
"title": "심폐 체력",
- "description": "시간에 따른 VO₂ max — 유산소 체력을 대표하는 지표예요.",
+ "description": "시간에 따른 VO₂ max로, 유산소 체력을 대표하는 지표예요.",
"chartTitle": "VO₂ max (mL/(kg·min))",
"emptyState": {
"title": "아직 심폐 체력 데이터가 없어요",
@@ -4389,13 +4547,13 @@
"detail": "숨긴 상세 페이지는 상단 탐색에서도 사라져요."
},
"customize": "인사이트 맞춤 설정",
- "regenerateRateLimited": "새로고침 한도에 도달했어요 — 잠시 후 다시 시도해 주세요.",
- "regenerateUnavailable": "새 분석이 도착하지 않았어요 — 잠시 후 다시 시도해 주세요.",
- "regenerateError": "아직 새로고침 중이에요 — 잠시 후 다시 시도해 주세요.",
+ "regenerateRateLimited": "새로고침 한도에 도달했어요. 잠시 후 다시 시도해 주세요.",
+ "regenerateUnavailable": "새 분석이 도착하지 않았어요. 잠시 후 다시 시도해 주세요.",
+ "regenerateError": "아직 새로고침 중이에요. 잠시 후 다시 시도해 주세요.",
"regenerateSettling": "평가를 아직 준비하고 있어요",
"regenerateSettleFailed": "새로고침을 마치지 못했어요. 잠시 후 다시 시도해 주세요.",
"deviceScore": {
- "learning": "아직 기준값을 익히는 중이에요 — 며칠 밤 더 지나면 추이가 채워져요.",
+ "learning": "아직 기준값을 익히는 중이에요. 며칠 밤 더 지나면 추이가 채워져요.",
"average": "30일 평균 {value}",
"unitScore": "점",
"unitMinutes": "min",
@@ -4408,14 +4566,14 @@
},
"recovery": {
"title": "회복",
- "description": "몸이 얼마나 회복했는지 — 웨어러블이 밤사이 기록한 회복·부하 신호예요.",
+ "description": "몸이 얼마나 회복했는지: 웨어러블이 밤사이 기록한 회복·부하 신호예요.",
"scoreCrossLink": "종합 회복 점수 보기",
"emptyTitle": "아직 회복 데이터가 없어요",
"empty": "아직 회복 데이터가 없어요. WHOOP, Polar, Oura 계정을 연결하면 기기가 동기화될 때 점수가 채워져요.",
"block": {
- "ansChargeExplainer": "오늘 하루를 맞을 자율신경계의 충전 상태예요 — 밤사이 심장·호흡 신호에서 읽어낸 재충전 지표예요.",
+ "ansChargeExplainer": "오늘 하루를 맞을 자율신경계의 충전 상태예요. 밤사이 심장·호흡 신호에서 읽어낸 재충전 지표예요.",
"dayStrainExplainer": "기기의 부하 척도로 본 하루 전체의 심혈관 부하예요. 그 자체를 목표로 삼지 말고 회복과 견줘 보세요.",
- "workoutStrainExplainer": "같은 부하 척도에서 운동 하나가 얼마나 힘들었는지 — 그 세션의 심혈관 비용이에요.",
+ "workoutStrainExplainer": "같은 부하 척도에서 운동 하나가 얼마나 힘들었는지: 그 세션의 심혈관 비용이에요.",
"cardioLoadExplainer": "쌓인 급성 심혈관 부하를 따라가는 훈련 부하 지표예요. 내 최근 며칠과 비교해 보세요.",
"averageHeartRateExplainer": "웨어러블이 보고한 하루 또는 주기 전체의 평균 심박수예요.",
"maxHeartRateExplainer": "웨어러블이 보고한 하루 또는 주기 전체의 최고 심박수예요.",
@@ -4428,7 +4586,7 @@
},
"resilience": {
"explainer": "최근 며칠간의 밤사이 신호로 읽은, 몸이 하루의 부하와 회복을 얼마나 잘 맞추고 있는지예요.",
- "learning": "아직 회복탄력성을 익히는 중이에요 — 며칠 더 지나면 범위가 자리 잡아요.",
+ "learning": "아직 회복탄력성을 익히는 중이에요. 며칠 더 지나면 범위가 자리 잡아요.",
"band": {
"limited": "제한적",
"adequate": "충분",
@@ -4454,11 +4612,12 @@
"healthStatus": {
"sectionTitle": "달라진 점",
"subtitle": "평소 범위에서 벗어나고 있는 신호예요.",
- "deviationAbove": "{vital} — 일반 범위보다 높아요.",
- "deviationBelow": "{vital} — 일반 범위보다 낮아요.",
- "shiftUp": "{vital} — 최근 평소 수준보다 올라갔어요.",
- "shiftDown": "{vital} — 최근 평소 수준보다 내려갔어요.",
+ "deviationAbove": "{vital}: 일반 범위보다 높아요.",
+ "deviationBelow": "{vital}: 일반 범위보다 낮아요.",
+ "shiftUp": "{vital}: 최근 평소 수준보다 올라갔어요.",
+ "shiftDown": "{vital}: 최근 평소 수준보다 내려갔어요.",
"rangeContext": "지금 {value}, 평소 {low}–{high}.",
+ "rangeContextTimeOfDay": "지금 {value}, 이 시간대 평소 {low}–{high}.",
"shiftContext": "이전엔 약 {before}, 이후엔 {after}."
},
"breathingScreening": {
@@ -4472,7 +4631,7 @@
"nights": "{count}일 밤 기록",
"indexLine": "최근 평균 지수 {value}",
"eventsFlagged": "기기가 표시한 이벤트 {count}건",
- "disclaimer": "진단이 아니라 선별 신호예요 — 임상의와 상의해 주세요."
+ "disclaimer": "진단이 아니라 선별 신호예요. 임상의와 상의해 주세요."
},
"labsChanges": {
"sectionTitle": "지난 검사 이후",
@@ -4488,14 +4647,14 @@
"navWaistToHeight": "허리-키 비율",
"gripStrength": {
"title": "악력",
- "description": "최대 악력 — 전신 근력을 잘 대변하는 지표예요. 성별 기준선(남성 27kg, 여성 16kg) 아래면 근감소증 가능성을 뜻해요. 해석은 내 추이가 우선이에요.",
+ "description": "최대 악력으로, 전신 근력을 잘 대변하는 지표예요. 성별 기준선(남성 27kg, 여성 16kg) 아래면 근감소증 가능성을 뜻해요. 해석은 내 추이가 우선이에요.",
"chartTitle": "악력",
"emptyState": {
"title": "아직 악력 기록이 없어요",
"description": "악력계로 잰 값을 기록해 보세요. 몇 번 쥐어 본 값 중 가장 좋은 값을 남기면 추이가 여기에 표시돼요.",
"cta": "악력 기록"
},
- "coachPrefill": "아직 악력을 기록한 적이 없어요 — 악력은 내 근육 건강에 대해 무엇을 알려주고, 어떻게 재고 어떻게 키우나요?"
+ "coachPrefill": "아직 악력을 기록한 적이 없어요. 악력은 내 근육 건강에 대해 무엇을 알려주고, 어떻게 재고 어떻게 키우나요?"
},
"pain": {
"title": "통증 (0–10)",
@@ -4506,36 +4665,36 @@
"description": "지금 느끼는 통증을 0–10 척도로 기록하면 추이가 여기에 표시돼요.",
"cta": "통증 점수 기록"
},
- "coachPrefill": "아직 통증 점수를 기록한 적이 없어요 — 0–10 척도로 통증을 어떻게 기록해야 하고, 어느 정도가 의미 있는 변화인가요?"
+ "coachPrefill": "아직 통증 점수를 기록한 적이 없어요. 0–10 척도로 통증을 어떻게 기록해야 하고, 어느 정도가 의미 있는 변화인가요?"
},
"waist": {
"title": "허리둘레",
- "description": "센티미터로 잰 허리둘레 — 복부 지방의 지표예요. WHO 위험 증가 기준선은 성별에 따라 달라요(남성 94cm, 여성 80cm). 해석은 내 추이가 우선이에요.",
+ "description": "센티미터로 잰 허리둘레로, 복부 지방의 지표예요. WHO 위험 증가 기준선은 성별에 따라 달라요(남성 94cm, 여성 80cm). 해석은 내 추이가 우선이에요.",
"chartTitle": "허리둘레",
"emptyState": {
"title": "아직 허리둘레 측정값이 없어요",
"description": "가장 아래 갈비뼈와 골반뼈의 중간 지점에서, 숨을 편히 내쉰 끝에 허리를 재서 기록해 주세요.",
"cta": "허리둘레 기록"
},
- "coachPrefill": "아직 허리둘레를 기록한 적이 없어요 — 허리둘레는 어떻게 정확히 재고, 그 숫자가 건강에 어떤 의미인가요?"
+ "coachPrefill": "아직 허리둘레를 기록한 적이 없어요. 허리둘레는 어떻게 정확히 재고, 그 숫자가 건강에 어떤 의미인가요?"
},
"waistToHeight": {
"title": "허리-키 비율",
- "description": "허리둘레를 키로 나눈 값 — 인종에 관계없이 통하는 단일 지표예요. 0.5 미만(허리둘레가 키의 절반 아래)으로 유지하는 게 간단한 목표예요.",
+ "description": "허리둘레를 키로 나눈 값으로, 인종에 관계없이 통하는 단일 지표예요. 0.5 미만(허리둘레가 키의 절반 아래)으로 유지하는 게 간단한 목표예요.",
"chartTitle": "허리-키 비율",
"emptyState": {
"title": "아직 허리-키 비율이 없어요",
"description": "허리-키 비율을 기록하면 추이가 여기에 표시돼요. 요령은 간단해요. 허리둘레를 키의 절반 아래로 유지하세요.",
"cta": "허리-키 비율 기록"
},
- "coachPrefill": "아직 허리-키 비율을 기록한 적이 없어요 — 이 숫자가 왜 유용하고, 어떻게 0.5 미만으로 유지하나요?"
+ "coachPrefill": "아직 허리-키 비율을 기록한 적이 없어요. 이 숫자가 왜 유용하고, 어떻게 0.5 미만으로 유지하나요?"
},
"ecg": {
"sectionTitle": "심전도 기록",
"loadError": "심전도 기록을 불러오지 못했어요.",
"backToList": "기록 목록으로",
"clinicianNote": "결과가 걱정된다면 이 기록을 임상의와 상의해 보세요.",
- "resultAttribution": "기록된 결과: {result} — 측정 기기가 보고한 값이에요.",
+ "resultAttribution": "기록된 결과: {result}. 측정 기기가 보고한 값이에요.",
"result": {
"irregular": "심방세동 감지",
"notDetected": "심방세동 징후 없음",
@@ -4569,9 +4728,9 @@
"emptyCta": "기기 연결"
},
"intradayPulse": {
- "title": "하루의 흐름",
- "caption": "10분 단위로 평균 낸 하루 동안의 심박수예요.",
- "hourlyNote": "이 날짜는 시간별 평균으로 보여줘요 — 최근 며칠은 10분 단위까지 자세히 볼 수 있어요.",
+ "title": "하루 동안의 맥박",
+ "caption": "10분 단위로 평균했어요.",
+ "hourlyNote": "이 날짜는 시간별 평균으로 보여줘요. 최근 며칠은 10분 단위까지 자세히 볼 수 있어요.",
"empty": "이 날짜는 심박수 측정값이 적어서 아직 흐름을 보여줄 수 없어요.",
"coverageOne": "{hours}h 동안 모은 측정값 1개를 기준으로 했어요.",
"coverageMany": "{hours}h 동안 모은 측정값 {count}개를 기준으로 했어요.",
@@ -4580,10 +4739,10 @@
"previousDay": "이전 날짜",
"nextDay": "다음 날짜",
"tension": {
- "morning": "오늘 아침에 안정 시 심박수가 높은 구간이 이어졌어요 — 긴장이 있었을 수 있어요.",
- "afternoon": "오늘 오후에 안정 시 심박수가 높은 구간이 이어졌어요 — 긴장이 있었을 수 있어요.",
- "evening": "오늘 저녁에 안정 시 심박수가 높은 구간이 이어졌어요 — 긴장이 있었을 수 있어요.",
- "night": "밤사이 안정 시 심박수가 높은 구간이 이어졌어요 — 긴장이 있었을 수 있어요."
+ "morning": "오늘 아침에 안정 시 심박수가 높은 구간이 이어졌어요. 긴장이 있었을 수 있어요.",
+ "afternoon": "오늘 오후에 안정 시 심박수가 높은 구간이 이어졌어요. 긴장이 있었을 수 있어요.",
+ "evening": "오늘 저녁에 안정 시 심박수가 높은 구간이 이어졌어요. 긴장이 있었을 수 있어요.",
+ "night": "밤사이 안정 시 심박수가 높은 구간이 이어졌어요. 긴장이 있었을 수 있어요."
}
},
"navEcg": "심전도",
@@ -4594,6 +4753,19 @@
"settingsBody": "HealthLog는 AI 없이도 모든 기능이 작동합니다. 일일 브리핑, 측정값에 대한 짧은 메모, 코치를 원하시면 아래에서 제공자를 연결하세요.",
"action": "AI 설정",
"dismiss": "나중에"
+ },
+ "bmiHeight": {
+ "coachPrefill": "아직 키를 입력하지 않았어요. BMI가 왜 중요하고, 입력하기 전에 무엇을 알아야 하나요?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "아직 약을 추가하지 않았어요. 여기서 복약 순응도를 기록하기 전에 무엇을 알아야 하나요?"
+ },
+ "scoreHistory": {
+ "healthTitle": "건강 점수 추이",
+ "healthDescription": "날마다 표시된 그대로의 건강 점수입니다.",
+ "emptyDescription": "이 기간에는 아직 기록된 점수가 없습니다.",
+ "usualRange": "음영: 평소 범위 {lo}에서 {hi}까지.",
+ "seam": "점수에 반영되는 항목이 바뀐 지점에서 선이 끊깁니다."
}
},
"thresholds": {
@@ -4623,14 +4795,15 @@
"metricBodyFat": "체지방",
"metricSleep": "수면 시간",
"metricSteps": "하루 걸음 수",
- "metricGlucoseFasting": "혈당 — 공복",
- "metricGlucosePostprandial": "혈당 — 식후",
- "metricGlucoseRandom": "혈당 — 임의",
- "metricGlucoseBedtime": "혈당 — 취침 전",
+ "metricGlucoseFasting": "혈당 (공복)",
+ "metricGlucosePostprandial": "혈당 (식후)",
+ "metricGlucoseRandom": "혈당 (임의)",
+ "metricGlucoseBedtime": "혈당 (취침 전)",
"metricBodyWater": "체수분",
"metricBoneMass": "골량",
"metricOxygenSaturation": "산소포화도(SpO₂)",
- "unsetExplanation": "사용자 설정 없음 — 계산된 기본값을 써요."
+ "unsetExplanation": "사용자 설정 없음: 계산된 기본값을 써요.",
+ "cardTitle": "지표별 범위"
},
"onboarding": {
"errorGeneric": "설정을 마치는 중 문제가 생겼어요. 다시 시도해 주세요.",
@@ -4643,7 +4816,7 @@
"steps": {
"dashboardOverview": {
"title": "내 대시보드",
- "body": "대시보드는 모든 모듈을 한눈에 모아 보여줘요. 타일을 길게 누르면 순서를 바꾸거나 숨길 수 있어요(설정 → 대시보드 레이아웃). 타일은 그 뒤에 쌓인 데이터만큼만 말해 줘요 — 앞으로 나올 모듈들이 그 데이터를 채워요."
+ "body": "대시보드는 모든 모듈을 한눈에 모아 보여줘요. 타일을 길게 누르면 순서를 바꾸거나 숨길 수 있어요(설정 → 대시보드 레이아웃). 타일은 그 뒤에 쌓인 데이터만큼만 말해 줘요. 앞으로 나올 모듈들이 그 데이터를 채워요."
},
"quickAdd": {
"title": "몇 초면 기록 끝",
@@ -4675,7 +4848,7 @@
},
"mood": {
"title": "기분",
- "body": "기분은 지금 느낌과 태그를 2초 만에 남겨요. 강점: 나만의 태그. 기분 추이는 수면, 약, 주기와 맞물리고 — 코치가 그 패턴을 찾아 줘요."
+ "body": "기분은 지금 느낌과 태그를 2초 만에 남겨요. 강점: 나만의 태그. 기분 추이는 수면, 약, 주기와 맞물리고, 코치가 그 패턴을 찾아 줘요."
},
"insights": {
"title": "인사이트",
@@ -4687,15 +4860,15 @@
},
"integrations": {
"title": "연동",
- "body": "연동하면 Withings, WHOOP, Polar, Oura, Apple Health가 자동으로 동기화돼요. 체중, 수면, 회복이 직접 입력 없이 채워지고 — 그게 다시 인사이트와 코치로 이어져요."
+ "body": "연동하면 Withings, WHOOP, Polar, Oura, Apple Health가 자동으로 동기화돼요. 체중, 수면, 회복이 직접 입력 없이 채워지고, 그게 다시 인사이트와 코치로 이어져요."
},
"export": {
"title": "진료용 리포트",
- "body": "진료용 리포트는 측정값, 약, 검사 수치, 건강검진을 PDF나 FHIR로 묶어 진료에 가져갈 수 있게 해 줘요. 모든 모듈이 여기서 만나요 — 잘 쌓인 데이터라야 온전한 리포트가 나와요."
+ "body": "진료용 리포트는 측정값, 약, 검사 수치, 건강검진을 PDF나 FHIR로 묶어 진료에 가져갈 수 있게 해 줘요. 모든 모듈이 여기서 만나요. 잘 쌓인 데이터라야 온전한 리포트가 나와요."
},
"achievements": {
"title": "업적",
- "body": "업적은 꾸준함에 보상을 줘요 — 연속 기록과 이정표. 계속하기 쉽게 만드는 작은 동기 장치예요."
+ "body": "업적은 꾸준함에 보상을 줘요: 연속 기록과 이정표. 계속하기 쉽게 만드는 작은 동기 장치예요."
},
"wrapUp": {
"title": "둘러보기 끝",
@@ -4747,13 +4920,13 @@
"anamnesis": {
"title": "건강에 대해 (선택)",
"subtitle": "HealthLog가 내 숫자를 더 공정하게 읽도록 약간의 맥락을 알려 주세요.",
- "intro": "원하는 만큼만 알려 주세요 — 모든 항목은 선택이고, 설정에서 언제든 바꿀 수 있어요.",
+ "intro": "원하는 만큼만 알려 주세요. 모든 항목은 선택이고, 설정에서 언제든 바꿀 수 있어요.",
"conditionsLabel": "앓고 있는 질환",
"conditionsPlaceholder": "예: 고혈압, 2형 당뇨",
"conditionsHint": "당연한 값을 경고로 띄우는 대신, 맥락 속에서 측정값을 보게 해 줘요.",
"allergiesLabel": "알레르기나 불내성",
"allergiesPlaceholder": "예: 페니실린, 유당",
- "privacyNote": "암호화해 저장하고, 더 나은 맥락을 주는 데만 써요. 여기서 약은 묻지 않아요 — 복약 페이지에서 언제든 추가하면 돼요."
+ "privacyNote": "암호화해 저장하고, 더 나은 맥락을 주는 데만 써요. 여기서 약은 묻지 않아요. 복약 페이지에서 언제든 추가하면 돼요."
},
"ai": {
"panelTitle": "인사이트가 어떤 모습인지 보기",
@@ -4762,18 +4935,18 @@
"sampleTitle": "데일리 브리핑",
"sampleTag": "예시",
"sampleAriaLabel": "데일리 브리핑 예시",
- "sampleBody": "2주 동안 체중 1.2kg 감소. 안정 시 맥박은 58bpm으로 꾸준해요. 혈압은 7일 중 6일 범위 안 — 높았던 하루는 늦은 시간의 짠 저녁 식사 다음이었어요.",
+ "sampleBody": "2주 동안 체중 1.2kg 감소. 안정 시 맥박은 58bpm으로 꾸준해요. 혈압은 7일 중 6일 범위 안, 높았던 하루는 늦은 시간의 짠 저녁 식사 다음이었어요.",
"sampleCaption": "예시일 뿐 내 데이터가 아니에요. 인사이트를 켜면 브리핑이 이런 식으로 읽혀요.",
"ladderTitle": "켜는 방법 세 가지",
- "ladderLocalTitle": "로컬 모델 — AI 요청이 내 네트워크 안에 머물러요",
- "ladderLocalBody": "내 컴퓨터의 Ollama나 LM Studio를 HealthLog에 연결해요. 무료이고 완전히 비공개 — 가장 마음 편한 기본값이에요.",
+ "ladderLocalTitle": "로컬 모델: AI 요청이 내 네트워크 안에 머물러요",
+ "ladderLocalBody": "내 컴퓨터의 Ollama나 LM Studio를 HealthLog에 연결해요. 무료이고 완전히 비공개라서 가장 마음 편한 기본값이에요.",
"ladderByokTitle": "내 API 키 사용",
"ladderByokBody": "본인의 OpenAI나 Anthropic 키를 쓰면 돼요. 내 계정과 결제로 돌아가고, API 등급은 기본적으로 데이터를 학습에 쓰지 않아요.",
"ladderOauthTitle": "구독 계정으로 로그인",
"ladderOauthBody": "ChatGPT 계정을 연결해요. 토큰당 비용은 없지만 소비자 등급 약관은 내용을 학습에 쓸 수 있어서, 건강 데이터에는 기본값이 아니라 신중히 고를 선택이에요.",
- "keylessLine": "AI 없이도 HealthLog는 충분히 쓸모 있어요. 타일, 차트, 추이, 내보내기 모두 그대로 작동하고 — AI는 그 위에 쉬운 말 해설을 더할 뿐이에요.",
+ "keylessLine": "AI 없이도 HealthLog는 충분히 쓸모 있어요. 타일, 차트, 추이, 내보내기 모두 그대로 작동하고, AI는 그 위에 쉬운 말 해설을 더할 뿐이에요.",
"setupCta": "설정하기 (1분 정도)",
- "sharedKeyNote": "이 인스턴스에는 이미 공용 AI 제공자가 있어서 지금 바로 인사이트를 쓸 수 있어요 — 따로 설정할 게 없어요.",
+ "sharedKeyNote": "이 인스턴스에는 이미 공용 AI 제공자가 있어서 지금 바로 인사이트를 쓸 수 있어요. 따로 설정할 게 없어요.",
"offer": {
"title": "이 인스턴스가 요약을 쉬운 말로 써 줄 수 있습니다.",
"body": "운영자가 제공하는 공급자가 지난 24시간 안에 마지막으로 정상 동작했습니다. 한 번만 누르면 쉬운 말 요약이 켜지며, 그러면 데이터는 그 하나의 공급자에게만 전달되고 다른 어느 곳에도 가지 않습니다.",
@@ -4999,7 +5172,7 @@
"dataSourceDescription": "Withings, WHOOP, Oura, Polar, Nightscout, Fitbit, Apple Health에서 체중·혈압·수면 등을 자동으로 동기화해요.",
"dataSourceCta": "연결",
"notificationsTitle": "알림 설정하기",
- "notificationsDescription": "HealthLog가 어떻게 알려줄지 고르세요 — Telegram, ntfy, 웹 푸시.",
+ "notificationsDescription": "HealthLog가 어떻게 알려줄지 고르세요: Telegram, ntfy, 웹 푸시.",
"notificationsCta": "알림 열기",
"insightsTitle": "인사이트 켜기(선택)",
"insightsDescription": "로컬 모델이나 내 AI 키를 넣으면 일일 브리핑과 코치가 열려요.",
@@ -5088,11 +5261,6 @@
"brief": "간략",
"default": "기본",
"detailed": "상세",
- "defaultWindow": "기본 기간",
- "last7days": "최근 7일",
- "last30days": "최근 30일",
- "last90days": "최근 90일",
- "allTime": "전체 기간",
"excludedMetrics": "제외할 지표",
"dataClusters": "데이터 그룹",
"customClusters": "그룹 직접 선택하기",
@@ -5152,12 +5320,10 @@
"sections": {
"account": {
"title": "계정",
- "description": "프로필, 비밀번호, 패스키, 시작 설정 둘러보기.",
"subtitle": "프로필과 신원, 로그인 정보예요."
},
"integrations": {
"title": "연동",
- "description": "연결된 서비스, 전송 채널, 소스 우선순위를 한곳에서.",
"subtitle": "데이터 소스와 기기를 연결하고 관리해요.",
"connectionsHeading": "연결",
"channelsHeading": "전송 채널",
@@ -5166,15 +5332,14 @@
},
"notifications": {
"title": "알림",
- "description": "모든 알림을 한곳에서 — 무엇을 언제 알릴지, 어디로 보낼지.",
"subtitle": "받을 알림과 전달될 곳을 골라요.",
"remindersHeading": "알림",
"matrixLink": "채널별 알림",
- "channelsLink": "전송 채널"
+ "channelsLink": "전송 채널",
+ "displayHeading": "휴대폰에서"
},
"layout": {
"title": "화면 구성",
- "description": "대시보드, 인사이트, 기록 모듈의 모습과 배치를 정해요.",
"backToHub": "화면 구성으로 돌아가기",
"dashboard": {
"title": "대시보드",
@@ -5184,7 +5349,7 @@
"title": "인사이트",
"description": "개요 섹션과 내비게이션 칩 순서."
},
- "subtitle": "대시보드, 인사이트, 기록 모듈의 모습을 원하는 대로 바꿔요.",
+ "subtitle": "대시보드, 인사이트, 기록 모듈에 무엇을 어떤 순서로 보여줄지 정해요.",
"medications": {
"title": "복약",
"description": "목록 보기와 수동 순서."
@@ -5204,6 +5369,10 @@
"mood": {
"title": "기분 태그",
"description": "기분 선택기의 그룹, 태그, 보관된 태그."
+ },
+ "documents": {
+ "title": "문서",
+ "description": "보기 방식과 월 배치."
}
},
"dashboard": {
@@ -5211,15 +5380,13 @@
},
"thresholds": {
"title": "목표",
- "description": "지표별로 건강한 값의 목표 범위를 정해요.",
"subtitle": "지표별 목표 범위를 직접 정해요."
},
"sources": {
"title": "소스 우선순위",
- "description": "같은 날 같은 지표를 여러 소스가 기록했을 때 어느 소스를 쓸지 골라요.",
"integrationsHint": "데이터 소스 연결과 해제는 연동 페이지에서 해요.",
"integrationsHintLink": "연동 열기",
- "cardTitle": "지표별 소스 우선순위",
+ "cardTitle": "지표별 순위",
"help": "같은 날 여러 소스가 같은 값을 보내면 맨 위 소스를 대표값으로 써요.",
"resetDefaults": "기본값으로 되돌리기",
"resetDefaultsTitle": "소스 순서를 되돌릴까요?",
@@ -5232,7 +5399,7 @@
"resetSuccess": "소스 우선순위를 기본값으로 되돌렸어요",
"perMetricToggle": "지표별 재정의 ({count})",
"perMetricHelp": "기본 순서에서 직접 바꾼 지표예요. 다른 지표는 그대로 두고 하나만 기본값으로 되돌릴 때 이 목록을 쓰세요.",
- "perMetricEmpty": "지표별 재정의가 없어요 — 모든 지표가 위의 전체 순서를 써요.",
+ "perMetricEmpty": "지표별 재정의가 없어요. 모든 지표가 위의 전체 순서를 써요.",
"resetMetric": "되돌리기",
"deviceTypeToggle": "기기 유형 우선순위 ({count})",
"deviceTypeHelp": "한 소스 안에서 어느 기기를 쓸지예요. Apple Watch와 iPhone이 모두 걸음 수를 기록하면 순위가 높은 기기 유형의 측정값을 대표값으로 써요.",
@@ -5257,7 +5424,8 @@
"respiratoryRate": "호흡수",
"recovery": "회복",
"stress": "스트레스",
- "sleepDebt": "수면 부채"
+ "sleepDebt": "수면 부채",
+ "hrvRmssd": "심박변이도 (RMSSD)"
},
"sourceLabels": {
"WITHINGS": "Withings",
@@ -5274,7 +5442,8 @@
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava",
- "EXTERNAL": "외부 기기"
+ "EXTERNAL": "외부 기기",
+ "HEALTH_CONNECT": "Health Connect"
},
"deviceLabels": {
"watch": "워치",
@@ -5290,32 +5459,29 @@
"title": "동기화된 영양 섭취",
"description": "최근 {days}일간 Apple Health에서 동기화한 하루 합계예요.",
"empty": "아직 동기화된 영양 데이터가 없어요.",
- "latest": "{date} · {amount} {unit}",
+ "latest": "{date}에 {amount} {unit}",
"daysWithData": "{days}일 중 {count}일",
"moduleHint": "동기화는 영양 섭취 모듈에서 조절해요.",
"moduleHintLink": "모듈 관리",
- "detail": "읽기 전용 — 숫자는 원본 앱이 관리해요."
+ "detail": "읽기 전용이에요. 숫자는 원본 앱이 관리해요."
},
- "helpDetail": "나머지는 감사 기록용으로 그대로 보관해요."
+ "helpDetail": "나머지는 감사 기록용으로 그대로 보관해요. 연결했거나 데이터가 있는 소스만 보여요.",
+ "noneInUse": "아직 순위를 정할 소스가 없어요. 연동에서 연결하거나 값을 기록해 보세요."
},
"ai": {
"title": "AI 제공자",
- "description": "인사이트에 쓸 제공자, 모델, API 키 — 그리고 '나에 대해' 컨텍스트.",
"subtitle": "AI 제공자를 고르고 직접 키를 등록해요."
},
"api": {
"title": "API와 토큰",
- "description": "내 스크립트와 앱에서 쓸 베어러 토큰 — 어디서든 측정값이나 약 복용을 기록해요.",
"subtitle": "API 엔드포인트와 액세스 토큰을 관리해요."
},
"advanced": {
"title": "고급",
- "description": "마지막 출구: 모든 건강 기록을 영구히 지우거나 계정을 완전히 닫아요.",
"subtitle": "데이터 초기화와 계정 삭제."
},
"export": {
"title": "내보내기와 불러오기",
- "description": "데이터를 CSV나 전체 JSON 백업으로 내려받고, Apple Health·JSON·CSV에서 불러와요. 진료용 리포트는 이제 건강 기록에 있어요.",
"otherOptionsHeading": "다른 내보내기 옵션",
"downloadFailed": "내보내기 파일을 내려받지 못했어요. 다시 시도해 주세요.",
"actions": {
@@ -5346,7 +5512,7 @@
"encryptToggle": "암호문구로 암호화",
"passphrasePlaceholder": "암호문구 (12자 이상)",
"passphraseHelp": "Argon2id로 보관 파일 키를 만들고 AES-256-GCM으로 봉인해요. 파일은 .hlx 확장자로 저장해 주세요.",
- "noRecoveryWarning": "복구 수단이 없어요 — 이 암호문구를 잃어버리면 보관 파일을 열 수 없어요. 안전한 곳에 보관해 주세요.",
+ "noRecoveryWarning": "복구 수단이 없어요. 이 암호문구를 잃어버리면 보관 파일을 열 수 없어요. 안전한 곳에 보관해 주세요.",
"detail": "주간 자동 백업과 같은 형식이에요. 아직 담기지 않는 기록 유형이 있고, 파일의 매니페스트에 무엇이 들었는지 적혀 있어요.",
"contents": "측정, 복약, 기분, 생리 주기, 검사, 아픈 날 에피소드, 알레르기, 가족력, 영양, 운동, 건강 프로필, 사용자 지표를 JSON 파일 하나에."
}
@@ -5362,7 +5528,7 @@
"dropHint": "또는 눌러서 파일 선택",
"fileInputLabel": "Apple Health export.zip 파일 선택",
"choose": "파일 선택",
- "idempotencyNote": "같은 보관 파일을 다시 올리면 병합돼요 — 중복이 생기지 않아요. 큰 파일은 백그라운드에서 처리해요.",
+ "idempotencyNote": "같은 보관 파일을 다시 올리면 병합돼요. 중복은 생기지 않아요. 큰 파일은 백그라운드에서 처리해요.",
"uploading": "올리는 중…",
"uploadFailed": "올리지 못했어요. 다시 시도해 주세요.",
"rateLimited": "업로드가 너무 잦아요. 잠시 뒤에 다시 시도해 주세요.",
@@ -5371,8 +5537,11 @@
"failureInterrupted": "불러오기가 끝나기 전에 중단됐어요. 보통 서버 재시작이나 메모리 부족 때문이에요. 데이터는 안전하니 내보내기 파일을 다시 올려 주세요.",
"failureInsufficientMemory": "이 크기의 보관 파일을 처리하기엔 서버 메모리 한도가 낮아요. 서버 운영자에게 Node.js 힙 한도(NODE_OPTIONS=--max-old-space-size)를 올려 달라고 한 뒤 내보내기 파일을 다시 올려 주세요.",
"rowsImported": "지금까지 {count}개 행을 불러왔어요",
- "doneSummary": "완료 — {read}개 중 {imported}개 기록을 불러왔고, 진료 기록 {skipped}개는 건너뛰었어요.",
+ "doneSummary": "완료: {read}개 중 {imported}개 기록을 불러왔고, 진료 기록 {skipped}개는 건너뛰었어요.",
"doneNothing": "그 보관 파일에서 불러온 게 없어요: {read}개 중 0개만 들어왔어요.",
+ "writtenByHealthLogOne": "HealthLog가 직접 건강 앱에 기록한 {count}개 항목은 이미 데이터에 있으므로 제외했습니다.",
+ "writtenByHealthLogOther": "HealthLog가 직접 건강 앱에 기록한 {count}개 항목은 이미 데이터에 있으므로 제외했습니다.",
+ "writtenByHealthLogFew": "HealthLog가 직접 건강 앱에 기록한 {count}개 항목은 이미 데이터에 있으므로 제외했습니다.",
"ecgResult": "심전도: 발견 {discovered}개, 불러옴 {imported}개, 갱신 {updated}개, 건너뜀 {skipped}개, 실패 {failed}개. 심전도가 실패해도 export.xml에서 불러온 지원 기록은 그대로 남아요.",
"estimateWarning": "{count}일치 누적 합계는 가장 큰 내보내기 소스에서 추정했어요. Apple Health 자체 통계가 여전히 기준이에요.",
"phase": {
@@ -5381,12 +5550,12 @@
"parsing": "내보내기 파일 읽는 중…",
"upserting": "기록 불러오는 중…"
},
- "skipUnsupported": "{count}개 샘플을 건너뛰었어요 — HealthLog가 불러오지 않는 데이터 유형이에요.",
+ "skipUnsupported": "{count}개 샘플을 건너뛰었어요. HealthLog가 불러오지 않는 데이터 유형이에요.",
"skipRefused": "{count}개 샘플을 쓰지 못했어요 (읽을 수 없거나, 범위를 벗어났거나, 저장에 실패했어요).",
"skipBreakdown": "자세히 보기 ({count}개 유형)",
"skipBreakdownMore": "+ {count}개 유형 더",
"cycleSummary": "생리 주기 샘플 {samples}개를 {days}일치 기록으로 합쳤어요.",
- "cycleModuleOff": "생리 주기 샘플 {count}개를 건너뛰었어요 — 이 계정은 주기 기록이 꺼져 있어요.",
+ "cycleModuleOff": "이 계정은 주기 기록이 꺼져 있어서 생리 주기 샘플 {count}개를 건너뛰었어요.",
"cycleDaysFailed": "생리 주기 {count}일치를 쓰지 못했어요: {reason}"
},
"json": {
@@ -5471,8 +5640,8 @@
"failed": "불러오지 못했어요. 아무것도 저장되지 않았어요.",
"columnsHeading": "각 열의 역할",
"jsonOnly": "JSON 전용",
- "previewLine": "읽은 행: {rows} · 불러올 행: {queued} · 거부된 행: {refused}",
- "submittedLine": "읽은 행: {rows} · 불러오기 대기: {queued}",
+ "previewLine": "읽은 행 {rows}개, 불러올 행 {queued}개, 거부된 행 {refused}개",
+ "submittedLine": "읽은 행 {rows}개, 불러오기 대기 {queued}개",
"unmatched": "기록에서 일치하는 약이 없어요: {names}",
"ambiguous": "약이 둘 이상 일치해요: {names}",
"mirrored": "다른 앱에서 미러링한 약이라 복용을 추가할 수 없어요: {names}",
@@ -5508,18 +5677,59 @@
"tooManyRows": "한 번에 불러올 수 있는 행 수를 넘었어요. 파일을 나눠서 불러와 주세요."
},
"detail": "파일 하나에 담긴 모든 약을 함께 처리해요."
+ },
+ "healthConnect": {
+ "title": "Health Connect 내보내기",
+ "description": "Android의 Health Connect 앱에서 데이터를 내보낼 때 만들어지는 ZIP 파일을 가져옵니다.",
+ "choose": "ZIP 파일 선택",
+ "uploading": "업로드 중…",
+ "uploadFailed": "업로드에 실패했습니다. 다시 시도해 주세요.",
+ "tooLarge": "파일이 이 서버에서 허용하는 크기보다 큽니다.",
+ "doneSummary": "값 {count}개를 가져왔습니다.",
+ "failed": "가져오기에 실패했습니다: {reason}",
+ "phase": {
+ "queued": "시작 대기 중",
+ "unpacking": "파일 압축 해제 중",
+ "parsing": "기록 읽는 중",
+ "upserting": "값 저장 중",
+ "done": "완료",
+ "failed": "실패"
+ },
+ "steps": {
+ "export": "Android 휴대폰에서 Health Connect를 열고 데이터 관리, 내보내기 및 가져오기로 이동해 내보내기를 시작하세요.",
+ "cloud": "Health Connect는 내보내기를 Google Drive, Dropbox, Nextcloud 같은 클라우드 저장소 앱에만 저장하고 휴대폰 자체에는 저장하지 않습니다. 그중 하나를 고르세요.",
+ "upload": "그 클라우드에서 ZIP 파일을 이 기기로 내려받아 여기에 업로드하세요."
+ },
+ "dropLabel": "Health Connect ZIP 파일을 여기에 놓으세요",
+ "dropHint": "또는 클릭해서 선택하세요. 최대 1GB.",
+ "fileInputLabel": "Health Connect 내보내기 ZIP 파일",
+ "privacyNote": "가져오기가 끝나면 파일은 서버에서 바로 삭제됩니다. Withings나 Fitbit처럼 이미 직접 연결한 앱의 기록은 중복되지 않도록 제외됩니다. 같은 파일을 다시 올려도 바뀌는 것은 없습니다.",
+ "rateLimited": "짧은 시간에 업로드가 너무 많습니다. 1분 후 다시 시도하세요.",
+ "busy": "계정에서 이미 가져오기가 진행 중입니다. 끝날 때까지 기다려 주세요.",
+ "rowsSoFar": "지금까지 {count}개 값 저장됨",
+ "doneNothing": "가져오기가 끝났지만 새로 추가할 내용이 없었습니다.",
+ "workouts": "운동",
+ "leftOutConnected": "이미 직접 연결한 앱의 기록 {count}개가 제외되었습니다.",
+ "nutrientsModuleOff": "영양소 모듈이 꺼져 있어 수분 및 영양소 합계는 가져오지 않았습니다.",
+ "cycleModuleOff": "생리 주기 추적이 꺼져 있어 생리 데이터는 가져오지 않았습니다.",
+ "failure": {
+ "unsupportedVersion": "이 Health Connect 내보내기는 너무 오래되어 읽을 수 없습니다. 휴대폰에서 Health Connect를 업데이트한 뒤 다시 내보내세요.",
+ "notHealthConnect": "이 파일은 Health Connect 내보내기가 아닙니다. Health Connect가 저장한 ZIP 파일을 선택하세요.",
+ "unsafeSchema": "이 파일은 Health Connect 내보내기 구조가 아니어서 읽지 않았습니다.",
+ "tooLarge": "내보내기 파일이 이 서버가 받을 수 있는 크기보다 큽니다.",
+ "stagingMissing": "업로드한 파일이 더 이상 서버에 없습니다. 다시 업로드하세요.",
+ "interrupted": "서버 재시작으로 가져오기가 중단되었습니다. 파일을 다시 업로드하세요."
+ }
}
},
"subtitle": "데이터를 내보내거나 백업에서 불러와요."
},
"about": {
"title": "앱 정보",
- "description": "버전, 라이선스, 링크.",
"subtitle": "버전, 라이선스, 프로젝트 링크."
},
"sharing": {
"title": "공유",
- "description": "임상의와 건강 기록을 나눌 읽기 전용 링크를 만들어요.",
"subtitle": "데이터를 읽기 전용으로 공유해요."
},
"insights": {
@@ -5546,7 +5756,7 @@
"error": "저장하지 못했어요. 다시 시도해 주세요.",
"toggleable": {
"title": "기록할 항목",
- "description": "끈 모듈은 내비게이션, 타일, 인사이트에서 빠져요 — 데이터는 그대로 남아요."
+ "description": "끈 모듈은 내비게이션, 타일, 인사이트에서 빠지지만 데이터는 그대로 남아요."
},
"manageIn": "{section}에서 관리",
"operatorDisabled": "서버 전체에서 꺼짐",
@@ -5554,12 +5764,10 @@
},
"gesundheitsakte": {
"title": "건강 기록",
- "description": "전체 건강 기록을 PDF, FHIR R4, 또는 하나로 묶은 ZIP으로 내보내요.",
"subtitle": "임상의에게 바로 건넬 수 있는 완전한 건강 기록을 내보내요."
},
"coach": {
"title": "코치",
- "description": "코치 켜기, 말투와 범위, 저장된 기억.",
"subtitle": "코치의 말투, 먼저 건네는 알림, 기억을 조정해요."
},
"security": {
@@ -5568,8 +5776,7 @@
},
"privacy": {
"title": "데이터와 개인정보",
- "subtitle": "데이터, 보관 기간, 암호화, 계정 보안을 한곳에서.",
- "description": "내보내기, 삭제, 보관 기간, 암호화 상태, 세션, 보안 활동."
+ "subtitle": "데이터, 보관 기간, 암호화, 계정 보안을 한곳에서."
},
"mcp": {
"title": "MCP 커넥터",
@@ -5577,8 +5784,7 @@
},
"anamnesis": {
"title": "병력",
- "subtitle": "알레르기와 가족력을 관리해요.",
- "description": "구조화된 알레르기·가족력 기록."
+ "subtitle": "병력, 생활 습관, 응급 정보와 AI가 읽을 수 있는 범위를 관리해요."
},
"environment": {
"title": "위치 및 날씨",
@@ -5600,19 +5806,20 @@
"storedDays": "날씨 {count}일치를 저장했어요.",
"home": {
"title": "기본 위치",
- "description": "도시 단위의 대략적인 위치예요 — 정밀한 이동 경로는 저장하지 않아요. 설정한 날짜부터, 위치 기간이 적용되지 않는 날에는 이곳의 날씨와 일조 시간을 가져와요.",
+ "description": "도시 단위의 대략적인 위치예요. 정밀한 이동 경로는 저장하지 않아요. 설정한 날짜부터, 위치 기간이 적용되지 않는 날에는 이곳의 날씨와 일조 시간을 가져와요.",
"current": "현재: {label}",
"none": "아직 설정한 기본 위치가 없어요.",
"since": "{date}부터 날씨를 기록해요. 그 이전 날짜는 위치 기간을 추가하기 전까지 비어 있어요."
},
"travel": {
"title": "위치 기간",
- "description": "장소와 기간을 추가하세요 — 여행일 수도, 지난 기록의 한 구간일 수도 있어요. 해당 기간에는 기본 위치 대신 이 위치가 쓰여서 실제로 있던 곳의 날씨가 반영돼요. 지난 기록을 바로잡는 방법이에요.",
+ "description": "장소와 기간을 추가하세요. 여행일 수도, 지난 기록의 한 구간일 수도 있어요. 해당 기간에는 기본 위치 대신 이 위치가 쓰여서 실제로 있던 곳의 날씨가 반영돼요. 지난 기록을 바로잡는 방법이에요.",
"none": "아직 위치 기간이 없어요.",
"add": "{label} 추가",
"remove": "위치 기간 삭제",
"removeConfirmTitle": "이 여행을 삭제할까요?",
- "removeConfirmBody": "그 기간에 측정한 값은 기록에 그대로 남고, 여행 항목만 사라져요."
+ "removeConfirmBody": "그 기간에 측정한 값은 기록에 그대로 남고, 여행 항목만 사라져요.",
+ "range": "{start}~{end}"
},
"backfill": {
"title": "지난 날짜 채우기",
@@ -5621,8 +5828,22 @@
},
"noResults": "일치하는 장소가 없어요.",
"fetchFailedOne": "마지막 백그라운드 날씨 업데이트가 실패했어요 ({when}).",
- "fetchFailedFew": "최근 백그라운드 날씨 업데이트 {count}번이 실패했어요 — 마지막은 {when}.",
- "fetchFailedOther": "최근 백그라운드 날씨 업데이트 {count}번이 실패했어요 — 마지막은 {when}."
+ "fetchFailedFew": "최근 백그라운드 날씨 업데이트 {count}번이 실패했어요. 마지막 실패: {when}.",
+ "fetchFailedOther": "최근 백그라운드 날씨 업데이트 {count}번이 실패했어요. 마지막 실패: {when}.",
+ "airQuality": {
+ "title": "대기질과 꽃가루",
+ "description": "Open-Meteo를 통해 CAMS 모델에서 가져온 내 위치의 일별 대기질, 꽃가루, 자외선 정보입니다.",
+ "toggle": "대기질과 꽃가루 포함",
+ "operatorDisabled": "이 서버에서는 대기질 기능이 꺼져 있습니다.",
+ "coverage": "대기질 데이터가 있는 날 {count}일",
+ "pollenEuropeOnly": "꽃가루 데이터는 유럽만 제공됩니다.",
+ "historyProgress": "대기질 과거 데이터 채우는 중: {total}일 중 {done}일",
+ "historyComplete": "출처가 제공하는 기록이 있는 이전 날짜 모두에 대기질이 채워졌습니다({total}일). 그보다 오래된 날짜는 비어 있습니다.",
+ "attribution": "데이터 출처와 라이선스",
+ "saveError": "설정을 저장하지 못했습니다.",
+ "latest": "가장 최근 날짜 ({date})",
+ "modelNote": "내 지역의 실외 모델값이며, 개인 노출량이 아닙니다."
+ }
},
"score": {
"title": "건강 점수",
@@ -5655,7 +5876,7 @@
"omitted": "{count}개 항목은 기록이 꺼져 있어서 목록에 없어요. 모듈을 다시 켜면 해당 항목도 돌아와요.",
"omittedLink": "모듈 열기",
"refusal": {
- "empty": "건강 점수에는 점수를 매길 건강 영역이 최소 하나 필요해요. 하나는 선택해 두세요 — 점수가 어떤 영역을 바탕으로 하는지 알려 줘요."
+ "empty": "건강 점수에는 점수를 매길 건강 영역이 최소 하나 필요해요. 하나는 선택해 두세요. 점수가 어떤 영역을 바탕으로 하는지 알려 줘요."
},
"notice": {
"upgrade": {
@@ -5749,7 +5970,6 @@
},
"changePassword": "비밀번호 변경",
"changePasswordDescription": "지금 쓰는 비밀번호를 새 비밀번호로 바꿔요.",
- "passwordReset": "비밀번호 재설정",
"currentPassword": "현재 비밀번호",
"newPassword": "새 비밀번호",
"confirmNewPassword": "새 비밀번호 확인",
@@ -5794,9 +6014,9 @@
"botToken": "봇 토큰",
"chatId": "채팅 ID",
"enableNotifications": "알림 켜기",
- "telegramStep1": "1. Telegram에서 @BotFather로 봇을 만들고 토큰을 복사하세요.",
- "telegramStep2": "2. 봇에게 /start를 보내 채팅을 활성화하세요.",
- "telegramStep3": "3. @userinfobot이나 Bot API로 채팅 ID를 확인하세요.",
+ "telegramStep1": "Telegram에서 @BotFather로 봇을 만들고 토큰을 복사하세요.",
+ "telegramStep2": "봇에게 /start를 보내 채팅을 활성화하세요.",
+ "telegramStep3": "@userinfobot이나 Bot API로 채팅 ID를 확인하세요.",
"ntfy": "ntfy",
"ntfyDescription": "ntfy(셀프 호스팅 또는 ntfy.sh)로 보내는 알림이에요.",
"ntfyEnable": "ntfy 켜기",
@@ -5805,7 +6025,7 @@
"ntfyAuthToken": "인증 토큰 (선택)",
"ntfyAuthTokenHint": "접근 제어가 걸린 비공개 토픽에만 필요해요.",
"webhook": "웹훅",
- "webhookDescription": "Gotify, Discord, Slack, Matrix, Home Assistant 등 어떤 URL로든 알림을 POST해요.",
+ "webhookDescription": "Discord, Slack, Gotify, Home Assistant, n8n 등 JSON을 받는 어떤 URL로든 알림을 POST해요.",
"webhookEnable": "웹훅 켜기",
"webhookUrl": "웹훅 URL",
"webhookUrlHint": "POST를 보낼 전체 URL이에요. 사설 네트워크 주소는 서버 운영자가 허용해야 해요.",
@@ -5813,6 +6033,7 @@
"webhookHeaderValue": "헤더 값 (선택)",
"webhookFormat": "메시지 형식",
"webhookFormatGeneric": "일반 JSON",
+ "webhookFormatGenericHint": "Discord나 Slack 웹후크 URL은 그대로 붙여 넣으면 돼요. Home Assistant와 n8n은 title, message, eventType, priority 필드를 읽어요.",
"webhookFormatGotify": "Gotify",
"webhookFormatGotifyHint": "/message로 끝나는 URL을 쓰고, 앱 토큰은 X-Gotify-Key 헤더에 넣어 주세요.",
"email": "이메일",
@@ -5829,7 +6050,6 @@
"webPushSubscribed": "푸시 알림을 켰어요!",
"webPushUnsubscribed": "푸시 알림을 껐어요.",
"webPushSubscribeFailed": "켜지 못했어요",
- "kiInsightsDescription": "선택 사항: OpenAI 키를 저장하면 인사이트에서 매일 자동으로 분석해요.",
"codexConnected": "ChatGPT를 연결했어요! 이제 인사이트를 쓸 수 있어요.",
"codexDisconnected": "ChatGPT 연결을 해제했어요.",
"codexConnectionFailed": "ChatGPT 연결에 실패했어요. 다시 시도해 주세요.",
@@ -5839,7 +6059,7 @@
"rawDataWarning": "자세한 기록이 켜져 있어요. 분석할 때마다 최근 90일의 일별 평균, 그 이전 1년의 주별 평균, 최대 5년 전까지의 월별 평균이 추가로 전송돼요.",
"regenerateInsights": "리포트 다시 생성",
"regenerateSuccess": "리포트를 다시 생성했어요",
- "regenerateRateLimit": "잠시만 기다려 주세요 — 시간당 분석 한도에 도달했어요.",
+ "regenerateRateLimit": "잠시만 기다려 주세요. 시간당 분석 한도에 도달했어요.",
"lastGeneratedAt": "마지막 생성",
"ai": {
"chatgptConnectedBadge": "ChatGPT 연결됨",
@@ -5849,11 +6069,11 @@
"deviceCodeHeading": "chatgpt.com에서 연결을 마무리하세요",
"deviceCodeStep1": "아무 기기에서나 이 링크를 여세요:",
"deviceCodeStep2": "이 일회용 코드를 입력하세요:",
- "deviceCodeStep3": "연결을 승인하세요 — 이 페이지는 자동으로 갱신돼요.",
+ "deviceCodeStep3": "연결을 승인하세요. 이 페이지는 자동으로 갱신돼요.",
"deviceCodeCopy": "복사",
"deviceCodeWaiting": "승인을 기다리는 중…",
"deviceCodeCancel": "취소",
- "oauthNotConfigured": "이 인스턴스에는 ChatGPT OAuth가 설정돼 있지 않아요 — 아래에서 직접 발급한 API 키를 쓰세요.",
+ "oauthNotConfigured": "이 인스턴스에는 ChatGPT OAuth가 설정돼 있지 않아요. 아래에서 직접 발급한 API 키를 쓰세요.",
"modelLabel": "모델",
"modelOptionDefault": "— 기본값 —",
"modelOptionCustom": "직접 입력…",
@@ -5861,7 +6081,7 @@
"anthropicKeyLabel": "Anthropic API 키",
"baseUrlLabel": "기본 URL",
"localKeyLabel": "API 키 (선택)",
- "localDescription": "OpenAI 호환 엔드포인트라면 무엇이든 쓸 수 있어요 — 로컬 모델(Ollama, LM Studio)은 물론 LiteLLM, OpenRouter, vLLM 같은 게이트웨이도요. 기본 URL은 /v1로 끝나고, API 키는 선택이에요.",
+ "localDescription": "OpenAI 호환 엔드포인트라면 무엇이든 쓸 수 있어요. 로컬 모델(Ollama, LM Studio)은 물론 LiteLLM, OpenRouter, vLLM 같은 게이트웨이도요. 기본 URL은 /v1로 끝나고, API 키는 선택이에요.",
"savedPreview": "(저장됨: {preview})",
"savedShort": "(저장됨)",
"saveCta": "저장",
@@ -5877,7 +6097,7 @@
"admin-openai": "관리자 OpenAI"
},
"title": "대체 순서",
- "description": "기본 제공자가 실패하면 HealthLog가 순서대로 다음 제공자를 시도해요. 행을 끌어 순서를 바꾸고, 스위치를 끄면 제공자를 지우지 않고 비활성화할 수 있어요.",
+ "description": "활성 제공자가 실패하면 HealthLog가 이 순서대로 다음 활성 제공자를 시도해요.",
"moveUp": "위로",
"moveDown": "아래로",
"removeFromChain": "순서에서 제거",
@@ -5890,11 +6110,8 @@
"resetConfirmTitle": "순서를 기본값으로 되돌릴까요?",
"resetConfirmBody": "순서가 Codex → OpenAI → Anthropic → 로컬 → 관리자 OpenAI로 돌아가요. 저장한 인증 정보는 그대로예요."
},
- "providerCardTitle": "AI 제공자",
"activeProviderHeading": "사용 중인 제공자",
- "activeProviderBody": "가장 먼저 쓸 제공자를 고르세요. 아래 양식은 선택한 제공자만 설정하고, 실패했을 때 어떻게 할지는 맨 아래 대체 순서가 정해요.",
"activeProviderLabel": "기본 제공자",
- "providerConfigTitle": "제공자 설정",
"providerSelect": {
"codex": "ChatGPT 계정 (Codex)",
"openai": "OpenAI (내 API 키)",
@@ -5904,7 +6121,7 @@
"admin-openai": "관리자가 제공하는 OpenAI"
},
"compat": {
- "description": "OpenAI /v1/chat/completions API를 쓰는 엔드포인트라면 무엇이든 정식 제공자로 쓸 수 있어요: LiteLLM, OpenRouter, vLLM 같은 게이트웨이요. 기본 URL은 /v1로 끝나고, API 키는 선택이며, 모델은 게이트웨이가 연결해 주는 것을 쓰면 돼요. OpenAI 키는 이 엔드포인트에 절대 전달되지 않아요 — 이 제공자는 자체 키를 써요.",
+ "description": "OpenAI /v1/chat/completions API를 쓰는 엔드포인트라면 무엇이든 정식 제공자로 쓸 수 있어요: LiteLLM, OpenRouter, vLLM 같은 게이트웨이요. 기본 URL은 /v1로 끝나고, API 키는 선택이며, 모델은 게이트웨이가 연결해 주는 것을 쓰면 돼요. OpenAI 키는 이 엔드포인트에 절대 전달되지 않아요. 이 제공자는 자체 키를 써요.",
"keyLabel": "API 키 (선택)",
"modelHint": "게이트웨이가 기대하는 모델 이름이에요. 비워 두면 다른 제공자에 저장한 모델을 쓰고, 둘 다 없으면 이 제공자는 건너뛰어요."
},
@@ -5940,13 +6157,13 @@
"notConfigured": "운영자가 이 인스턴스에 공용 OpenAI 키를 설정하지 않았어요."
},
"testProvider": "사용 중인 제공자 테스트",
- "testSuccess": "정상 — {provider} ({model})",
+ "testSuccess": "정상: {provider} ({model})",
"testFailedShort": "테스트 실패: {message}",
- "testUnexpectedResponse": "AI 제공자 연결에 실패했어요 — 서버에서 예상 밖의 응답이 왔어요.",
- "testReasonCredentials": "제공자가 인증 정보를 거부했어요 — AI 설정에서 다시 인증해 주세요.",
- "testReasonRateLimited": "제공자가 요청 빈도를 제한했어요 — 잠시 뒤 다시 시도해 주세요.",
+ "testUnexpectedResponse": "AI 제공자 연결에 실패했어요. 서버에서 예상 밖의 응답이 왔어요.",
+ "testReasonCredentials": "제공자가 인증 정보를 거부했어요. AI 설정에서 다시 인증해 주세요.",
+ "testReasonRateLimited": "제공자가 요청 빈도를 제한했어요. 잠시 뒤 다시 시도해 주세요.",
"testReasonServerError": "AI 제공자가 서버 오류를 반환했어요.",
- "testReasonBadRequest": "제공자가 요청을 거부했어요(HTTP {status}) — 엔드포인트는 응답했으니 연결 문제가 아니라 요청 형식이나 모델 이름 문제예요.",
+ "testReasonBadRequest": "제공자가 요청을 거부했어요(HTTP {status}), 엔드포인트는 응답했으니 연결 문제가 아니라 요청 형식이나 모델 이름 문제예요.",
"testReasonUnreachable": "AI 제공자에 연결하지 못했어요.",
"coachMemory": {
"title": "코치가 기억하는 것",
@@ -5958,7 +6175,6 @@
"categoryGoal": "목표",
"categoryConstraint": "제약",
"categoryContext": "맥락",
- "forget": "잊기",
"forgetAria": "이 정보 잊기",
"forgetTitle": "이 정보를 잊을까요?",
"forgetBody": "다음 메시지부터 코치가 이 정보를 쓰지 않아요. 한번 잊은 정보는 되돌릴 수 없어요.",
@@ -5974,7 +6190,8 @@
"forgotError": "그 정보를 잊지 못했어요. 다시 시도해 주세요.",
"clearedToast": "저장된 정보 {count}개를 잊었어요.",
"clearError": "저장된 정보를 지우지 못했어요. 다시 시도해 주세요.",
- "detail": "코치는 이 정보로 답변을 상황에 맞게 유지해요. 잊었으면 하는 건 지우세요."
+ "detail": "코치는 이 정보로 답변을 상황에 맞게 유지해요. 잊었으면 하는 건 지우세요.",
+ "categoryMedication": "약"
},
"coachReminders": {
"title": "내 알림",
@@ -6034,20 +6251,20 @@
"autoRead": {
"title": "AI로 문서 자동 읽기",
"subLabel": "문서마다 따로 누르지 않아도 설정한 AI 제공자가 문서를 읽고 색인해요.",
- "honesty": "켜 두면 올리는 문서마다 설정한 AI 제공자에게 전송되어 읽히고, 설명이 붙고, 검색용으로 색인돼요 — 문서별 확인 절차는 없어요. 켜는 순간 과거 문서에도 적용돼요. 이미 올렸지만 아직 요약이 없는 문서도 켤 때마다 최대 200개까지 함께 전송돼요. 이 작업은 서버에서 이뤄지고 문서 내용은 이 기기를 떠나 그 제공자에게 전달돼요. 설정한 제공자가 구독 연결(API 키가 아니라 로그인한 AI 계정)이라면 그 제공자의 데이터 정책이 적용돼요 — 구독 제공자는 내용을 모델 개선에 쓸 수 있고, 이를 보호하는 데이터 처리 계약도 없어요. 셀프 호스팅 로컬 모델이 읽은 문서는 서버 밖으로 나가지 않아요. 기본값은 꺼짐이고, 이 조건을 받아들일 때만 켜세요.",
- "confirm": "이해했어요 — 켜기",
+ "honesty": "켜 두면 올리는 문서마다 설정한 AI 제공자에게 전송되어 읽히고, 설명이 붙고, 검색용으로 색인돼요. 문서별 확인 절차는 없어요. 켜는 순간 과거 문서에도 적용돼요. 이미 올렸지만 아직 요약이 없는 문서도 켤 때마다 최대 200개까지 함께 전송돼요. 이 작업은 서버에서 이뤄지고 문서 내용은 이 기기를 떠나 그 제공자에게 전달돼요. 설정한 제공자가 구독 연결(API 키가 아니라 로그인한 AI 계정)이라면 그 제공자의 데이터 정책이 적용돼요. 구독 제공자는 내용을 모델 개선에 쓸 수 있고, 이를 보호하는 데이터 처리 계약도 없어요. 셀프 호스팅 로컬 모델이 읽은 문서는 서버 밖으로 나가지 않아요. 기본값은 꺼짐이고, 이 조건을 받아들일 때만 켜세요.",
+ "confirm": "이해했어요, 켜기",
"cancel": "취소"
},
"centralCodex": {
"title": "서버의 공용 AI 연결 사용",
- "subLabel": "운영자가 이 서버에 설정한 연결로 AI 요청을 보내고, 내 제공자는 대체 수단으로 함께 써요.",
- "honesty": "공용 연결은 운영자가 이 서버의 모든 사용자를 위해 연결해 둔 로그인 AI 계정 하나예요 — 내 키가 아니에요. 이 연결로 보낸 요청은 기본적으로 제공자의 학습에 쓰이고 이를 보호하는 데이터 처리 계약도 없으며, 계정의 시간당·주당 한도를 사용을 선택한 다른 모든 사용자와 나눠 써요. 내가 설정한 제공자는 여전히 먼저 시도돼요. 이 조건을 받아들일 때만 켜세요.",
- "confirm": "이해했어요 — 연결 사용",
+ "subLabel": "AI 요청을 서버의 공유 연결로 보내고, 내 제공자는 대체 수단으로 써요.",
+ "honesty": "공용 연결은 운영자가 이 서버의 모든 사용자를 위해 연결해 둔 로그인 AI 계정 하나예요. 내 키가 아니에요. 이 연결로 보낸 요청은 기본적으로 제공자의 학습에 쓰이고 이를 보호하는 데이터 처리 계약도 없으며, 계정의 시간당·주당 한도를 사용을 선택한 다른 모든 사용자와 나눠 써요. 내가 설정한 제공자는 여전히 먼저 시도돼요. 이 조건을 받아들일 때만 켜세요.",
+ "confirm": "이해했어요, 연결 사용",
"cancel": "취소"
},
"consent": {
"title": "AI 동의",
- "activeSince": "{date}에 동의했어요. AI 기능을 위해 건강 데이터가 설정한 제공자에게 전송될 수 있어요.",
+ "activeSince": "{date}에 동의했어요: AI 기능을 위해 건강 데이터가 설정한 제공자에게 전송될 수 있어요.",
"withdrawn": "동의를 철회했어요. 다른 동의가 적용되지 않는 한, 코치와 인사이트는 더 이상 이 서버가 제공하는 AI 연결을 사용하지 않고 온라인 모델도 문서를 읽지 않아요. 자신의 키나 로컬 모델을 사용하는 코치와 인사이트는 계속 작동해요.",
"withdraw": "동의 철회",
"confirmBody": "다른 동의가 여전히 적용되지 않는 한, 다음이 즉시 중지됩니다: 이 서버가 제공하는 AI 연결을 사용하는 코치와 인사이트, 그리고 온라인 모델을 통한 문서 읽기. 자신의 키나 로컬 모델을 사용하는 코치와 인사이트는 계속 작동합니다. 인사이트에 적용되는 다른 동의가 없으면 모델이 작성한 메모가 삭제됩니다: 상태 메모, 일일 브리핑, 모델이 작성한 기간 요약, 새 측정값 알림 문장, 운동 메모. 코치 대화, 코치가 기억한 내용, 계획, 문서 요약은 그대로 남으며 직접 읽거나 삭제할 수 있습니다.",
@@ -6058,7 +6275,7 @@
"error": "철회를 저장하지 못했어요. 다시 시도해 주세요.",
"documentReading": {
"title": "문서 읽기 동의",
- "activeSince": "{date}에 동의함. 문서, 스캔한 검사 결과지, 약 설명이 읽기용으로 AI 서비스에 전송될 수 있습니다.",
+ "activeSince": "{date}에 동의했어요: 문서, 스캔한 검사지, 약 설명이 읽기용으로 AI 서비스에 전송될 수 있어요.",
"confirmBody": "문서, 스캔한 검사 결과지, 약 설명이 더 이상 읽기용으로 전송되지 않습니다. 이미 가지고 있는 것은 삭제되지 않습니다."
}
},
@@ -6080,7 +6297,21 @@
"title": "코치 계획",
"description": "코치와 합의한 계획을 읽거나 삭제할 수 있습니다.",
"open": "계획 열기"
- }
+ },
+ "reasoning": {
+ "label": "추론",
+ "hint": "Gemma나 Qwen 같은 추론 모델은 답하기 전에 먼저 생각합니다. 끄면 답변 예산을 모두 답변 자체에 씁니다.",
+ "options": {
+ "default": "기본값",
+ "none": "끄기",
+ "low": "낮음",
+ "medium": "보통",
+ "high": "높음"
+ },
+ "coachNote": "코치에서는 코치 설정의 사고 깊이가 적용됩니다."
+ },
+ "providerCardDescription": "HealthLog가 가장 먼저 쓰는 제공자와 그 키, 모델이에요.",
+ "runtimeTitle": "확인 및 새로고침"
},
"dashboard": {
"units": {
@@ -6104,8 +6335,8 @@
"withingsClientId": "클라이언트 ID",
"withingsClientSecret": "클라이언트 시크릿",
"withingsCredentialsSaved": "자격 증명을 저장했어요",
- "withingsCredentialsSavedPlaceholder": "저장됨 — 바꾸려면 새로 입력",
- "withingsCredentialsSavedPlaceholderSecret": "저장됨 — 바꾸려면 새로 입력",
+ "withingsCredentialsSavedPlaceholder": "저장됨: 바꾸려면 새로 입력",
+ "withingsCredentialsSavedPlaceholderSecret": "저장됨: 바꾸려면 새로 입력",
"withingsSaveCredentials": "저장",
"configured": "설정됨",
"withingsSync": "지금 동기화",
@@ -6120,7 +6351,7 @@
},
"withingsFullSyncResult": "측정값 {count}개를 전체 동기화했어요",
"withingsSyncFailed": "동기화에 실패했어요",
- "withingsOauthConnected": "Withings를 연결했어요 — 첫 동기화가 백그라운드에서 시작돼요.",
+ "withingsOauthConnected": "Withings를 연결했어요. 첫 동기화가 백그라운드에서 시작돼요.",
"withingsOauthFailed": "Withings 연결에 실패했어요",
"withingsOauthError": {
"csrf1": "브라우저 쿠키가 없어서 보안 확인에 실패했어요. 같은 브라우저에서 연결을 다시 시작해 주세요.",
@@ -6145,8 +6376,8 @@
"whoopClientId": "클라이언트 ID",
"whoopClientSecret": "클라이언트 시크릿",
"whoopCredentialsSaved": "자격 증명을 저장했어요",
- "whoopCredentialsSavedPlaceholder": "저장됨 — 바꾸려면 새로 입력",
- "whoopCredentialsSavedPlaceholderSecret": "저장됨 — 바꾸려면 새로 입력",
+ "whoopCredentialsSavedPlaceholder": "저장됨: 바꾸려면 새로 입력",
+ "whoopCredentialsSavedPlaceholderSecret": "저장됨: 바꾸려면 새로 입력",
"whoopSaveCredentials": "저장",
"whoopSync": "지금 동기화",
"whoopFullSync": "전체 데이터 동기화",
@@ -6161,7 +6392,7 @@
"whoopConnect": "WHOOP으로 연결",
"whoopNoCredentials": "WHOOP을 연결하려면 위에 API 자격 증명을 입력해 주세요.",
"whoopBackfillInProgress": "WHOOP 기록을 백그라운드에서 가져오는 중이에요…",
- "whoopOauthConnected": "WHOOP을 연결했어요 — 첫 동기화가 백그라운드에서 시작돼요.",
+ "whoopOauthConnected": "WHOOP을 연결했어요. 첫 동기화가 백그라운드에서 시작돼요.",
"whoopOauthFailed": "WHOOP 연결에 실패했어요",
"whoopOauthError": {
"csrf1": "브라우저 쿠키가 없어서 보안 확인에 실패했어요. 같은 브라우저에서 연결을 다시 시작해 주세요.",
@@ -6175,14 +6406,14 @@
},
"fitbit": "Fitbit",
"fitbitTag": "Fitbit & Pixel",
- "fitbitDescription": "Fitbit을 연결하면 Fitbit과 Pixel Watch의 건강 지표를 동기화해요 — 체중, 심박수 등.",
+ "fitbitDescription": "Fitbit을 연결하면 Fitbit과 Pixel Watch의 건강 지표를 동기화해요: 체중, 심박수 등.",
"fitbitExperimentalBadge": "실험 기능",
"fitbitCredentials": "API 자격 증명",
"fitbitClientId": "클라이언트 ID",
"fitbitClientSecret": "클라이언트 시크릿",
"fitbitCredentialsSaved": "자격 증명을 저장했어요",
- "fitbitCredentialsSavedPlaceholder": "저장됨 — 바꾸려면 새로 입력",
- "fitbitCredentialsSavedPlaceholderSecret": "저장됨 — 바꾸려면 새로 입력",
+ "fitbitCredentialsSavedPlaceholder": "저장됨: 바꾸려면 새로 입력",
+ "fitbitCredentialsSavedPlaceholderSecret": "저장됨: 바꾸려면 새로 입력",
"fitbitSaveCredentials": "저장",
"fitbitSync": "지금 동기화",
"fitbitFullSync": "전체 데이터 동기화",
@@ -6197,7 +6428,7 @@
"fitbitConnect": "Fitbit으로 연결",
"fitbitNoCredentials": "Fitbit을 연결하려면 위에 API 자격 증명을 입력해 주세요.",
"fitbitBackfillInProgress": "Fitbit 기록을 백그라운드에서 가져오는 중이에요…",
- "fitbitOauthConnected": "Fitbit을 연결했어요 — 첫 동기화가 백그라운드에서 시작돼요.",
+ "fitbitOauthConnected": "Fitbit을 연결했어요. 첫 동기화가 백그라운드에서 시작돼요.",
"fitbitOauthFailed": "Fitbit 연결에 실패했어요",
"googleHealth": "Google Health",
"googleHealthTag": "Fitbit & Pixel",
@@ -6207,8 +6438,8 @@
"googleHealthClientId": "클라이언트 ID",
"googleHealthClientSecret": "클라이언트 시크릿",
"googleHealthCredentialsSaved": "자격 증명을 저장했어요",
- "googleHealthCredentialsSavedPlaceholder": "저장됨 — 바꾸려면 새로 입력",
- "googleHealthCredentialsSavedPlaceholderSecret": "저장됨 — 바꾸려면 새로 입력",
+ "googleHealthCredentialsSavedPlaceholder": "저장됨: 바꾸려면 새로 입력",
+ "googleHealthCredentialsSavedPlaceholderSecret": "저장됨: 바꾸려면 새로 입력",
"googleHealthSaveCredentials": "저장",
"googleHealthSync": "지금 동기화",
"googleHealthSyncCheckStatus": "상태 새로고침",
@@ -6242,9 +6473,9 @@
"googleHealthConnect": "Google로 연결",
"googleHealthNoCredentials": "Google Health를 연결하려면 위에 API 자격 증명을 입력해 주세요.",
"googleHealthBackfillInProgress": "Google Health 기록을 백그라운드에서 가져오는 중이에요…",
- "googleHealthReauthBanner": "Google이 연결을 만료시켰어요 — OAuth 앱이 테스트 모드로 있는 동안 주기적으로 생기는 일이에요. 다시 연결하면 동기화가 이어져요.",
+ "googleHealthReauthBanner": "Google이 연결을 만료시켰어요. OAuth 앱이 테스트 모드로 있는 동안 주기적으로 생기는 일이에요. 다시 연결하면 동기화가 이어져요.",
"googleHealthReconnect": "다시 연결",
- "googleHealthOauthConnected": "Google Health를 연결했어요 — 첫 동기화가 백그라운드에서 시작돼요.",
+ "googleHealthOauthConnected": "Google Health를 연결했어요. 첫 동기화가 백그라운드에서 시작돼요.",
"googleHealthOauthFailed": "Google Health 연결에 실패했어요",
"googleHealthOauthError": {
"csrf1": "브라우저 쿠키가 없어서 보안 확인에 실패했어요. 같은 브라우저에서 연결을 다시 시작해 주세요.",
@@ -6279,10 +6510,10 @@
},
"integrationPill": {
"connected": "연결됨",
- "errorReconnect": "오류 — 다시 연결",
+ "errorReconnect": "오류: 다시 연결",
"warningServerError": "동기화 실패 중",
"failureCount": "{count}/{threshold}",
- "parkedReconnect": "일시중지 — 다시 연결",
+ "parkedReconnect": "일시중지: 다시 연결",
"notConnected": "연결 안 됨",
"justNow": "방금",
"minutesAgo": "{count}분 전",
@@ -6297,7 +6528,7 @@
},
"integrationFreshness": {
"title": "받은 데이터",
- "summary": "데이터 유형 {count}개 · 최신 {relative}",
+ "summary": "데이터 유형 {count}개, 최신 {relative}",
"quiet": "{count}개 잠잠",
"workouts": "운동"
},
@@ -6311,31 +6542,33 @@
"fitbitViewData": "수면 보기",
"googleHealthViewData": "걸음 수 보기",
"apiTokens": "API 토큰",
- "apiTokensDescription": "외부에서 복용을 기록할 때 쓰는 API 토큰을 확인하고 폐기해요.",
+ "apiTokensDescription": "측정값, 운동, 문서, 복용 기록용 토큰을 확인하고 폐기해요.",
"measurementsToken": {
"title": "측정 토큰",
"description": "체중계나 시계, 홈 오토메이션이 측정값을 기록할 수 있도록 토큰을 만들어요.",
"detail": "토큰은 한 번만 표시돼요.",
"scopeNote": "본인 기록에 새 측정값을 추가하는 것만 할 수 있어요. 데이터를 읽거나, 무언가를 고치거나 지우거나, 다른 토큰을 만들 수는 없어요. 측정값은 외부 기기에서 온 것으로 표시되니 나중에 구분할 수 있고, 언제든 고칠 수 있어요.",
- "createFailed": "토큰을 만들지 못했어요. 다시 시도해 주세요."
+ "createFailed": "토큰을 만들지 못했어요. 다시 시도해 주세요.",
+ "namePlaceholder": "이름 (예: Home Assistant)"
},
"workoutsToken": {
"title": "운동 토큰",
"description": "시계 연동 브리지나 동기화 스크립트가 운동 기록을 추가할 수 있도록 토큰을 만들어요.",
"detail": "토큰은 한 번만 표시돼요.",
"scopeNote": "본인 기록에 새 운동을 추가하는 것만 할 수 있어요. 데이터를 읽거나, 무언가를 고치거나 지우거나, 다른 토큰을 만들 수는 없어요. 이 토큰으로 추가된 운동은 외부 기기에서 온 것으로 표시되고, 휴대폰이 같은 운동을 기록했다면 휴대폰의 기록이 보여요.",
- "createFailed": "토큰을 만들지 못했어요. 다시 시도해 주세요."
+ "createFailed": "토큰을 만들지 못했어요. 다시 시도해 주세요.",
+ "namePlaceholder": "이름 (예: Garmin)"
},
"documentsToken": {
"title": "문서 토큰",
"description": "Paperless-ngx, Papra 또는 가져오기 스크립트가 문서함에 문서를 추가할 수 있도록 토큰을 만들어요.",
"detail": "토큰은 한 번만 표시돼요.",
"scopeNote": "본인 문서함에 새 문서를 추가하는 것만 할 수 있어요. 목록을 보거나, 열거나, 내려받거나, 고치거나 지울 수 없고, 다른 토큰을 만들 수도 없어요. 파일이 저장됐는지만 알 수 있고, 어떻게 정리했는지는 알 수 없어요.",
- "createFailed": "토큰을 만들지 못했어요. 다시 시도해 주세요."
+ "createFailed": "토큰을 만들지 못했어요. 다시 시도해 주세요.",
+ "namePlaceholder": "이름 (예: Paperless-ngx)"
},
- "tokenMintMovedDescription": "토큰은 쓰이는 곳에서 발급돼요. 측정값과 문서는 위에서, 특정 약은 그 약의 API 엔드포인트에서 만들 수 있어요.",
- "tokenNamePlaceholder": "토큰 이름 (예: iPhone 단축어)",
- "tokenCreated": "토큰을 만들었어요 — 지금 복사하세요! 다시 보여주지 않아요.",
+ "tokenMintMovedDescription": "토큰은 쓰이는 곳에서 발급돼요. 측정값, 운동, 문서는 위에서, 특정 약은 그 약의 API 엔드포인트에서 만들 수 있어요.",
+ "tokenCreated": "토큰을 만들었어요. 지금 복사하세요! 다시 보여주지 않아요.",
"tokenCopy": "토큰 복사",
"tokenCopied": "토큰을 클립보드에 복사했어요",
"tokenCopyFailed": "토큰을 복사하지 못했어요",
@@ -6374,7 +6607,7 @@
"deleteAccountCta": "계정 삭제",
"deleteAccountConfirmTitle": "계정을 영구히 삭제할까요?",
"deleteAccountConfirmDescription": "계정과 모든 건강 데이터, 패스키, 세션, 감사 기록이 영구히 삭제돼요. 삭제 직후 로그아웃돼요.",
- "deleteAccountSuccess": "계정을 삭제했어요 — 로그아웃할게요.",
+ "deleteAccountSuccess": "계정을 삭제했어요. 로그아웃할게요.",
"deleteAccountFailed": "계정을 삭제하지 못했어요",
"deleteAccountFinal": "영구 삭제",
"saved": "저장됨",
@@ -6397,7 +6630,7 @@
"testing": "테스트 중…",
"ok": "연결됨 (지연 {latency}ms)",
"errors": {
- "credentials_rejected": "자격 증명이 거부됐어요 — 토큰을 확인해 주세요",
+ "credentials_rejected": "자격 증명이 거부됐어요. 토큰을 확인해 주세요",
"rate_limited": "상대 서버에서 요청 수를 제한했어요",
"timeout": "요청 시간이 초과됐어요",
"upstream_error": "상대 서버가 오류를 반환했어요",
@@ -6408,7 +6641,7 @@
"private_origin_not_approved": "비공개 네트워크 대상이에요. 운영자가 NOTIFICATION_PRIVATE_ORIGINS에 이 출처를 등록해야 해요",
"private_origin_not_grantable": "이 주소는 링크 로컬, 메타데이터 또는 미지정 주소예요. 운영자 등록으로도 열 수 없으니 릴레이가 실제로 수신하는 주소를 사용해 주세요",
"url_invalid": "URL이 올바르지 않아요",
- "redirected": "엔드포인트가 리디렉션돼요 — URL을 확인해 주세요",
+ "redirected": "엔드포인트가 리디렉션돼요. URL을 확인해 주세요",
"endpoint_not_found": "해당 URL에 엔드포인트가 없어요",
"credentials_unreadable": "자격 증명을 복호화할 수 없어요",
"upstream_invalid_json": "상대 서버가 잘못된 JSON을 보냈어요",
@@ -6419,7 +6652,7 @@
},
"notificationStatus": {
"title": "발송 상태",
- "description": "채널별 상태예요 — '자동 비활성화'는 영구 오류가 이어져 재시도를 멈췄다는 뜻이에요.",
+ "description": "채널별 상태이며, '자동 비활성화'는 영구 오류가 이어져 재시도를 멈췄다는 뜻이에요.",
"emptyDescription": "설정 › 연동 › 발송 채널에서 채널을 설정해 주세요.",
"stateActive": "사용 중",
"stateAutoDisabled": "자동 비활성화",
@@ -6445,7 +6678,7 @@
},
"healthRecord": {
"title": "건강 기록 내보내기",
- "description": "의사에게 보여줄 전체 기록 — PDF, FHIR, 또는 둘 다.",
+ "description": "의사에게 보여줄 전체 기록: PDF, FHIR, 또는 둘 다.",
"format": "형식",
"format_pdf": "PDF",
"format_fhir": "구조화 (FHIR)",
@@ -6477,7 +6710,7 @@
"createTitle": "새 공유 링크",
"createDescription": "링크는 읽기 전용이고 유효 기간이 있어요.",
"label": "라벨",
- "labelPlaceholder": "예: 김 선생님 — 건강검진",
+ "labelPlaceholder": "예: 김 선생님, 건강검진",
"range": "공유할 기록 기간(일)",
"rangeHint": "공유 범위가 과거 며칠까지 포함될지 정해요.",
"expiry": "만료까지(일)",
@@ -6497,7 +6730,7 @@
"statusRevoked": "해지됨",
"statusExpired": "만료됨",
"inactiveTitle": "해지·만료됨 ({count})",
- "shownOnce": "한 번만 보여줘요. 지금 QR 코드와 암호를 저장해 두세요 — 나중에는 둘 다 복구할 수 없어요.",
+ "shownOnce": "한 번만 보여줘요. 지금 QR 코드와 암호를 저장해 두세요. 나중에는 둘 다 복구할 수 없어요.",
"qrAlt": "공유 링크와 암호가 담긴 QR 코드",
"linkLabel": "링크",
"passphraseLabel": "암호",
@@ -6508,12 +6741,12 @@
"attachTitle": "문서 첨부",
"attachButton": "문서 선택",
"attachCount": "{max}개 중 {count}개 선택됨",
- "attachFrozen": "링크를 만들면 문서 구성이 고정돼요 — 나중에 문서를 추가하거나 뺄 수 없어요.",
+ "attachFrozen": "링크를 만들면 문서 구성이 고정돼요. 나중에 문서를 추가하거나 뺄 수 없어요.",
"attachRemove": "제거: {title}",
"exifNote": "공유할 때 JPEG, PNG, WebP 이미지에서는 카메라 메타데이터(EXIF/GPS)를 제거해요. PDF, TIFF, HEIC, Office 파일은 그대로 전달돼요.",
"documentCount": "첨부한 문서: {count}개",
"qrScanTitle": "다른 기기에서 열려면 스캔하세요",
- "qrScanHint": "휴대폰 카메라로 코드를 비추면 공유된 기록이 열려요 — 암호는 이미 포함돼 있어요.",
+ "qrScanHint": "휴대폰 카메라로 코드를 비추면 공유된 기록이 열려요. 암호는 이미 포함돼 있어요.",
"pickerTitle": "문서 첨부",
"pickerDescription": "보관함에서 검색해 공유할 문서를 고르세요. 최대 {max}개예요.",
"pickerSearch": "문서 검색",
@@ -6526,7 +6759,8 @@
"statusNeedsReselect": "내용 다시 선택",
"reselectReason": "이 링크는 선택한 적 없는 범위를 제공하고 있어서 닫았어요. 새로 만들고 담을 내용을 골라 주세요.",
"reselectAction": "새 링크 만들기",
- "createDetail": "토큰은 만들 때 한 번만 보여줘요 — 그때 저장해 두세요. 복구할 수 없어요."
+ "createDetail": "토큰은 만들 때 한 번만 보여줘요. 그때 저장해 두세요. 복구할 수 없어요.",
+ "activeDescription": "공유한 링크와 만료 시점, 열람 횟수를 보여줘요."
},
"genderOther": "기타",
"nightscout": "Nightscout",
@@ -6534,10 +6768,10 @@
"nightscoutDescription": "직접 운영하는 Nightscout 사이트를 연결해 연속 혈당 측정값을 혈당 패널로 동기화해요.",
"nightscoutViewData": "혈당 보기",
"nightscoutUrl": "Nightscout URL",
- "nightscoutUrlSavedPlaceholder": "저장됨 — 바꾸려면 새 URL을 입력하세요",
+ "nightscoutUrlSavedPlaceholder": "저장됨: 바꾸려면 새 URL을 입력하세요",
"nightscoutToken": "API 토큰",
"nightscoutTokenOptional": "공개 사이트라면 선택 사항이에요",
- "nightscoutTokenSavedPlaceholder": "저장됨 — 바꾸려면 새 토큰을 입력하세요",
+ "nightscoutTokenSavedPlaceholder": "저장됨: 바꾸려면 새 토큰을 입력하세요",
"nightscoutTokenHelp": "Nightscout 관리자 도구에서 발급한 액세스 토큰을 붙여 넣으세요. 사이트를 누구나 읽을 수 있다면 비워 두세요.",
"nightscoutPrivateOperatorTitle": "비공개 Nightscout에 접근하려면 서버 운영자의 승인이 필요해요.",
"nightscoutPrivateOperatorDescription": "서버 운영자에게 이 Nightscout 출처를 그대로 승인해 달라고 요청하세요.",
@@ -6558,7 +6792,7 @@
"polarClientId": "클라이언트 ID",
"polarClientSecret": "클라이언트 시크릿",
"polarCredentialsSaved": "자격 증명을 저장했어요",
- "polarCredentialsSavedPlaceholder": "저장됨 — 바꾸려면 새로 입력하세요",
+ "polarCredentialsSavedPlaceholder": "저장됨: 바꾸려면 새로 입력하세요",
"polarSaveCredentials": "저장",
"polarUnavailable": "이 서버에는 Polar가 설정돼 있지 않아요. 위에 직접 Polar OAuth 자격 증명을 넣거나, 관리자에게 공용 앱 설정을 요청하세요.",
"polarConnect": "Polar 연결",
@@ -6568,7 +6802,7 @@
"polarDisconnect": "연결 해제",
"polarDisconnectTitle": "Polar 연결을 해제할까요?",
"polarDisconnectDescription": "앞으로 동기화가 멈춰요. 이미 가져온 측정값은 기록에 그대로 남아요.",
- "polarOauthConnected": "Polar를 연결했어요 — 첫 동기화가 백그라운드에서 시작돼요.",
+ "polarOauthConnected": "Polar를 연결했어요. 첫 동기화가 백그라운드에서 시작돼요.",
"polarOauthFailed": "Polar 연결에 실패했어요",
"polarOauthError": {
"csrf1": "브라우저 쿠키가 없어서 보안 확인에 실패했어요. 같은 브라우저에서 연결을 다시 시작해 주세요.",
@@ -6587,7 +6821,7 @@
"ouraClientId": "클라이언트 ID",
"ouraClientSecret": "클라이언트 시크릿",
"ouraCredentialsSaved": "자격 증명을 저장했어요",
- "ouraCredentialsSavedPlaceholder": "저장됨 — 바꾸려면 새로 입력하세요",
+ "ouraCredentialsSavedPlaceholder": "저장됨: 바꾸려면 새로 입력하세요",
"ouraSaveCredentials": "저장",
"ouraUnavailable": "이 서버에는 Oura가 설정돼 있지 않아요. 위에 직접 Oura OAuth 자격 증명을 넣거나, 관리자에게 공용 앱 설정을 요청하세요.",
"ouraConnect": "Oura 연결",
@@ -6597,7 +6831,7 @@
"ouraDisconnect": "연결 해제",
"ouraDisconnectTitle": "Oura 연결을 해제할까요?",
"ouraDisconnectDescription": "앞으로 동기화가 멈춰요. 이미 가져온 측정값은 기록에 그대로 남아요.",
- "ouraOauthConnected": "Oura를 연결했어요 — 첫 동기화가 백그라운드에서 시작돼요.",
+ "ouraOauthConnected": "Oura를 연결했어요. 첫 동기화가 백그라운드에서 시작돼요.",
"ouraOauthFailed": "Oura 연결에 실패했어요",
"ouraOauthError": {
"csrf1": "브라우저 쿠키가 없어서 보안 확인에 실패했어요. 같은 브라우저에서 연결을 다시 시작해 주세요.",
@@ -6607,6 +6841,7 @@
"nocreds": "이 서버에는 Oura가 설정돼 있지 않아요. 관리자에게 Oura OAuth 자격 증명을 추가해 달라고 요청하세요.",
"token": "Oura와의 토큰 교환에 실패했어요. 연결을 다시 시작해 주세요.",
"rate_limited": "연결 시도가 너무 많아요. 잠시 후 다시 시도해 주세요.",
+ "issuer": "로그인 응답이 Oura의 인증 서버에서 오지 않아서 안전을 위해 연결을 중단했어요. 연결을 다시 시작해 주세요. 계속 발생하면 관리자에게 알려 주세요.",
"generic": "연결 중 알 수 없는 오류가 났어요. 연결을 다시 시작해 주세요."
},
"integrationSetupGuide": "설정 가이드",
@@ -6618,10 +6853,17 @@
"savedOn": "코치를 켰어요.",
"savedOff": "코치를 숨겼어요.",
"saveError": "변경 사항을 저장하지 못했어요. 다시 시도해 주세요."
+ },
+ "memory": {
+ "edit": "편집",
+ "source": {
+ "user": "내가 알려 줌",
+ "coach": "코치가 기억함"
+ }
}
},
"integrationRedirectGuide": {
- "intro": "직접 만든 {provider} 개발자 앱으로 연결해요 — 설정에 5분쯤 걸려요:",
+ "intro": "직접 만든 {provider} 개발자 앱으로 연결해요. 설정에 5분쯤 걸려요:",
"step1": "{provider} 개발자 포털에서 앱을 만드세요.",
"step2": "리디렉션/콜백 URL을 다음으로 설정하세요:",
"step3": "발급받은 클라이언트 ID와 클라이언트 시크릿을 위에 붙여 넣으세요."
@@ -6709,7 +6951,7 @@
},
"passkeyNudge": {
"title": "패스키를 추가하면 더 빠르게 로그인해요",
- "body": "패스키로 지문, 얼굴, 기기 PIN을 써서 로그인할 수 있어요 — 비밀번호를 외울 필요가 없어요."
+ "body": "패스키로 지문, 얼굴, 기기 PIN을 써서 로그인할 수 있어요. 비밀번호를 외울 필요가 없어요."
},
"trustedDevices": {
"title": "신뢰하는 기기",
@@ -6728,7 +6970,7 @@
"revokeAllConfirmBody": "이 기기를 포함해 모든 기기에서 다음에 로그인할 때 2단계 인증을 다시 물어봐요."
},
"revokeSessionConfirmTitle": "이 기기를 로그아웃할까요?",
- "revokeSessionConfirmBody": "{device} — 다시 로그인해야 해요. 기기에 기록된 내용은 영향받지 않아요.",
+ "revokeSessionConfirmBody": "{device}: 다시 로그인해야 해요. 기기에 기록된 내용은 영향받지 않아요.",
"signOutEverywhereConfirmTitle": "다른 기기를 모두 로그아웃할까요?",
"signOutEverywhereConfirmBody": "다른 모든 기기, 연결된 모든 AI 어시스턴트와 모든 API 토큰은 다시 로그인하거나 다시 설정해야 합니다. 이 기기는 로그인 상태로 유지됩니다. 기록한 내용에는 영향이 없습니다.",
"signOutEverywhereShareLinks": "의료진 공유 링크도 종료",
@@ -6745,12 +6987,11 @@
},
"stored": {
"title": "저장되는 항목",
- "measurements": "측정값 (체중, 혈압, 맥박, 혈당, 수면, 체성분)",
- "medications": "약과 복용 기록",
- "moodLabs": "기분 기록과 검사 결과",
- "coach": "코치 대화",
+ "measurements": "측정값: 체중, 혈압, 맥박과 직접 또는 기기로 기록한 모든 값",
"integrations": "연동 토큰 (암호화됨)",
- "security": "계정·보안 활동 (세션, 로그인 기록)"
+ "security": "계정·보안 활동 (세션, 로그인 기록)",
+ "profile": "프로필, 병력, 건강 기록 정보",
+ "modules": "모듈별 기록: {list}"
},
"retention": {
"title": "보관 기간",
@@ -6781,13 +7022,11 @@
"mcp": {
"enableTitle": "원격 MCP 엔드포인트",
"enableDescription": "MCP를 지원하는 어시스턴트(Claude.ai, ChatGPT)가 범위가 제한된 토큰으로 건강 데이터를 읽게 해요.",
- "enabled": "켜짐",
- "disabled": "꺼짐",
"enableToggleLabel": "MCP 엔드포인트 켜기",
"endpointLabel": "커넥터 URL",
"connectDescription": "어시스턴트의 커스텀 커넥터 설정에 이 URL을 붙여 넣고, 아래 토큰으로 인증하거나 안내가 나오면 로그인하세요.",
"tokensTitle": "커넥터 토큰",
- "tokensDescription": "어시스턴트를 연결할 토큰을 발급하세요 — 아래에서 기록을 허용하지 않으면 읽기 전용이에요.",
+ "tokensDescription": "어시스턴트를 연결할 토큰을 발급하세요, 아래에서 기록을 허용하지 않으면 읽기 전용이에요.",
"scopeNote": "새 토큰은 본인 기록만 읽을 수 있어요. 쓰기 권한(health:write)은 아래 토글로 토큰마다 따로 부여해요. 토큰은 관리자 기능에 절대 접근할 수 없어요.",
"writeScopeToggleLabel": "기록 허용 (health:write)",
"writeScopeNote": "health:write 범위를 추가해 어시스턴트가 새 측정값과 기분 기록을 계정에 남길 수 있게 해요. 모든 쓰기는 먼저 확인을 거쳐요. 그래도 토큰은 기존 기록을 삭제하거나 수정할 수 없고, 약을 바꾸거나 REST·관리자 기능에 접근할 수도 없어요. 읽기 전용으로 쓰려면 꺼 두세요.",
@@ -6801,7 +7040,8 @@
"connectionRevokeDescription": "어시스턴트의 접근을 즉시 끊고 갱신도 막아요. 다시 연결하려면 승인을 새로 받아야 해요.",
"connectionRevokeFailed": "어시스턴트 연결을 해제하지 못했어요.",
"connectionsDetail": "연결을 해지하면 갱신 권한까지 포함해 접근이 즉시 끊겨요.",
- "tokensDetail": "토큰은 한 번만 보여줘요."
+ "tokensDetail": "토큰은 한 번만 보여줘요.",
+ "tokenNamePlaceholder": "이름 (예: Claude Desktop)"
},
"strava": "Strava",
"stravaDescription": "Strava 계정을 연결해 활동을 운동 기록으로 동기화해요.",
@@ -6810,7 +7050,7 @@
"stravaClientId": "클라이언트 ID",
"stravaClientSecret": "클라이언트 시크릿",
"stravaCredentialsSaved": "자격 증명을 저장했어요",
- "stravaCredentialsSavedPlaceholder": "저장됨 — 바꾸려면 새로 입력하세요",
+ "stravaCredentialsSavedPlaceholder": "저장됨: 바꾸려면 새로 입력하세요",
"stravaSaveCredentials": "저장",
"stravaUnavailable": "이 서버에는 Strava가 설정돼 있지 않아요. 위에 직접 Strava API 자격 증명을 넣거나, 관리자에게 공용 앱 설정을 요청하세요.",
"stravaConnect": "Strava 연결",
@@ -6820,7 +7060,7 @@
"stravaDisconnect": "연결 해제",
"stravaDisconnectTitle": "Strava 연결을 해제할까요?",
"stravaDisconnectDescription": "앞으로 동기화가 멈춰요. 이미 가져온 활동은 기록에 그대로 남아요.",
- "stravaOauthConnected": "Strava를 연결했어요 — 첫 동기화가 백그라운드에서 시작돼요.",
+ "stravaOauthConnected": "Strava를 연결했어요. 첫 동기화가 백그라운드에서 시작돼요.",
"stravaOauthFailed": "Strava 연결에 실패했어요",
"stravaOauthError": {
"csrf1": "브라우저 쿠키가 없어서 보안 확인에 실패했어요. 같은 브라우저에서 연결을 다시 시작해 주세요.",
@@ -6848,21 +7088,28 @@
"title": "동기화 진행 상황",
"receivedLabel": "받은 기록 수",
"oldestLabel": "받아온 가장 오래된 측정값",
- "flowing": "최근 1시간 안에 데이터가 들어왔어요 — iPhone 앱이 계속 보내는 중이에요.",
- "waiting": "iPhone 앱이 데이터를 더 보내기를 기다리는 중이에요. 앱이 열려 있거나 iOS가 백그라운드 시간을 줄 때 전송이 이어져요 — 처음 대량으로 채워 넣는 동안 잠잠한 구간이 생기는 건 정상이고 오류가 아니에요."
+ "flowing": "최근 1시간 안에 데이터가 들어왔어요. iPhone 앱이 계속 보내는 중이에요.",
+ "waiting": "iPhone 앱이 데이터를 더 보내기를 기다리는 중이에요. 앱이 열려 있거나 iOS가 백그라운드 시간을 줄 때 전송이 이어져요. 처음 대량으로 채워 넣는 동안 잠잠한 구간이 생기는 건 정상이고 오류가 아니에요."
},
"delivery": {
"title": "전송",
"lastTriggerLabel": "마지막 동기화 도착",
"lastBackgroundLabel": "마지막 백그라운드 동기화",
- "backgroundNever": "없음 — 지금까지 모든 동기화가 앱이 열린 상태에서 들어왔어요",
+ "backgroundNever": "없음: 지금까지 모든 동기화가 앱이 열린 상태에서 들어왔어요",
"noSyncYet": "아직 도착한 동기화가 없어서 여기 보여줄 내용이 없어요.",
"trigger": {
"foreground": "앱이 열린 상태에서",
"background": "백그라운드에서",
"push": "푸시 깨우기로",
- "unknown": "앱이 트리거를 알려주지 않았어요"
+ "unknown": "앱이 트리거를 알려주지 않았어요",
+ "manual": "'모두 동기화'로"
}
+ },
+ "freshness": {
+ "title": "데이터 유형별",
+ "lastReceived": "마지막 수신 {relative}",
+ "lastNewValue": "마지막 새 값 {relative}",
+ "never": "아직 수신되지 않음"
}
},
"garminInfo": {
@@ -6923,6 +7170,12 @@
"description": "건강 데이터를 공유하거나 내보내는 작업입니다. 계속하려면 본인임을 확인하세요.",
"signInAgain": "다시 로그인한 후 한 번 더 시도하세요. 이 계정에는 여기서 확인할 비밀번호나 패스키가 없습니다.",
"failed": "확인하지 못했습니다. 입력한 내용을 확인하고 다시 시도하세요."
+ },
+ "passwordTitle": "비밀번호",
+ "tokenNameLabel": "토큰 이름",
+ "displayCard": {
+ "title": "표시",
+ "description": "언어, 단위, 형식은 바꾸는 즉시 저장돼요."
}
},
"admin": {
@@ -6949,20 +7202,20 @@
"memUsedPercent": "메모리",
"diskBusyPercent": "디스크 I/O",
"last2hours": "최근 2시간",
- "empty": "샘플링을 시작했어요 — 1–2분 뒤에 데이터가 나타나요.",
+ "empty": "샘플링을 시작했어요. 1–2분 뒤에 데이터가 나타나요.",
"loadError": "호스트 지표를 불러오지 못했어요. 페이지를 새로고침해 보세요."
},
"aiServerKey": {
"title": "AI 서버 키",
"description": "자기 키를 설정하지 않은 계정을 위한 운영자 키예요.",
- "byokHint": "사용자 본인 키(BYOK, ChatGPT 로그인, 로컬 제공자)가 항상 우선해요 — 서버 키는 제공자 체인의 마지막 단계예요.",
+ "byokHint": "사용자 본인 키(BYOK, ChatGPT 로그인, 로컬 제공자)가 항상 우선해요. 서버 키는 제공자 체인의 마지막 단계예요.",
"consentHint": "서버 키로 나가는 요청에는 사용자의 유효한 AI 동의가 필요해요. 동의 기록이 없으면 아무것도 전송하지 않아요.",
"baseUrlHint": "기본 URL이 API를 결정합니다. https://api.anthropic.com/v1 같은 Anthropic 주소는 인식되며, 그 밖의 호스트는 OpenAI 호환 API로 호출됩니다.",
"statusConfigured": "설정됨",
"statusNotConfigured": "설정 안 됨",
"keyLabel": "API 키",
"keyPlaceholder": "sk-...",
- "keyPlaceholderSet": "저장됨 ({preview}) — 바꾸려면 새 키를 입력하세요",
+ "keyPlaceholderSet": "저장됨 ({preview}): 바꾸려면 새 키를 입력하세요",
"modelLabel": "모델",
"baseUrlLabel": "Base URL",
"save": "저장",
@@ -6981,7 +7234,7 @@
"title": "인사이트 품질",
"loading": "피드백 요약 불러오는 중…",
"loadError": "인사이트 품질 요약을 불러오지 못했어요. 페이지를 새로고침해 보세요.",
- "empty": "아직 피드백이 없어요. 집계는 매일 04:00에 돌아요 — 사용자가 추천을 평가하기 시작하면 도움됨 비율 구간이 여기에 나타나요.",
+ "empty": "아직 피드백이 없어요. 집계는 매일 04:00에 돌아요. 사용자가 추천을 평가하기 시작하면 도움됨 비율 구간이 여기에 나타나요.",
"windowLabel": "최근 {days}일",
"generatedAtLabel": "생성 시각",
"colSeverity": "심각도",
@@ -6996,7 +7249,7 @@
"title": "코치 피드백",
"loading": "코치 피드백 요약 불러오는 중…",
"loadError": "코치 피드백 요약을 불러오지 못했어요. 페이지를 새로고침해 보세요.",
- "empty": "아직 코치 피드백이 없어요. 집계는 매일 04:00에 돌아요 — 사용자가 코치 답변을 평가하면 도움됨 비율 구간이 여기에 나타나요.",
+ "empty": "아직 코치 피드백이 없어요. 집계는 매일 04:00에 돌아요. 사용자가 코치 답변을 평가하면 도움됨 비율 구간이 여기에 나타나요.",
"windowLabel": "최근 {days}일",
"generatedAtLabel": "생성 시각",
"colPromptVersion": "프롬프트 버전",
@@ -7015,13 +7268,13 @@
"appSettingsDescription": "회원가입, 언어, 새 계정 시간대의 전역 기본값이에요.",
"registrationEnabled": "회원가입 허용",
"registrationEnabledDescription": "끄면 새 사용자가 가입할 수 없어요.",
- "registrationOpenHint": "회원가입이 열려 있어요 — 이 인스턴스에 접근할 수 있는 누구나 계정을 만들 수 있어요. 필요한 사람이 다 가입했으면 여기서 닫으세요.",
+ "registrationOpenHint": "회원가입이 열려 있어요. 이 인스턴스에 접근할 수 있는 누구나 계정을 만들 수 있어요. 필요한 사람이 다 가입했으면 여기서 닫으세요.",
"defaultLanguage": "기본 언어",
"defaultLanguageDescription": "새 사용자와 대체값으로 쓰여요.",
"defaultUserTimezone": "새 계정의 기본 시간대",
- "defaultUserTimezoneDescription": "브라우저에서 시간대를 감지하지 못한 새 계정에 쓰여요. 기존 사용자에게는 영향이 없어요.",
+ "defaultUserTimezoneDescription": "브라우저에서 시간대를 감지하지 못한 새 계정에 쓰이고, 기존 계정은 그대로예요.",
"defaultUserTimezoneFallback": "Europe/Berlin (기본값)",
- "servicesGlobal": "서비스 (전역)",
+ "servicesGlobal": "채널과 API",
"servicesGlobalDescription": "끈 서비스는 어떤 사용자도 쓸 수 없어요.",
"telegramGlobal": "Telegram 알림을 허용해요.",
"ntfyGlobal": "ntfy 알림을 허용해요.",
@@ -7029,19 +7282,21 @@
"integrationWebPush": "웹 푸시",
"offlineGeoLabel": "위치 데이터",
"offlineGeoEnabled": "오프라인 (GeoLite2)",
- "offlineGeoFallback": "온라인 ({host}) — GeoLite2 데이터베이스를 마운트하고 GEOLITE2_DIR을 설정하세요",
+ "offlineGeoFallback": "온라인 ({host}). GeoLite2 데이터베이스를 마운트하고 GEOLITE2_DIR을 설정하세요",
+ "offlineGeoOff": "꺼짐",
+ "offlineGeoChosen": "온라인 ({host})",
"apiGlobal": "API 엔드포인트와 API 토큰을 허용해요.",
"umamiTitle": "Umami",
- "umamiDescription": "Umami 웹 분석",
+ "umamiDescription": "Umami 웹 분석이에요.",
"glitchtipTitle": "GlitchTip",
- "glitchtipDescription": "GlitchTip 오류 추적",
+ "glitchtipDescription": "GlitchTip 오류 추적이에요.",
"webPushVapidTitle": "웹 푸시 VAPID",
- "umamiEnabled": "Umami 추적을 켜요.",
+ "umamiEnabled": "Umami 추적 켜기",
"umamiScriptUrl": "Umami 스크립트 URL",
"umamiScriptUrlPlaceholder": "https://analytics.example.com/script.js",
"umamiWebsiteId": "Umami 웹사이트 ID",
"umamiWebsiteIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "glitchtipEnabled": "GlitchTip 오류 추적을 켜요.",
+ "glitchtipEnabled": "GlitchTip 오류 추적 켜기",
"glitchtipDsn": "GlitchTip DSN",
"glitchtipDsnPlaceholder": "https://@glitchtip.example.com/",
"glitchtipEnvironment": "GlitchTip 환경",
@@ -7092,18 +7347,23 @@
"documentAi": {
"title": "문서 읽기",
"description": "저장된 문서의 AI 읽기, 검사 결과지 스캔, 텍스트에서 약물 추출. 서버 밖으로 나가는 데이터의 대부분을 차지합니다."
+ },
+ "reasoning": {
+ "enabled": "깊은 생각 허용",
+ "max": "최대 생각 깊이",
+ "description": "모델이 답하기 전에 생각합니다: 코치, 일일 브리핑, 기간 요약. 더 정확하지만 토큰을 더 씁니다."
}
},
"modules": {
"title": "모듈 사용 여부",
"description": "서버 전체에서 모듈을 끌 수 있어요.",
"saveError": "모듈 사용 여부를 바꾸지 못했어요.",
- "detail": "여기서 끈 모듈은 개인 설정과 관계없이 모든 계정에서 꺼져요. 핵심 영역(체중, 혈압, 맥박, 복약)은 항상 켜져 있고 끌 수 없어요.",
- "coachFollowsSwitch": "어시스턴트의 코치 스위치를 따릅니다."
+ "detail": "체중, 혈압, 맥박은 핵심 측정값이라 항상 켜져 있어요.",
+ "coachFollowsSwitch": "코치 섹션의 코치 스위치를 따라요."
},
"invites": {
- "title": "초대",
- "description": "가입 초대 링크 — 회원가입이 닫혀 있어도 동작해요.",
+ "title": "초대 링크",
+ "description": "링크마다 회원가입이 닫혀 있어도 가입할 수 있어요.",
"create": "초대 만들기",
"createTitle": "초대 만들기",
"createDescription": "회원가입이 닫혀 있어도 이 링크로는 가입할 수 있어요.",
@@ -7115,7 +7375,7 @@
"createdToast": "초대를 만들었어요",
"createError": "초대를 만들지 못했어요.",
"tokenTitle": "초대 링크",
- "tokenOnceHint": "지금만 복사할 수 있어요 — 링크를 복사하거나 QR 코드를 스캔하게 하세요.",
+ "tokenOnceHint": "지금만 복사할 수 있어요. 링크를 복사하거나 QR 코드를 스캔하게 하세요.",
"hashExplainer": "링크의 해시만 저장하기 때문에 나중에 다시 보여줄 수 없어요. 잃어버렸다면 초대를 취소하고 새로 만드세요.",
"copyLink": "링크 복사",
"copiedToast": "링크를 복사했어요",
@@ -7149,8 +7409,8 @@
"revokedToast": "초대를 취소했어요.",
"revokeError": "초대를 취소하지 못했어요."
},
- "userManagement": "사용자 관리",
- "userManagementDescription": "계정 관리: 역할, 비밀번호 재설정, 강제 로그아웃.",
+ "userManagement": "계정",
+ "userManagementDescription": "계정마다 역할, 비밀번호 재설정, 강제 로그아웃을 관리해요.",
"userName": "이름",
"userEmail": "이메일",
"userRole": "역할",
@@ -7177,14 +7437,13 @@
"tokenName": "이름",
"tokenPermissions": "권한",
"tokenPermissionAll": "전체",
- "tokenPermissionAllTooltip": "와일드카드 권한 — 이 토큰은 모든 API 경로를 호출할 수 있어요.",
+ "tokenPermissionAllTooltip": "와일드카드 권한: 이 토큰은 모든 API 경로를 호출할 수 있어요.",
"tokenStatus": "상태",
"tokenLastUsed": "마지막 사용",
"tokenCreated": "생성",
"tokenNeverUsed": "없음",
"tokensEmptyTitle": "아직 API 토큰이 없어요",
"tokensEmptyDescription": "네이티브 클라이언트가 로그인할 때나 설정 → 계정에서 토큰이 발급돼요.",
- "loginOverview": "로그인 개요",
"loginOverviewDescription": "모든 계정의 로그인·인증 이벤트예요. 필터링하고 CSV로 내보낼 수 있어요.",
"allAuthEvents": "전체 인증 이벤트",
"failedOnly": "실패만",
@@ -7217,6 +7476,7 @@
"finalDelete": "영구 삭제",
"deletedResult": "테이블 {tables}개에서 행 {rows}개를 삭제했어요. 사용자 계정과 로그인 자격 증명은 남겼어요.",
"authRegister": "회원가입",
+ "authClaim": "프로필 넘겨받음",
"authLogin": "로그인",
"authLoginPasskey": "패스키 로그인",
"authLoginPassword": "비밀번호 로그인",
@@ -7291,7 +7551,7 @@
},
"services": {
"title": "서비스",
- "subtitle": "알림 채널과 연동을 시스템 전체에서 켜거나 꺼요."
+ "subtitle": "알림 채널과 API를 서버 전체에서 켜거나 꺼요."
},
"integrations": {
"title": "연동",
@@ -7316,7 +7576,7 @@
"forceLogout": "강제 로그아웃",
"forceLogoutConfirmTitle": "{name} 강제 로그아웃할까요?",
"forceLogoutConfirmBody": "이 사용자의 활성 세션이 모두 해제돼요. 모든 기기에서 다시 로그인해야 해요.",
- "forceLogoutSuccess": "{name} — 세션 {count}개에서 로그아웃했어요",
+ "forceLogoutSuccess": "{name}: 세션 {count}개에서 로그아웃했어요",
"forceLogoutFailed": "강제 로그아웃하지 못했어요",
"cannotLogoutSelf": "자기 자신은 강제 로그아웃할 수 없어요",
"emptyTitle": "이 화면에 사용자가 없어요",
@@ -7355,7 +7615,7 @@
"app-logs": {
"title": "앱 로그",
"subtitle": "이 프로세스가 남긴 최근 구조화 wide-event를 실시간으로 봐요.",
- "processNote": "앱 프로세스 로그만 보여줘요 — 워커 프로세스 로그는 Loki 연동을 설정하면 그쪽으로 가요.",
+ "processNote": "앱 프로세스 로그만 보여주며, 워커 프로세스 로그는 Loki 연동을 설정하면 그쪽으로 가요.",
"filterTraceId": "추적 ID",
"filterAction": "동작 포함…",
"filterLevel": "레벨",
@@ -7377,7 +7637,8 @@
"refresh": "새로고침",
"eventDetails": "이벤트 상세",
"closeDetails": "상세 닫기",
- "processDetail": "버퍼는 휘발성이고 최대 500개까지만 담아요."
+ "processDetail": "버퍼는 휘발성이고 최대 500개까지만 담아요.",
+ "cardTitle": "최근 이벤트"
},
"backups": {
"title": "백업",
@@ -7389,7 +7650,7 @@
"loading": "백업을 불러오는 중…",
"loadError": "백업을 불러오지 못했어요.",
"emptyTitle": "아직 백업이 없어요",
- "emptyDescription": "백업은 매주 일요일 03:00에 자동으로 돌아요 — 지금 실행해서 첫 스냅샷을 만들 수도 있어요.",
+ "emptyDescription": "백업은 매주 일요일 03:00에 자동으로 돌아요. 지금 실행해서 첫 스냅샷을 만들 수도 있어요.",
"colUser": "사용자",
"colType": "유형",
"typeWeeklyAuto": "주간 (자동)",
@@ -7404,6 +7665,12 @@
"scheduleFailedDescription": "{when}: {reason}",
"scheduleFailedNoReason": "기록된 사유 없음",
"scheduleRemedy": "지금 백업을 실행하고 새 항목이 나타나는지 확인하세요. 다시 실패하면 워커 로그에 중단된 계정이 표시됩니다.",
+ "activity": {
+ "running": "{when}부터 실행 중입니다.",
+ "interruptedTitle": "실행이 도중에 멈추고 다시 돌아오지 않았습니다",
+ "interruptedAccount": "{username}: {when}에 시작, 끝나지 않음",
+ "interruptedRemedy": "이 사본을 쓰는 동안 앱이 다시 시작되었거나 메모리가 부족했을 가능성이 큽니다. 다음 실행은 이 계정들을 마지막에 처리하므로 다른 계정이 먼저 사본을 받습니다. 계속 반복되면 컨테이너에 메모리를 더 할당하세요."
+ },
"download": "다운로드",
"downloadAria": "{username}의 백업 다운로드",
"downloadFailed": "다운로드하지 못했어요",
@@ -7419,11 +7686,11 @@
"restoreTitle": "백업에서 복원",
"restoreDescription": "{username}의 현재 데이터 전체를 {when} 스냅샷으로 바꿔요. 기존 측정, 약, 복용 기록, 기분 기록, 알림 채널, 푸시 구독, 대기 중인 Telegram 삭제 항목이 지워져요.",
"restoreInstanceSettingsLabel": "인스턴스 설정도 함께 복원",
- "restoreInstanceSettingsHint": "가입, MFA 필수 여부, 기본 언어, 모듈 사용 여부, 알림과 AI 설정 — 스냅샷에서 가져와 이 설치본의 모든 사용자에게 적용돼요. 이 계정만 복원하려면 선택하지 마세요.",
+ "restoreInstanceSettingsHint": "가입, MFA 필수 여부, 기본 언어, 모듈 사용 여부, 알림과 AI 설정은 스냅샷에서 가져와 이 설치본의 모든 사용자에게 적용돼요. 이 계정만 복원하려면 선택하지 마세요.",
"restorePromptLabel": "계속하려면 RESTORE를 입력해 주세요",
"previewTitle": "이 백업에 담긴 내용",
"previewLoading": "내용을 읽는 중…",
- "previewUnavailable": "내용을 읽지 못했어요 — 복원은 그래도 할 수 있어요.",
+ "previewUnavailable": "내용을 읽지 못했어요. 복원은 그래도 할 수 있어요.",
"previewStillReading": "이 백업은 이전 버전에서 만들어져서 내용을 처음부터 끝까지 읽고 있어요. 기록이 많으면 시간이 좀 걸려요. 몇 분 뒤에 이 창을 다시 열면 내용이 보여요. 복원은 이것과 상관없이 할 수 있어요.",
"previewEmpty": "파일에 기록이 없어요.",
"previewOther": "기타 기록",
@@ -7443,7 +7710,7 @@
"restoreInProgress": "복원 중…",
"restoreSuccess": "복원을 마쳤어요",
"restoreFailed": "복원하지 못했어요",
- "restoreSkippedToast": "복원을 마쳤어요 — 되돌리지 못한 증상 연결: {links}",
+ "restoreSkippedToast": "복원을 마쳤어요. 되돌리지 못한 증상 연결: {links}",
"restoreSkippedTitle": "복원하지 못한 연결: {links}",
"restoreSkippedDescription": "파일이 이 설치본에 없는 기록을 가리키고 있어요. 나머지는 모두 복원했어요. 같은 목록이 감사 로그에도 남아 있어요.",
"restoreSkippedLinks": "연결 {count}개",
@@ -7466,7 +7733,7 @@
"restoreSkippedMedicationTarget": "약 목표 분석물",
"restoreSkippedScheduleRevisionLink": "일정 개정 연결",
"restoreSkippedAccountSetting": "계정 설정",
- "restoreMissingTitle": "복원을 중단했어요 — 파일이 불완전해요",
+ "restoreMissingTitle": "복원을 중단했어요. 파일이 불완전해요",
"restoreMissingDescription": "파일의 매니페스트에는 아래 항목이 들어 있다고 적혀 있는데 실제로는 없어요. 아무것도 바꾸지 않았어요. 복원은 해당 기록 전체를 갈아엎기 때문에, 파일에 없는 항목을 되돌리면 그 항목이 통째로 비워졌을 거예요.",
"restoreMissingRemedy": "재해 복구용 스냅샷을 쓰거나, 계정을 다시 내보내서 새 파일을 업로드해 주세요. 매니페스트가 처음부터 '빠졌다'고 표시한 항목은 다른 경우라 정상적으로 복원돼요.",
"restoreMissingDocuments": "문서",
@@ -7534,7 +7801,12 @@
"restoreFailureKeyMissing": "이 백업에는 암호화 키 {keys}가 필요하지만 이 서버는 그 키로 백업을 열 수 없습니다. 키를 ENCRYPTION_KEYS에 다시 추가하고 재시작한 뒤 복원을 다시 시작하세요.",
"restoreFailureForeignReference": "파일이 다른 계정에 속한 기록을 가리키고 있습니다. 아무것도 기록되지 않았습니다.",
"previewKeyMissing": "이 서버에서는 이 백업을 복원할 수 없습니다. 암호화 키 {keys}가 필요합니다. 먼저 키를 ENCRYPTION_KEYS에 다시 추가하고 재시작하세요.",
- "restoreSkippedSymptomEpisodeReference": "파일에 없는 질병과 연결된 증상 기록(연결 없이 유지됨)"
+ "restoreSkippedSymptomEpisodeReference": "파일에 없는 질병과 연결된 증상 기록(연결 없이 유지됨)",
+ "restoreSkippedCycleCiphertext": "이 서버에 없는 키로 암호화된 주기 메모 또는 이름(해당 날짜나 증상은 그 내용 없이 유지됨)",
+ "restoreSkippedMoodLabelCiphertext": "이 서버에 없는 키로 암호화된 기분 태그 또는 그룹 이름(이름 없이 유지됨)",
+ "restoreSkippedVisitCiphertext": "이 서버에 없는 키로 암호화된 진료 또는 의료진 텍스트(해당 내용 없이 유지됨)",
+ "restoreSkippedVaccinationCiphertext": "이 서버에 없는 키로 암호화된 예방접종 메모(접종 기록은 메모 없이 유지됨)",
+ "restoreSkippedEnvironmentLocationCiphertext": "이 서버에 없는 키로 암호화된 환경 기간 또는 날짜의 위치(암호화된 채 유지되며 해당 날짜는 새로 가져오지 않음)"
},
"danger-zone": {
"title": "위험 구역",
@@ -7551,7 +7823,6 @@
"encryption": {
"title": "암호화",
"subtitle": "저장 데이터의 암호화 적용 범위와 키 교체 상태예요.",
- "loading": "암호화 상태를 불러오는 중…",
"loadError": "암호화 상태를 불러오지 못했어요.",
"coverageTitle": "적용 범위",
"coverageDescription": "키 id별로 암호화된 행이 얼마나 있는지 보여줘요.",
@@ -7586,7 +7857,7 @@
"completed": "완료",
"failed": "실패"
},
- "coverageDetail": "키 id와 개수만 보여줘요 — 키 자체는 절대 표시하지 않아요.",
+ "coverageDetail": "키 id와 개수만 보여줘요. 키 자체는 절대 표시하지 않아요.",
"backupsTitle": "백업에 아직 필요한 키",
"backupsDescription": "아래 백업 중 하나라도 표시하는 동안 폐기한 키는 계속 필요합니다.",
"backupsStoredLine": "키 {key}: 저장된 복사본 {count}개, 가장 오래된 것은 {when}",
@@ -7596,12 +7867,45 @@
"backupsNone": "아직 필요한 키를 기록한 백업이 없습니다.",
"backupsStillNeed": "모든 행이 회전되었지만 백업에 아직 {keys}가 필요합니다",
"backupsUnrecordedBadge": "모든 행이 회전되었지만 이전 백업에는 아직 폐기한 키가 필요할 수 있습니다",
- "backupsDetail": "회전은 모든 행을 다시 암호화하지만, 백업은 내용이 기록된 키를 그대로 유지하며 복원에는 그 키가 필요합니다. 아래에 더 이상 표시되지 않을 때까지 폐기한 키를 ENCRYPTION_KEYS에 유지하세요."
+ "backupsDetail": "회전은 모든 행을 다시 암호화하지만, 백업은 내용이 기록된 키를 그대로 유지하며 복원에는 그 키가 필요합니다. 아래에 더 이상 표시되지 않을 때까지 폐기한 키를 ENCRYPTION_KEYS에 유지하세요.",
+ "columnsShow": "표 보기 (행 있음 {count}개)",
+ "columnsHide": "표 숨기기",
+ "columnsShowAll": "열 {count}개 모두 보기",
+ "columnsShowWithRows": "행이 있는 열만",
+ "columnsNoneWithRows": "아직 행이 있는 암호화 열이 없어요."
+ },
+ "maintenance": {
+ "title": "측정값 테이블 유지 관리",
+ "description": "정리가 끝난 뒤, 오래된 동기화 잔여 데이터가 차지하던 공간을 되돌려 줍니다.",
+ "body": "측정값 테이블에 vacuum을 실행한 다음 인덱스를 하나씩 다시 만듭니다. 실행 중에도 읽기와 저장은 계속 작동합니다. 한가한 시간을 고르고, 가장 큰 인덱스를 한 번 더 복사할 만큼의 디스크 여유 공간을 확보하세요.",
+ "tableSize": "인덱스 포함 테이블",
+ "indexSize": "인덱스",
+ "loading": "유지 관리 상태를 확인하는 중…",
+ "loadError": "유지 관리 상태를 읽을 수 없습니다.",
+ "unavailable": "연결된 백그라운드 작업자가 없어 여기에서는 유지 관리를 실행할 수 없습니다.",
+ "purgePending": "오래된 동기화 잔여 데이터 정리가 아직 진행 중입니다. 정리가 끝나면 유지 관리를 시작할 수 있습니다.",
+ "state": {
+ "idle": "아직 실행되지 않았습니다.",
+ "queued": "백그라운드 작업자를 기다리는 중입니다.",
+ "running": "{since}부터 실행 중입니다. 큰 인스턴스에서는 시간이 걸릴 수 있습니다.",
+ "completed": "{at}에 완료되었습니다.",
+ "refusedPurge": "정리가 아직 진행 중이어서 시작되지 않았습니다. 정리가 끝난 뒤 다시 시작하세요.",
+ "alreadyRunning": "다른 실행이 이미 작업 중이어서 시작되지 않았습니다.",
+ "failed": "{at}에 중단되었습니다. 앱 로그에 단계가 나와 있고, 런북에서 확인할 사항을 설명합니다."
+ },
+ "start": "유지 관리 시작",
+ "confirmTitle": "지금 유지 관리를 시작할까요?",
+ "confirmBody": "백그라운드에서 한 단계씩 실행되며 시간이 걸릴 수 있습니다. 오늘 아직 백업하지 않았다면 먼저 백업하세요.",
+ "confirm": "시작",
+ "cancel": "취소",
+ "started": "유지 관리를 시작했습니다.",
+ "alreadyQueued": "유지 관리가 이미 대기 중이거나 실행 중입니다.",
+ "startFailed": "유지 관리를 시작할 수 없습니다."
}
},
"carrier": "통신사",
"mfaRequired": "2단계 인증 필수",
- "mfaRequiredDescription": "2단계 인증이 없는 계정은 로그인 후 등록 화면으로 보내요. 웹 사용자에게 등록을 요구할 뿐, 아직 등록하지 않은 계정의 API나 네이티브(Bearer) 접근을 막지는 않아요.",
+ "mfaRequiredDescription": "2단계 인증이 없는 계정은 웹 로그인 후 등록 화면으로 보내고, API와 네이티브(Bearer) 접근은 그대로 열려 있어요.",
"mfaEnforcedOn": "MFA 켬",
"mfaEnforcedOff": "MFA 끔",
"mfaEnforcedOnHint": "이 사용자에게 2단계 인증이 필수예요. 누르면 해제돼요.",
@@ -7631,13 +7935,13 @@
"connectedSince": "연결 시작: {when}",
"connected": "공용 AI 연결을 등록했어요",
"disconnected": "공용 AI 연결을 해제했어요",
- "error": "문제가 생겼어요 — 다시 시도해 주세요"
+ "error": "문제가 생겼어요. 다시 시도해 주세요"
},
"failingJobs": {
"title": "백그라운드 작업",
"description": "재시도를 다 쓰고 포기한 작업이 있는 큐예요.",
"none": "최근 {hours}시간 동안 실패한 작업이 없어요.",
- "unavailable": "읽을 작업 큐가 없어요 — 이 배포본은 백그라운드 워커 없이 돌고 있어요.",
+ "unavailable": "읽을 작업 큐가 없어요. 이 배포본은 백그라운드 워커 없이 돌고 있어요.",
"countOne": "실패 {count}건",
"countFew": "실패 {count}건",
"countOther": "실패 {count}건",
@@ -7663,7 +7967,10 @@
"typeCodex": "ChatGPT 로그인 (개인)",
"typeLocal": "로컬 모델 (개인)",
"typeGateway": "OpenAI 호환 게이트웨이 (개인)",
- "spendToday": "오늘({date}): 토큰 {total}개, 그중 {operator}개는 운영자 키에서 사용되었습니다."
+ "spendToday": "오늘(각 계정의 현지 날짜 기준): 토큰 {total}개, 그중 {operator}개는 운영자 키에서 사용되었습니다(사용량이 있는 계정: {accounts}개).",
+ "statusPaused": "일시 중지됨",
+ "pausedHint": "반복된 오류로 사용자 {count}명에 대해 {at}까지 일시 중지되었습니다. 그 후 자동으로 다시 시도합니다.",
+ "modelNotServed": "엔드포인트가 더 이상 {model} 모델을 제공하지 않습니다. AI 설정에서 제공되는 모델을 선택하세요."
},
"cardError": {
"title": "페이지의 이 부분을 표시할 수 없습니다",
@@ -7720,7 +8027,9 @@
"restartNote": "변경 후 HealthLog를 다시 시작하세요.",
"lastResort": "최후의 수단: 키가 맞다고 확신하고 위 세 가지가 모두 해당하지 않을 때만 {setting}을 설정하고 다시 시작하세요. HealthLog는 그래도 서비스하며 결과를 로그에만 남깁니다. 이 설정은 안전 검사를 끕니다. 키가 틀리면 모든 암호화된 값이 실패하고 새 데이터가 그 키로 기록됩니다.",
"codes": "운영자용: API 요청은 오류 코드 {code}와 함께 503으로 응답하고, /api/health는 원인 {reason}을 알려 줍니다."
- }
+ },
+ "loginOverviewCardTitle": "로그인 이벤트",
+ "userColumn": "사용자"
},
"achievements": {
"title": "업적",
@@ -7736,7 +8045,9 @@
"noneUnlockedYet": "아직 달성한 업적이 없어요.",
"pointsValue": "{points}포인트",
"metricCount": "{count}",
- "metricDays": "{count}일",
+ "metricDaysOne": "{count}일",
+ "metricDaysFew": "{count}일",
+ "metricDaysOther": "{count}일",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -7759,14 +8070,6 @@
"title": "복용 전설",
"description": "복용 기록 300회를 남겨요."
},
- "overIntake1": {
- "title": "한 번 더",
- "description": "계획보다 한 번 이상 더 복용해요."
- },
- "skippedIntake1": {
- "title": "한 걸음 물러서기",
- "description": "계획된 복용을 한 번 이상 건너뛰어요."
- },
"passkeyCreated1": {
"title": "패스키 등록",
"description": "첫 패스키를 만들어요."
@@ -7792,19 +8095,19 @@
"description": "하루 동안 모든 약을 제때 복용해요."
},
"compliance801": {
- "title": "복약 순응도 80% · 1일",
+ "title": "1일간 복약 순응도 80%",
"description": "30일 복약 순응도 80% 이상을 1일 유지해요."
},
"bmiGreen1": {
- "title": "BMI 정상 · 1일",
+ "title": "1일간 BMI 정상",
"description": "BMI를 1일 동안 정상 범위로 유지해요."
},
"bpGreen1": {
- "title": "혈압 정상 · 1일",
+ "title": "1일간 혈압 정상",
"description": "혈압을 1일 동안 정상 범위로 유지해요."
},
"pulseGreen1": {
- "title": "맥박 정상 · 1일",
+ "title": "1일간 맥박 정상",
"description": "안정 시 맥박을 1일 동안 정상 범위로 유지해요."
},
"onTimePerfect7": {
@@ -7812,19 +8115,19 @@
"description": "7일 연속으로 모든 약을 제때 복용해요."
},
"compliance807": {
- "title": "복약 순응도 80% · 7일",
+ "title": "7일간 복약 순응도 80%",
"description": "30일 복약 순응도 80% 이상을 7일 연속 유지해요."
},
"bmiGreen7": {
- "title": "BMI 정상 · 7일",
+ "title": "7일간 BMI 정상",
"description": "BMI를 7일 연속 정상 범위로 유지해요."
},
"bpGreen7": {
- "title": "혈압 정상 · 7일",
+ "title": "7일간 혈압 정상",
"description": "혈압을 7일 연속 정상 범위로 유지해요."
},
"pulseGreen7": {
- "title": "맥박 정상 · 7일",
+ "title": "7일간 맥박 정상",
"description": "안정 시 맥박을 7일 연속 정상 범위로 유지해요."
},
"onTimePerfect30": {
@@ -7832,19 +8135,19 @@
"description": "30일 연속으로 모든 약을 제때 복용해요."
},
"compliance8030": {
- "title": "복약 순응도 80% · 30일",
+ "title": "30일간 복약 순응도 80%",
"description": "30일 복약 순응도 80% 이상을 30일 연속 유지해요."
},
"bmiGreen30": {
- "title": "BMI 정상 · 30일",
+ "title": "30일간 BMI 정상",
"description": "BMI를 30일 연속 정상 범위로 유지해요."
},
"bpGreen30": {
- "title": "혈압 정상 · 30일",
+ "title": "30일간 혈압 정상",
"description": "혈압을 30일 연속 정상 범위로 유지해요."
},
"pulseGreen30": {
- "title": "맥박 정상 · 30일",
+ "title": "30일간 맥박 정상",
"description": "안정 시 맥박을 30일 연속 정상 범위로 유지해요."
},
"onTimePerfect180": {
@@ -7852,19 +8155,19 @@
"description": "180일 연속으로 모든 약을 제때 복용해요."
},
"compliance80180": {
- "title": "복약 순응도 80% · 180일",
+ "title": "180일간 복약 순응도 80%",
"description": "30일 복약 순응도 80% 이상을 180일 연속 유지해요."
},
"bmiGreen180": {
- "title": "BMI 정상 · 180일",
+ "title": "180일간 BMI 정상",
"description": "BMI를 180일 연속 정상 범위로 유지해요."
},
"bpGreen180": {
- "title": "혈압 정상 · 180일",
+ "title": "180일간 혈압 정상",
"description": "혈압을 180일 연속 정상 범위로 유지해요."
},
"pulseGreen180": {
- "title": "맥박 정상 · 180일",
+ "title": "180일간 맥박 정상",
"description": "안정 시 맥박을 180일 연속 정상 범위로 유지해요."
},
"onTimePerfect360": {
@@ -7872,19 +8175,19 @@
"description": "360일 연속으로 모든 약을 제때 복용해요."
},
"compliance80360": {
- "title": "복약 순응도 80% · 360일",
+ "title": "360일간 복약 순응도 80%",
"description": "30일 복약 순응도 80% 이상을 360일 연속 유지해요."
},
"bmiGreen360": {
- "title": "BMI 정상 · 360일",
+ "title": "360일간 BMI 정상",
"description": "BMI를 360일 연속 정상 범위로 유지해요."
},
"bpGreen360": {
- "title": "혈압 정상 · 360일",
+ "title": "360일간 혈압 정상",
"description": "혈압을 360일 연속 정상 범위로 유지해요."
},
"pulseGreen360": {
- "title": "맥박 정상 · 360일",
+ "title": "360일간 맥박 정상",
"description": "안정 시 맥박을 360일 연속 정상 범위로 유지해요."
},
"moodFirst": {
@@ -7968,15 +8271,15 @@
"description": "앱 언어를 한 번 이상 바꿨어요."
},
"missFree7": {
- "title": "복용 누락 없음 · 7일",
+ "title": "7일간 복용 누락 없음",
"description": "7일 연속으로 복용을 한 번도 놓치지 않아요."
},
"missFree30": {
- "title": "복용 누락 없음 · 30일",
+ "title": "30일간 복용 누락 없음",
"description": "30일 연속으로 복용을 한 번도 놓치지 않아요."
},
"missFree90": {
- "title": "복용 누락 없음 · 90일",
+ "title": "90일간 복용 누락 없음",
"description": "90일 연속으로 복용을 한 번도 놓치지 않아요."
},
"measurementWeeks4": {
@@ -7985,10 +8288,10 @@
},
"selfContextComplete": {
"title": "자기 보고 완성",
- "description": "자기 보고 항목 네 가지를 모두 채웠어요 — 코치가 상황을 알아요."
+ "description": "자기 보고 항목 네 가지를 모두 채웠어요. 코치가 상황을 알아요."
},
"sleepLog7": {
- "title": "수면 기록 · 7일",
+ "title": "7일간 수면 기록",
"description": "7일 연속으로 수면 데이터를 기록해요."
}
},
@@ -8016,7 +8319,7 @@
"dashboardCard": {
"title": "최근 달성",
"viewAll": "전체 보기",
- "empty": "아직 업적이 없어요 — 기록을 이어가 첫 업적을 달성해 보세요."
+ "empty": "아직 업적이 없어요. 기록을 이어가 첫 업적을 달성해 보세요."
}
},
"notifications": {
@@ -8078,9 +8381,6 @@
"enabledToast": "능동 코치 알림을 켰어요.",
"disabledToast": "능동 코치 알림을 껐어요.",
"saveError": "저장하지 못했어요. 나중에 다시 시도해 주세요.",
- "suggestionsLabel": "예시 질문",
- "suggestionsDesc": "코치 화면에 미리 준비된 예시 질문을 가끔 보여줘요.",
- "suggestionsAria": "코치 예시 질문 켜고 끄기",
"aiComposedLabel": "코치가 알림 문구 작성",
"aiComposedDesc": "제공자를 쓸 수 있으면 고정 템플릿 대신 모델이 알림 문구를 써요. 기본값은 꺼짐이에요.",
"aiComposedAria": "AI가 쓴 능동 알림 켜고 끄기"
@@ -8151,7 +8451,46 @@
"loginRequired": "알림을 관리하려면 로그인해 주세요.",
"someChannelsGloballyDisabled": "설정에서 꺼둔 채널이 있어서 그 채널로는 알림을 받지 못해요.",
"reminderSettingsLink": "알림 설정",
- "channelsLink": "채널"
+ "channelsLink": "채널",
+ "handover": {
+ "claimedTitle": "{name} 님에게 이제 자신의 계정이 있습니다",
+ "claimedEnd": "{name} 님이 자신의 건강 기록을 넘겨받았습니다. 내 접근 권한은 종료되었습니다.",
+ "claimedRead": "{name} 님이 자신의 건강 기록을 넘겨받았습니다. 계속 볼 수는 있지만 더 이상 변경할 수 없습니다.",
+ "claimedManage": "{name} 님이 자신의 건강 기록을 넘겨받았습니다. 다르게 결정할 때까지 계속 관리할 수 있습니다.",
+ "changedTitle": "{name} 님이 내 접근 권한을 바꿨습니다",
+ "changedEnd": "{name} 님의 건강 기록에 대한 내 접근 권한이 종료되었습니다.",
+ "changedRead": "이제 {name} 님의 건강 기록을 볼 수는 있지만 변경할 수는 없습니다.",
+ "changedManage": "{name} 님의 건강 기록을 관리할 수 있습니다.",
+ "unnamed": {
+ "claimedTitle": "돌보던 분이 이제 자신의 계정을 갖게 되었어요",
+ "claimedEnd": "돌보던 분이 자신의 건강 기록을 넘겨받았어요. 내 접근 권한은 종료되었어요.",
+ "claimedRead": "돌보던 분이 자신의 건강 기록을 넘겨받았어요. 계속 볼 수는 있지만 더 이상 변경할 수 없어요.",
+ "claimedManage": "돌보던 분이 자신의 건강 기록을 넘겨받았어요. 그분이 다르게 결정할 때까지 계속 관리할 수 있어요.",
+ "changedTitle": "건강 기록에 대한 접근 권한이 변경되었어요",
+ "changedEnd": "돌보던 분의 건강 기록에 대한 접근 권한이 종료되었어요.",
+ "changedRead": "이제 돌보던 분의 건강 기록을 볼 수 있지만 변경할 수는 없어요.",
+ "changedManage": "돌보던 분의 건강 기록을 관리할 수 있어요."
+ }
+ },
+ "links": {
+ "open": "HealthLog에서 열기"
+ },
+ "quickActions": {
+ "moodGood": "좋음",
+ "moodOkay": "보통",
+ "moodBad": "나쁨"
+ },
+ "content": {
+ "title": "알림의 건강 수치",
+ "description": "알림에 아침 점수 같은 수치를 표시할지 정합니다.",
+ "detail": "끄면 알림은 무엇에 관한 것인지만 알려 주므로 잠금 화면에 건강 수치가 나타나지 않아요. 켜면 수치도 함께 보여 줘요. 예를 들어 브리핑에 아침 점수가 표시됩니다. 어느 쪽이든 알림을 누르면 앱의 알맞은 곳이 열려요.",
+ "toggleAria": "알림에 건강 수치 표시",
+ "statusDetailed": "수치 표시",
+ "statusGeneric": "수치 없이",
+ "detailedToast": "이제 알림에 건강 수치가 포함됩니다.",
+ "genericToast": "이제 알림에 건강 수치가 포함되지 않습니다.",
+ "saveError": "저장하지 못했어요. 나중에 다시 시도해 주세요."
+ }
},
"i18n": {
"maintainershipBanner": {
@@ -8182,7 +8521,7 @@
}
},
"edit": {
- "title": "목표 범위 편집 — {metric}",
+ "title": "목표 범위 편집: {metric}",
"description": "차트·인사이트·진료용 리포트에 쓰이는 목표 범위를 조정해요. 값 단위는 {unit}.",
"derivedHint": "이 카드는 다른 지표에서 계산돼요. 기준이 되는 목표(예: 혈압이나 체중)를 편집해 주세요.",
"minLabel": "최소",
@@ -8206,15 +8545,19 @@
"currentValue": "현재: {value} {unit}",
"targetRangeValue": "목표: {min}–{max} {unit}",
"sourceLabel": "출처: {source}",
+ "sourceNames": {
+ "moodEntries": "기분 기록",
+ "sevenDays": "7일"
+ },
"inTarget": "목표 범위 안",
"aboveTarget": "목표보다 {delta} {unit} 높음",
"belowTarget": "목표보다 {delta} {unit} 낮음",
"diastolic": "이완기",
- "glucoseFasting": "혈당 — 공복",
- "glucosePostprandial": "혈당 — 식후",
- "glucoseRandom": "혈당 — 무작위",
- "glucoseBedtime": "혈당 — 취침 전",
- "glucoseUnspecified": "혈당 — 미지정",
+ "glucoseFasting": "혈당 (공복)",
+ "glucosePostprandial": "혈당 (식후)",
+ "glucoseRandom": "혈당 (무작위)",
+ "glucoseBedtime": "혈당 (취침 전)",
+ "glucoseUnspecified": "혈당 (미지정)",
"status": {
"underweight": "저체중",
"normal": "정상",
@@ -8277,11 +8620,11 @@
"errorUnknownAction": "알 수 없는 동작이에요.",
"snoozeOneHour": "1시간",
"snoozeThreeHours": "3시간",
- "snoozedFor": "{name} — {duration} 동안 미뤘어요.",
- "skipped": "{name} — 건너뛰었어요.",
- "alreadyRecorded": "{name} — 이미 기록돼 있어요.",
- "recordedAsTaken": "{name} — 복용으로 기록했어요.",
- "confirmed": "{name} — 확인했어요.",
+ "snoozedFor": "{name}: {duration} 동안 미뤘어요.",
+ "skipped": "{name}: 건너뛰었어요.",
+ "alreadyRecorded": "{name}: 이미 기록돼 있어요.",
+ "recordedAsTaken": "{name}: 복용으로 기록했어요.",
+ "confirmed": "{name}: 확인했어요.",
"genericConfirmed": "확인했어요.",
"cancelled": "취소했어요.",
"moodLogged": "기록함: {score}/5",
@@ -8300,7 +8643,7 @@
"askMedicationName": "약을 지정해 주세요. 예: taken 라미프릴.",
"medicationNotFoundExact": "약을 찾지 못했어요. HealthLog에 있는 이름 그대로 보내 주세요.",
"helpHeader": "사용할 수 있는 명령:",
- "helpBody": "/help — 이 도움말 보기\n/add — 복용 기록하기\n/start — 봇 시작하기\n\n텍스트 명령:\ntaken <이름> — 복용 확인\n\n알림 속 버튼:\n• 복용 — 복용 확인\n• 🕐 1h / 🕐 3h — 알림 미루기\n• ⏭ 건너뛰기 — 복용 건너뛰기\n• ✓ 확인 — 놓친 복용 확인",
+ "helpBody": "/help: 이 도움말 보기\n/add: 복용 기록하기\n/start: 봇 시작하기\n\n텍스트 명령:\ntaken <이름>: 복용 확인\n\n알림 속 버튼:\n• 복용: 복용 확인\n• 🕐 1h / 🕐 3h: 알림 미루기\n• ⏭ 건너뛰기: 복용 건너뛰기\n• ✓ 확인: 놓친 복용 확인",
"measureValuePrompt": "값을 답장으로 보내면 기록해요(선택).",
"buttonMeasureValue": "값…",
"measureValueSaved": "측정값을 저장했어요.",
@@ -8325,9 +8668,9 @@
},
"lowStockReminders": {
"title": "재고 부족: {medName}",
- "bodyToday": "{medName}: {units}개 남았어요 — 하루치도 안 돼요.",
- "bodyReorder": "{medName}: {units}개 남았어요. {runsOutOn}쯤 소진돼요 — {reorderBy}까지 재주문하세요.",
- "bodyLastDose": "{medName}: {units}개 남았어요 — 마지막 복용분이에요. {runsOutOn}쯤 소진돼요.",
+ "bodyToday": "{medName}: {units}개 남았어요. 하루치도 안 돼요.",
+ "bodyReorder": "{medName}: {units}개 남았어요. {runsOutOn}쯤 소진돼요. {reorderBy}까지 재주문하세요.",
+ "bodyLastDose": "{medName}: {units}개 남았어요. 마지막 복용분이에요. {runsOutOn}쯤 소진돼요.",
"bodyExpired": "{medName}: 쓸 수 있는 재고가 없어요. {units}개가 유효기간 만료로 표시돼 있어요.",
"bodyAsNeeded": "{medName}: {units}개만 남았어요. 다음에 필요하기 전에 재주문하세요."
},
@@ -8354,11 +8697,11 @@
"questionConditions": "코치가 알아야 할 만성 질환이 있나요?",
"questionFocus": "코치가 특별히 신경 써야 할 부분이 있나요?",
"questionAllergies": "알레르기나 불내증이 있나요?",
- "questionAboutMe": "코치에게 자신에 대해 조금 알려줄래요 — 생활 루틴, 목표, 상황 같은 것?"
+ "questionAboutMe": "코치에게 생활 루틴, 목표, 상황 같은 자신의 이야기를 조금 알려줄래요?"
},
"clinicianView": {
"title": "공유된 건강 기록",
- "period": "보고 기간: {start} – {end}",
+ "period": "보고 기간: {start}–{end}",
"expires": "이 링크는 {date}에 만료돼요.",
"provenance": "본인이 직접 기록한 HealthLog 데이터에서 내보낸 읽기 전용 요약이에요. 값은 본인이 기록한 것이고 기기에서 동기화된 측정값이 섞여 있을 수 있어요. 임상 검사가 아니라 환자가 보고한 자료로 봐 주세요.",
"labs": "검사 수치",
@@ -8382,7 +8725,7 @@
"strain": "부하 점수",
"score": "웰니스 점수"
},
- "footer": "HealthLog에서 공유했어요 — 셀프 호스팅 개인 건강 기록 앱.",
+ "footer": "HealthLog(셀프 호스팅 개인 건강 기록 앱)에서 공유했어요.",
"unlock": {
"title": "암호 입력",
"description": "이 공유 건강 기록은 보호돼 있어요. 전달받은 암호를 입력해 주세요.",
@@ -8417,7 +8760,11 @@
},
"course": "복용 기간 {n}",
"courseRange": "{start}부터 {end}까지",
- "courseSince": "{start}부터"
+ "courseSince": "{start}부터",
+ "unavailable": {
+ "title": "링크가 만료되었거나 유효하지 않아요",
+ "description": "이 공유 링크는 만료되었거나 취소되었거나 불완전해요. 공유한 분께 새 링크를 요청해 주세요."
+ }
},
"cycle": {
"symptomCategory": {
@@ -8470,7 +8817,7 @@
},
"crosstab": {
"description": "내 데이터를 기준으로, 각 측정값이 황체기와 난포기에서 어떻게 다른지 비교했어요.",
- "pairLabel": "{metric} · 황체기와 난포기 비교",
+ "pairLabel": "{metric}: 황체기와 난포기 비교",
"detail": "황체기 {lutealAvg}{unit} ({lutealDays}일), 난포기 {follicularAvg}{unit} ({follicularDays}일).",
"metricRestingHeartRate": "안정 시 심박수",
"metricHrv": "심박변이도",
@@ -8593,8 +8940,8 @@
"ariaUnknown": "아직 진행 중인 주기가 없어요",
"caption": "주기 중",
"firstPeriodCta": "첫 생리 기록하기",
- "periodOverdue": "생리가 평소보다 늦어지고 있어요 — 다음 생리를 기록할 때까지 날짜 세기를 멈춰요.",
- "periodOverdueDays": "생리가 평소보다 {days}일 늦어지고 있어요 — 다음 생리를 기록할 때까지 날짜 세기를 멈춰요.",
+ "periodOverdue": "생리가 평소보다 늦어지고 있어요. 다음 생리를 기록할 때까지 날짜 세기를 멈춰요.",
+ "periodOverdueDays": "생리가 평소보다 {days}일 늦어지고 있어요. 다음 생리를 기록할 때까지 날짜 세기를 멈춰요.",
"logPeriodCta": "생리 기록"
},
"calendar": {
@@ -8606,7 +8953,7 @@
"prevMonth": "이전 달",
"nextMonth": "다음 달",
"loading": "캘린더를 불러오는 중…",
- "learning": "주기를 배우는 중이에요 — 전체 주기를 세 번 이상 기록하면 가임기, 배란, 단계가 채워져요. 그때까지 캘린더에는 기록한 것만 보여요.",
+ "learning": "주기를 배우는 중이에요. 전체 주기를 세 번 이상 기록하면 가임기, 배란, 단계가 채워져요. 그때까지 캘린더에는 기록한 것만 보여요.",
"legendSymptom": "증상",
"legendOvulationConfirmed": "확인된 배란",
"flowSpotting": "점상 출혈",
@@ -8619,7 +8966,7 @@
"predictions": {
"title": "다음에 올 일",
"nextPeriod": "다음 생리",
- "nextPeriodWindow": "{low} – {high}",
+ "nextPeriodWindow": "{low}–{high}",
"fertileWindow": "가임기",
"ovulation": "예상 배란일",
"confidenceHigh": "높음",
@@ -8628,7 +8975,7 @@
"stillLearning": "아직 배우는 중",
"stillLearningBody": "전체 주기를 세 번 이상 기록하면 예측이 더 정확해져요. 그때까지는 대략적인 추정이에요.",
"rawMode": "예측 꺼짐",
- "rawModeBody": "예측을 꺼 두셨어요 — 예보 없이 기록한 데이터만 보여요.",
+ "rawModeBody": "예측을 꺼 두셨어요. 예보 없이 기록한 데이터만 보여요.",
"none": "아직 예측이 없어요. 생리를 기록하고 시작해 보세요.",
"ovulationConfirmed": "배란 (확인됨)"
},
@@ -8661,7 +9008,7 @@
"symptoms": "증상",
"temperature": "기초 체온",
"temperatureDisturbed": "측정값을 방해받음으로 표시",
- "temperatureDisturbedHint": "발열, 아픈 날, 음주, 늦은 측정 — 체온 평가에서 제외돼요.",
+ "temperatureDisturbedHint": "발열, 아픈 날, 음주, 늦은 측정은 체온 평가에서 제외돼요.",
"ovulationTest": "배란 테스트",
"mucus": "자궁경부 점액",
"cervix": "자궁경부",
@@ -8690,7 +9037,7 @@
"temperatureSection": "체온 및 배란",
"intimacySection": "성관계 및 피임",
"testsSection": "검사",
- "phaseDayHeader": "{day}일째 · {phase}",
+ "phaseDayHeader": "{day}일째, {phase}",
"bbtHint": "아침에 일어나기 전, 가장 먼저 재 주세요.",
"titleForDate": "{date} 기록",
"descriptionToday": "오늘. 생리량, 증상, 메모예요.",
@@ -8757,7 +9104,7 @@
"fieldInfo": {
"bbt": "아침에 일어나기 전 가장 먼저 재는 안정 시 체온이에요. 배란 후에는 약 0.2~0.3°C 올라가요.",
"bbtLabel": "기초 체온이 뭔가요?",
- "mucus": "질 분비물은 주기에 따라 달라져요 — 건조하거나 끈적한 상태에서 크림 같은 상태로, 가장 가임력이 높은 날 무렵에는 물 같고 달걀흰자 같은 상태로 바뀌어요.",
+ "mucus": "질 분비물은 주기에 따라 달라져요. 건조하거나 끈적한 상태에서 크림 같은 상태로, 가장 가임력이 높은 날 무렵에는 물 같고 달걀흰자 같은 상태로 바뀌어요.",
"mucusLabel": "자궁경부 점액이 뭔가요?",
"ovulationTest": "LH 급상승을 잡아내는 가정용 소변 검사예요. LH는 배란 약 24~36h 전에 치솟는 호르몬이고, 양성이면 가장 가임력이 높은 시기라는 뜻이에요.",
"ovulationTestLabel": "배란 테스트가 뭔가요?",
@@ -8765,7 +9112,7 @@
"spottingLabel": "생리 사이 출혈이 뭔가요?",
"pregnancyTest": "가정용 임신(hCG) 테스트 결과예요.",
"pregnancyTestLabel": "임신 테스트가 뭔가요?",
- "progesteroneTest": "배란이 일어났는지 확인하는 검사예요 — 배란 후에 프로게스테론이 올라가요.",
+ "progesteroneTest": "배란이 일어났는지 확인하는 검사예요. 배란 후에 프로게스테론이 올라가요.",
"progesteroneTestLabel": "프로게스테론 검사가 뭔가요?",
"intercourse": "이 날 성관계가 있었는지 기록해요.",
"intercourseLabel": "성관계 항목은 무엇을 기록하나요?",
@@ -8818,7 +9165,7 @@
"postpone": {
"menuItem": "미루기…",
"title": "예정일 미루기",
- "description": "이번 예정일만 옮겨요 — 반복 주기는 그대로예요.",
+ "description": "이번 예정일만 옮겨요. 반복 주기는 그대로예요.",
"chip": "+{days}일",
"dateLabel": "새 날짜",
"confirm": "미루기"
@@ -8867,10 +9214,10 @@
"VISCERAL_FAT": "내장지방",
"BODY_MASS_INDEX": "체질량지수(BMI)",
"WAIST_CIRCUMFERENCE": "허리둘레",
- "PHQ9_SCORE": "PHQ-9 · 기분",
- "GAD7_SCORE": "GAD-7 · 걱정",
- "WHO5_SCORE": "WHO-5 · 웰빙",
- "SCI_SCORE": "SCI · 수면"
+ "PHQ9_SCORE": "PHQ-9 (기분)",
+ "GAD7_SCORE": "GAD-7 (걱정)",
+ "WHO5_SCORE": "WHO-5 (웰빙)",
+ "SCI_SCORE": "SCI (수면)"
},
"typeGroups": {
"vitals": "활력징후",
@@ -9010,7 +9357,7 @@
"sourceRangePlaceholder": "예: 3.5 - 5.0 또는 < 5",
"sourceRangeAdd": "+ 결과지의 범위 추가",
"sourceRangeHint": "선택 사항이에요. 이 측정값에만 적용되고, 저장된 범위는 그대로예요.",
- "sourceRangeUnparsed": "적은 그대로 뒀어요 — 수치 범위를 읽지 못했어요."
+ "sourceRangeUnparsed": "적은 그대로 뒀어요. 수치 범위를 읽지 못했어요."
},
"backToLabs": "검사 결과로 돌아가기",
"hasNote": "메모",
@@ -9028,7 +9375,7 @@
"emptyDescription": "값을 추가하면 이 바이오마커의 추이를 볼 수 있어요.",
"title": "바이오마커",
"genericDescription": "기록 중인 검사 바이오마커예요. 측정값을 추가하면 참고 범위와 비교한 추이를 볼 수 있어요.",
- "valuesTitle": "{marker} — 전체 측정값",
+ "valuesTitle": "{marker}: 전체 측정값",
"valuesDescription": "이 바이오마커의 모든 측정값이에요. 항목을 바로 편집하거나 삭제할 수 있어요.",
"backToMarker": "{marker} 화면으로 돌아가기",
"latestResult": "최근 결과"
@@ -9067,7 +9414,7 @@
"form": {
"seedLabel": "자주 쓰는 마커에서 시작",
"seedPlaceholder": "마커 선택 (선택 사항)",
- "seedHint": "하나를 고르면 아래 항목이 채워져요 — 범위는 검사실에 맞게 조정하세요.",
+ "seedHint": "하나를 고르면 아래 항목이 채워져요. 범위는 검사실에 맞게 조정하세요.",
"name": "이름",
"namePlaceholder": "예: LDL 콜레스테롤",
"unit": "단위",
@@ -9081,7 +9428,7 @@
"upperPlaceholder": "상한값",
"context": "설명",
"contextPlaceholder": "이 마커에 대한 메모 (선택)",
- "rangeHint": "하한과 상한 중 하나는 비워도 돼요 — 한쪽만 있는 범위가 돼요 (예: LDL < 116).",
+ "rangeHint": "하한과 상한 중 하나는 비워도 돼요. 한쪽만 있는 범위가 돼요 (예: LDL < 116).",
"requiredError": "이름과 단위는 필수예요.",
"rangeOrderError": "하한은 상한보다 클 수 없어요.",
"save": "바이오마커 저장",
@@ -9199,12 +9546,12 @@
"dateLabel": "날짜",
"refLowLabel": "참고 하한",
"refHighLabel": "참고 상한",
- "newBiomarker": "새 바이오마커 — 새로 만들어요",
+ "newBiomarker": "새 바이오마커: 새로 만들어요",
"linksExisting": "기존 {name} 항목에 연결돼요",
"duplicateWarning": "같은 측정값이 이미 있어요",
"lowConfidence": "이 값은 다시 확인해 주세요",
"lowConfidenceBadge": "확신 낮음",
- "valueUnreadable": "값을 읽지 못했어요 — 직접 입력해 주세요",
+ "valueUnreadable": "값을 읽지 못했어요. 직접 입력해 주세요",
"saveSelected": "선택한 {count}개 저장",
"discardAll": "모두 버리기",
"savedToast": "검사 결과 {count}개를 저장했어요",
@@ -9320,6 +9667,10 @@
"vaccinations": {
"label": "예방접종",
"description": "예방접종 기록을 구조화해서: 용량, 차수, 다음 추가 접종."
+ },
+ "timeline": {
+ "label": "타임라인",
+ "description": "몇 년을 한눈에: 수치 옆에 질환, 약, 진료가 놓이고, 내 삶의 사건도 함께 보여요."
}
},
"moduleOff": {
@@ -9351,7 +9702,7 @@
},
"form": {
"date": "날짜",
- "catalogLabel": "백신 (목록)",
+ "catalogLabel": "백신",
"identityHint": "목록에서 고르거나 아래에 직접 입력해 주세요. 둘 중 하나면 충분해요.",
"catalogNone": "백신 선택",
"catalogSearch": "백신 검색",
@@ -9472,10 +9823,31 @@
"overdue": "{date}부터 추가접종 기한 지남",
"dueSoon": "{date} 추가접종 예정",
"current": "다음 추가접종 {date}"
+ },
+ "custom": {
+ "title": "직접 추가한 백신",
+ "add": "백신 직접 추가",
+ "name": "이름",
+ "components": "예방 대상",
+ "seriesDoses": "기초 접종 횟수",
+ "boosterMonths": "추가 접종 주기(개월)",
+ "saved": "백신을 저장했습니다",
+ "deleted": "백신을 삭제했습니다",
+ "deleteConfirm": "이 백신을 삭제할까요? 이미 기록된 접종은 이름이 그대로 남습니다.",
+ "saveFailed": "백신을 저장하지 못했습니다.",
+ "nameTaken": "같은 이름의 백신이 이미 있습니다.",
+ "editTitle": "직접 추가한 백신 수정",
+ "description": "목록에 없는 백신도 목록의 백신과 똑같이 계산돼요.",
+ "componentsHint": "예방하는 질병을 모두 골라 주세요.",
+ "boosterHint": "추가 접종이 없으면 비워 두세요.",
+ "protectsAgainst": "{list} 예방",
+ "deleteTitle": "직접 추가한 백신을 삭제할까요?",
+ "sectionDescription": "목록에 없어서 직접 추가한 백신이에요.",
+ "catalogGroup": "목록"
}
},
"illness": {
- "title": "아픈 날 일지",
+ "title": "아픈 날",
"subtitle": "아픈 날과 컨디션 에피소드를 돌아보는 기록이에요.",
"settings": {
"viewDescription": "에피소드를 카드로 볼지 간단한 목록으로 볼지 선택하세요.",
@@ -9562,7 +9934,7 @@
},
"correlation": {
"title": "어떻게 진행됐나",
- "learning": "아직 학습 중이에요 — 며칠 더 기록하면 패턴이 보여요.",
+ "learning": "아직 학습 중이에요. 며칠 더 기록하면 패턴이 보여요.",
"preOnsetTitle": "어떤 신호가 먼저 왔나",
"preOnsetBody": "시작일로 표시하기 전에 기준값에서 벗어났던 신호예요.",
"nadirTitle": "무엇이 달라졌나",
@@ -9589,7 +9961,7 @@
"countOne": "이 기간에 에피소드를 1개 기록했어요.",
"countOther": "이 기간에 에피소드를 {count}개 기록했어요.",
"typicalGap": "평균 회복 시차: 몸이 나아지고 수치가 안정되기까지 {days}일.",
- "typicalGapWithDriver": "평균 회복 시차: {days}일 — {vital} 안정보다 몸이 먼저 나아지는 편이에요.",
+ "typicalGapWithDriver": "평균 회복 시차: {days}일, {vital} 안정보다 몸이 먼저 나아지는 편이에요.",
"typicalGapLearning": "종료된 에피소드가 몇 개 더 쌓이면 평균 회복 시차가 나와요.",
"none": "이 기간에 기록한 에피소드가 없어요.",
"analyze": "회복 시차 분석"
@@ -9650,8 +10022,8 @@
"stop": "충분히 측정하고 있어요",
"accepted": "알림을 설정했어요",
"dismissed": "알겠어요, 다시 제안하지 않을게요",
- "stopped": "알겠어요 — 알림 제안은 그만할게요",
- "failed": "설정하지 못했어요 — 다시 시도해 주세요",
+ "stopped": "알겠어요. 알림 제안은 그만할게요",
+ "failed": "설정하지 못했어요. 다시 시도해 주세요",
"cadence": {
"weightDaily": "매일 비슷한 시간에 체중을 재세요",
"bp722": "일주일 동안 하루 두 번 혈압을 재세요",
@@ -9663,7 +10035,7 @@
"dismiss": "나중에",
"applied": "추가했어요",
"dismissed": "닫았어요",
- "failed": "추가하지 못했어요 — 다시 시도해 주세요",
+ "failed": "추가하지 못했어요. 다시 시도해 주세요",
"checkup": {
"title": "건강검진 알림을 추가할까요?"
},
@@ -9691,9 +10063,9 @@
"reviewPrefix": "점검: {when}",
"accept": "계획 수락",
"decline": "거절",
- "accepted": "계획을 수락했어요 — 코치가 기억할게요.",
+ "accepted": "계획을 수락했어요. 코치가 기억할게요.",
"declined": "제안을 거절했어요.",
- "failed": "잘 안 됐어요 — 다시 시도해 주세요.",
+ "failed": "잘 안 됐어요. 다시 시도해 주세요.",
"markMet": "목표 달성",
"abandon": "계획 종료",
"remove": "삭제",
@@ -9714,8 +10086,8 @@
}
},
"refusal": {
- "outOfScope": "저는 기록에 있는 건강 지표만 도와드릴 수 있어요 — 측정값, 약, 기분, 추이 같은 것들이요. 그 범위 밖의 질문에는 답할 수 없어요.",
- "promptInjection": "제 지침을 무시하게 만들려는 표현이 보였어요. 저는 HealthLog 코치이고 본인의 건강 데이터만 정리해 드려요 — 그 범위에 맞게 다시 질문해 주세요.",
+ "outOfScope": "저는 기록에 있는 건강 지표만 도와드릴 수 있어요. 측정값, 약, 기분, 추이 같은 것들이요. 그 범위 밖의 질문에는 답할 수 없어요.",
+ "promptInjection": "제 지침을 무시하게 만들려는 표현이 보였어요. 저는 HealthLog 코치이고 본인의 건강 데이터만 정리해 드려요. 그 범위에 맞게 다시 질문해 주세요.",
"conversationPoisoned": "이 대화의 앞선 메시지에 제 지침을 무시하게 만드는 표현이 있어요. 새 대화를 시작해 주세요."
},
"noRecordedData": "물어보신 내용에 대해 기록된 측정값이 없어서 수치를 알려드릴 수 없어요. 기록을 남기신 뒤에 다시 물어보시면 본인 수치로 답해 드릴게요.",
@@ -9726,9 +10098,6 @@
"showResult": "이전 표 여는 중",
"reuse": "앞서 만든 표 사용 중",
"headerActive": "{label}…",
- "headerDoneOne": "출처 1개 확인함",
- "headerDoneFew": "출처 {count}개 확인함",
- "headerDoneOther": "출처 {count}개 확인함",
"listLabel": "코치가 확인한 내용",
"toggleShow": "코치가 확인한 내용 보기",
"toggleHide": "코치가 확인한 내용 숨기기",
@@ -9754,7 +10123,9 @@
"illness": "질병과 회복",
"cycle": "생리 주기",
"correlations": "지표 간 연관성",
- "snapshot": "기록 요약"
+ "snapshot": "기록 요약",
+ "environment": "날씨와 대기",
+ "day": "하루"
},
"window": {
"last7days": "최근 7일",
@@ -9878,7 +10249,8 @@
"relatedMetric": "함께 보기: {metric}",
"continue": "계속 찾아보기",
"settingLabel": "후속 질문 추천",
- "settingHint": "코치의 가장 최근 답변 아래에 후속 질문을 최대 세 개까지 보여줘요."
+ "settingHint": "코치의 가장 최근 답변 아래에 후속 질문을 최대 세 개까지 보여줘요.",
+ "changeAssumption": "대신: {value}"
},
"clarify": {
"cardLabel": "코치가 질문이 있어요",
@@ -9891,6 +10263,12 @@
"last90days": "최근 90일",
"lastYear": "최근 12개월",
"allTime": "전체 기간"
+ },
+ "comparison": {
+ "baseline90d": "평소 90일과 비교하기"
+ },
+ "anchor": {
+ "illness": "{date} 질환 이후부터"
}
},
"reuse": {
@@ -9936,10 +10314,10 @@
"saved": "저장됨",
"error": "문제가 생겼어요. 다시 시도해 주세요.",
"instrument": {
- "phq9": "PHQ-9 · 기분",
- "gad7": "GAD-7 · 걱정",
- "who5": "WHO-5 · 웰빙",
- "sci": "SCI · 수면"
+ "phq9": "PHQ-9 (기분)",
+ "gad7": "GAD-7 (걱정)",
+ "who5": "WHO-5 (웰빙)",
+ "sci": "SCI (수면)"
},
"instrumentSub": {
"phq9": "우울 선별, 9문항",
@@ -9960,7 +10338,7 @@
"2": "많이 어려웠다",
"3": "매우 많이 어려웠다"
},
- "functionalOptionalHint": "선택 사항이에요 — 이 문항에 답하지 않아도 제출할 수 있어요.",
+ "functionalOptionalHint": "선택 사항이에요. 이 문항에 답하지 않아도 제출할 수 있어요.",
"result": {
"title": "내 결과",
"totalLabel": "총점",
@@ -10150,7 +10528,7 @@
},
"openDetail": "자세히 보기",
"historyLoadError": "검사 기록을 불러오지 못했어요",
- "historyLoadErrorHint": "결과가 없다는 뜻은 아니에요 — 목록을 가져오지 못했을 뿐이에요. 다시 시도해 주세요.",
+ "historyLoadErrorHint": "결과가 없다는 뜻은 아니에요. 목록을 가져오지 못했을 뿐이에요. 다시 시도해 주세요.",
"privacyNote": "답변은 암호화되어 저장되고, AI 코치에게는 전달되지 않아요."
},
"records": {
@@ -10380,12 +10758,34 @@
"daylight": "낮 길이",
"precip": "강수량",
"pressureMean": "기압",
- "pressureDelta": "기압 변화"
+ "pressureDelta": "기압 변화",
+ "pm25": "초미세먼지 (PM2.5)",
+ "pm10": "미세먼지 (PM10)",
+ "no2": "이산화질소",
+ "ozone8h": "오존 (8시간 최고)",
+ "europeanAqi": "유럽 대기질 지수",
+ "uvIndexMax": "자외선 지수 (최고)",
+ "dustMax": "사하라 먼지",
+ "pollenMax": "꽃가루 (최고)"
+ },
+ "chip": {
+ "day": "{date} 날씨",
+ "pollenHigh": "꽃가루 많음: {kinds}",
+ "hotNight": "더운 밤",
+ "veryPoorAir": "대기질 매우 나쁨"
+ },
+ "pollen": {
+ "alder": "오리나무",
+ "birch": "자작나무",
+ "grass": "잔디",
+ "mugwort": "쑥",
+ "olive": "올리브",
+ "ragweed": "돼지풀"
}
},
"documents": {
"title": "문서",
- "subtitle": "모든 소견서·리포트·영상을 한곳에서 검색해요 — 저장할 때 암호화돼요.",
+ "subtitle": "모든 소견서, 리포트, 영상을 한곳에서 검색해요. 저장할 때 암호화돼요.",
"pageUpload": "업로드",
"ownerOnlyUpload": "문서는 기록 소유자만 추가할 수 있습니다.",
"kind": {
@@ -10405,11 +10805,13 @@
"searchPlaceholder": "제목이나 파일명 검색…",
"typeGroup": "문서 종류",
"conditionGroup": "질환",
+ "procedureGroup": "시술 및 수술",
"yearGroup": "연도",
"clear": "필터 해제",
"typeAll": "모든 종류",
"typeCount": "{count}개 종류",
"conditionAll": "모든 질환",
+ "procedureAll": "모든 시술",
"yearAll": "모든 연도"
},
"card": {
@@ -10425,15 +10827,15 @@
},
"error": {
"fileTooLarge": "이 파일은 {maxSize} 제한보다 커요.",
- "quotaExceeded": "저장 공간이 가득 찼어요 — {quota} 중 {used} 사용 중이에요.",
+ "quotaExceeded": "저장 공간이 가득 찼어요. {quota} 중 {used} 사용 중이에요.",
"unsupportedType": "지원하지 않는 파일 형식이에요.",
"purged": "이 문서는 완전히 삭제되어 복구할 수 없어요.",
"duplicateExists": "같은 문서가 이미 저장돼 있어요.",
- "rateLimited": "지금은 업로드가 너무 많아요 — 몇 분 뒤에 다시 시도해 주세요.",
+ "rateLimited": "지금은 업로드가 너무 많아요. 몇 분 뒤에 다시 시도해 주세요.",
"generic": "업로드하지 못했어요. 다시 시도해 주세요."
},
"toast": {
- "duplicate": "이미 저장돼 있어요 — 기존 문서를 표시해요.",
+ "duplicate": "이미 저장돼 있어요. 기존 문서를 표시해요.",
"deleted": "문서를 삭제했어요",
"restoreFailed": "문서를 복구하지 못했어요."
},
@@ -10456,7 +10858,7 @@
},
"empty": {
"title": "문서 보관함이 비어 있어요",
- "description": "의사 소견서, 검사 리포트, 영상, 처방전, 예방접종 기록을 암호화해 보관하고 종류·질환·연도로 검색해요.",
+ "description": "의사 소견서, 검사 리포트, 영상, 처방전, 예방접종 기록을 암호화해 보관하고 종류, 질환, 연도별로 검색해요.",
"action": "첫 문서 올리기",
"noMatchesTitle": "일치하는 문서 없음",
"noMatchesDescription": "현재 검색·필터에 맞는 문서가 없어요."
@@ -10487,6 +10889,7 @@
"saveError": "변경 내용을 저장하지 못했어요.",
"deleteError": "문서를 삭제하지 못했어요.",
"download": "다운로드",
+ "openInNewTab": "새 탭에서 열기",
"delete": "삭제",
"titleLabel": "제목",
"titlePlaceholder": "제목 입력",
@@ -10496,9 +10899,9 @@
"linkCondition": "연결",
"noConditions": "질환에 연결되지 않음",
"previewUnavailable": "이 파일 형식은 미리보기를 지원하지 않아요",
- "uploadedMeta": "추가 {date} · {size}",
+ "uploadedMeta": "{date} 추가, {size}",
"importedFrom": "{system}에서 가져옴",
- "importedFromWithId": "{system}에서 가져옴 · ID {id}",
+ "importedFromWithId": "{system}에서 가져옴, ID {id}",
"sourceOther": "다른 시스템",
"share": "공유",
"labValuesLink": "검사 수치 {count}개 보기",
@@ -10565,7 +10968,7 @@
"errorNotIndexed": "문서를 먼저 읽은 뒤에 추출해 주세요.",
"errorLocalOcrDisabled": "AI 설정에서 로컬 OCR을 먼저 켜 주세요.",
"errorProvider": "이 작업에 쓸 AI 제공자가 설정돼 있지 않아요.",
- "errorTextImageOnly": "로컬 OCR은 이미지만 읽어요 — 이 파일은 읽을 수 없어요.",
+ "errorTextImageOnly": "로컬 OCR은 이미지만 읽어요. 이 파일은 읽을 수 없어요.",
"errorConsent": "AI로 문서를 읽으려면 먼저 동의가 필요합니다.",
"errorUnavailable": "지금 여기에서는 AI로 문서를 읽을 수 없습니다."
},
@@ -10714,6 +11117,21 @@
"finishedHint": "다시 검색하거나 필터를 바꿔 문서를 더 고르세요.",
"maxReached": "한 번에 최대 {max}개 문서를 가져올 수 있습니다.",
"noMatchHint": "이름의 일부로 검색하거나 태그와 날짜를 지워 보세요."
+ },
+ "customize": "보기 사용자 지정",
+ "layout": {
+ "viewDescription": "미리보기가 있는 카드 또는 간단한 목록.",
+ "arrangementHeading": "배치",
+ "arrangementDescription": "월별로 따로 묶거나 전체 너비에 이어서 표시합니다.",
+ "stacked": "월별로 나누기",
+ "flow": "이어서 보기",
+ "saveFailed": "보기 설정을 저장하지 못했습니다."
+ },
+ "deleteConfirm": {
+ "titleOne": "이 문서를 삭제할까요?",
+ "titleMany": "문서 {count}개를 삭제할까요?",
+ "body": "삭제 직후에는 되돌릴 수 있습니다. 30일이 지나면 완전히 삭제됩니다.",
+ "action": "삭제"
}
},
"selfDescription": {
@@ -10800,7 +11218,7 @@
"genericDescription": "직접 기록하는 측정이에요. 값을 추가하면 시간에 따른 변화를 볼 수 있어요.",
"emptyTitle": "아직 값이 없어요",
"emptyDescription": "첫 값을 추가하면 이 지표를 차트로 볼 수 있어요.",
- "valuesTitle": "{metric} — 전체 값",
+ "valuesTitle": "{metric}: 전체 값",
"valuesDescription": "이 지표에 기록된 모든 값이에요.",
"backToMetric": "돌아가기: {metric}"
}
@@ -10853,67 +11271,73 @@
}
},
"hydration": {
- "referenceMeta": "권장 섭취량 {value} {unit}/일 · EFSA"
+ "referenceMeta": "권장 섭취량 {value} {unit}/일 (EFSA)"
},
"caffeine": {
- "ceilingMeta": "안전 상한 {value} {unit}/일 · EFSA"
+ "ceilingMeta": "안전 상한 {value} {unit}/일 (EFSA)"
},
"micronutrients": {
"title": "비타민·미네랄",
- "rowMeta": "{window}일 중 {days}일 · 기준 {value} {unit} (EFSA)",
+ "rowMeta": "{window}일 중 {days}일, 기준 {value} {unit} (EFSA)",
"rowMetaNoRef": "{window}일 중 {days}일",
"footer": "이 기간에 데이터가 있는 미량영양소는 추적 중인 {total}개 중 {tracked}개예요.",
"emptyTitle": "아직 동기화된 미량영양소 데이터가 없어요",
"loadError": "영양소 데이터를 불러오지 못했어요"
},
- "sparseNote": "여기 수치가 낮은 건 보통 결핍이 아니라 기록이 드물다는 뜻이에요 — Apple Health의 하루 합계는 앱이 기록한 양만 반영하고, 전체 식이 섭취량은 아니에요."
+ "sparseNote": "여기 수치가 낮은 건 보통 결핍이 아니라 기록이 드물다는 뜻이에요. Apple Health의 하루 합계는 앱이 기록한 양만 반영하고, 전체 식이 섭취량은 아니에요."
},
"daily": {
"line": {
- "score": "오늘 건강 점수는 {score}점이에요.",
- "allClear": "오늘은 신경 쓸 일이 없어요."
+ "allClear": "오늘은 신경 쓸 일이 없어요.",
+ "scoreReady": "오늘의 건강 점수가 준비됐어요."
},
"push": {
"title": "아침 브리핑",
- "bodyProvisional": "{line} · 어젯밤 수면은 아직 들어오지 않았어요."
+ "bodyProvisional": "{line}\n어젯밤 수면은 아직 들어오지 않았어요.",
+ "titleScore": "아침 점수 {score}",
+ "band": {
+ "green": "오늘은 안정적으로 보여요.",
+ "yellow": "오늘은 몇 가지를 살펴볼 만해요.",
+ "red": "오늘은 몇몇 영역에 주의가 필요해요."
+ }
},
"item": {
"doseWindow": {
"title": "복약 시간",
"body": "곧 복용할 시간이에요.",
- "bodyNamed": "{name} — 곧 복용할 시간이에요.",
+ "bodyNamed": "{name}: 곧 복용할 시간이에요.",
"overdueTitle": "복약 시간 지남",
"overdueBody": "복용 시간이 지났어요.",
- "overdueBodyNamed": "{name} — 복용 시간이 지났어요."
+ "overdueBodyNamed": "{name}: 복용 시간이 지났어요."
},
"preventiveCare": {
"title": "건강검진 시기",
- "body": "{label} — 받을 때가 됐어요.",
+ "body": "{label}: 받을 때가 됐어요.",
"bodyManyNamed": "받을 때가 된 건강검진: {labels}"
},
"syncIssue": {
"title": "동기화 확인이 필요해요",
- "body": "{integration} 동기화가 멈췄어요 — 데이터를 최신으로 유지하려면 다시 연결해 주세요."
+ "body": "{integration} 동기화가 멈췄어요. 데이터를 최신으로 유지하려면 다시 연결해 주세요."
},
"coachCheckin": {
"title": "계획을 점검할 때예요",
- "body": "이 계획을 세운 지 일주일쯤 됐어요 — 계속할지, 조정할지, 놓아줄지 정해 보세요. 어느 쪽이든 부담 갖지 않아도 돼요.",
- "bodyPlan": "일주일쯤 전에 이렇게 정했어요: “{plan}”. 계속할지, 조정할지, 놓아줄지 정해 보세요 — 어느 쪽이든 부담 갖지 않아도 돼요.",
+ "body": "이 계획을 세운 지 일주일쯤 됐어요. 계속할지, 조정할지, 놓아줄지 정해 보세요. 어느 쪽이든 부담 갖지 않아도 돼요.",
+ "bodyPlan": "일주일쯤 전에 이렇게 정했어요: “{plan}”. 계속할지, 조정할지, 놓아줄지 정해 보세요. 어느 쪽이든 부담 갖지 않아도 돼요.",
"adjustPrompt": "점검 계획을 조정하고 싶어요.",
"adjustPromptPlan": "이 계획을 조정하고 싶어요: “{plan}”"
},
"tensionWindow": {
"title": "오늘 긴장이 있었을 수 있어요",
"body": {
- "morning": "오늘 아침 가만히 있는 동안에도 심박수가 안정 시 기준값 위로 한동안 유지됐어요 — 긴장이 있었을 수 있어요.",
- "afternoon": "오늘 오후 가만히 있는 동안에도 심박수가 안정 시 기준값 위로 한동안 유지됐어요 — 긴장이 있었을 수 있어요.",
- "evening": "오늘 저녁 가만히 있는 동안에도 심박수가 안정 시 기준값 위로 한동안 유지됐어요 — 긴장이 있었을 수 있어요.",
- "night": "밤사이 가만히 있는 동안에도 심박수가 안정 시 기준값 위로 한동안 유지됐어요 — 긴장이 있었을 수 있어요."
+ "morning": "오늘 아침 가만히 있는 동안에도 심박수가 안정 시 기준값 위로 한동안 유지됐어요. 긴장이 있었을 수 있어요.",
+ "afternoon": "오늘 오후 가만히 있는 동안에도 심박수가 안정 시 기준값 위로 한동안 유지됐어요. 긴장이 있었을 수 있어요.",
+ "evening": "오늘 저녁 가만히 있는 동안에도 심박수가 안정 시 기준값 위로 한동안 유지됐어요. 긴장이 있었을 수 있어요.",
+ "night": "밤사이 가만히 있는 동안에도 심박수가 안정 시 기준값 위로 한동안 유지됐어요. 긴장이 있었을 수 있어요."
}
},
"ecgNewRecording": {
"title": "새 심전도 기록",
- "body": "기기가 새 심전도를 기록했어요 — 지금 볼 수 있어요.",
+ "body": "기기가 새 심전도를 기록했어요. 지금 볼 수 있어요.",
"bodyVerdict": "기기가 새 심전도를 기록하고 이렇게 알렸어요: {verdict}.",
"verdict": {
"irregular": "불규칙한 리듬 가능성",
@@ -10933,9 +11357,9 @@
},
"upcomingVisit": {
"title": "다가오는 예약",
- "bodyToday": "{what} — 오늘",
- "bodyTomorrow": "{what} — 내일",
- "bodyDayAfterTomorrow": "{what} — 모레"
+ "bodyToday": "{what}: 오늘",
+ "bodyTomorrow": "{what}: 내일",
+ "bodyDayAfterTomorrow": "{what}: 모레"
}
},
"action": {
@@ -10957,7 +11381,31 @@
"sleepPending": "어젯밤 수면이 아직 들어오지 않아 오늘 결과는 잠정치예요.",
"worthALook": "살펴볼 만한 것",
"allClear": "오늘은 신경 쓸 일이 없어요.",
- "ringLink": "{metric} 자세히 보기"
+ "ringLink": "{metric} 자세히 보기",
+ "heading": "오늘",
+ "steadyWeeksOne": "{count}주째 안정적",
+ "steadyWeeksFew": "{count}주째 안정적",
+ "steadyWeeksOther": "{count}주째 안정적",
+ "valueWithUnit": "{value} {unit}",
+ "rangeWithUnit": "{low}~{high} {unit}",
+ "rangeAtThisTime": "이 시간대 기준 {range}",
+ "vital": {
+ "RESTING_HEART_RATE": "안정 시 심박수",
+ "HEART_RATE_VARIABILITY": "심박 변이도",
+ "RESPIRATORY_RATE": "호흡수",
+ "OXYGEN_SATURATION": "산소포화도",
+ "BODY_TEMPERATURE": "체온",
+ "SKIN_TEMPERATURE": "피부 온도",
+ "BLOOD_PRESSURE_SYS": "수축기 혈압",
+ "BLOOD_PRESSURE_DIA": "이완기 혈압",
+ "BLOOD_GLUCOSE": "혈당",
+ "PULSE": "맥박",
+ "WEIGHT": "체중",
+ "generic": "생체 지표"
+ },
+ "steadyAtLeastWeeksOne": "최소 {count}주째 안정적",
+ "steadyAtLeastWeeksFew": "최소 {count}주째 안정적",
+ "steadyAtLeastWeeksOther": "최소 {count}주째 안정적"
},
"milestone": {
"metric": {
@@ -10969,7 +11417,7 @@
},
"record": {
"title": "{metric} 최고 기록",
- "body": "오늘 {metric} 개인 최고 기록이에요 — 조용히 적어 뒀어요, 하실 일은 없어요."
+ "body": "오늘 {metric} 개인 최고 기록이에요. 조용히 적어 뒀어요, 하실 일은 없어요."
},
"returnToBaseline": {
"title": "범위 복귀: {metric}",
@@ -10993,6 +11441,68 @@
"body": "{metric} 수치가 꼬박 한 달 동안 평소 범위 안이었어요. 아주 꾸준해요."
}
}
+ },
+ "lead": {
+ "vitalOutside": {
+ "above": "{metric}이(가) {value}로 평소 범위({range})보다 높아요.",
+ "below": "{metric}이(가) {value}로 평소 범위({range})보다 낮아요."
+ },
+ "vitalsOutside": {
+ "aboveOne": "{metric}이(가) {value}로 평소 범위({range})보다 높고, 다른 지표 {count}개도 범위를 벗어났어요.",
+ "aboveFew": "{metric}이(가) {value}로 평소 범위({range})보다 높고, 다른 지표 {count}개도 범위를 벗어났어요.",
+ "aboveOther": "{metric}이(가) {value}로 평소 범위({range})보다 높고, 다른 지표 {count}개도 범위를 벗어났어요.",
+ "belowOne": "{metric}이(가) {value}로 평소 범위({range})보다 낮고, 다른 지표 {count}개도 범위를 벗어났어요.",
+ "belowFew": "{metric}이(가) {value}로 평소 범위({range})보다 낮고, 다른 지표 {count}개도 범위를 벗어났어요.",
+ "belowOther": "{metric}이(가) {value}로 평소 범위({range})보다 낮고, 다른 지표 {count}개도 범위를 벗어났어요."
+ },
+ "sleep": {
+ "more": "어젯밤 {duration} 잤어요. 평소보다 {diff} 더 잤어요.",
+ "less": "어젯밤 {duration} 잤어요. 평소보다 {diff} 덜 잤어요.",
+ "usual": "어젯밤 {duration} 잤어요. 평소와 비슷해요.",
+ "plain": "어젯밤 {duration} 잤어요."
+ },
+ "vitalsInRangeOne": "최근 생체 지표가 평소 범위 안에 있어요.",
+ "vitalsInRangeFew": "최근 생체 지표 {count}개가 모두 평소 범위 안에 있어요.",
+ "vitalsInRangeOther": "최근 생체 지표 {count}개가 모두 평소 범위 안에 있어요.",
+ "vitalInRange": "{metric}이(가) {value}로 평소 범위({range}) 안에 있어요."
+ },
+ "todayFact": {
+ "restMode": {
+ "label": "휴식 모드",
+ "value": "{day}일째"
+ },
+ "medications": {
+ "label": "복약",
+ "value": "{scheduled}회 중 {taken}회 복용"
+ },
+ "appointment": {
+ "label": "예약",
+ "today": "오늘 {time}, {what}",
+ "tomorrow": "내일 {time}, {what}"
+ },
+ "sleep": {
+ "label": "어젯밤",
+ "plain": "{duration}",
+ "usual": "{duration}, 평소와 비슷",
+ "more": "{duration}, 평소보다 {diff} 더",
+ "less": "{duration}, 평소보다 {diff} 덜"
+ },
+ "vitals": {
+ "label": "생체 지표",
+ "allInOne": "{count}개 확인, 범위 안",
+ "allInFew": "{count}개 확인, 모두 범위 안",
+ "allInOther": "{count}개 확인, 모두 범위 안",
+ "oneOut": {
+ "above": "{metric} 범위보다 높음",
+ "below": "{metric} 범위보다 낮음"
+ },
+ "someOut": "{total}개 중 {count}개 범위 밖"
+ },
+ "cycle": {
+ "label": "주기",
+ "value": "{phase}, {day}일째",
+ "dayOnly": "{day}일째"
+ }
}
},
"reportSelection": {
@@ -11069,7 +11579,7 @@
"canManage": "열람, 추가, 그리고 있는 내용의 수정·삭제까지 가능",
"entireRecord": "기록 전체가 대상",
"sections": "대상: {sections}",
- "noSections": "대상 없음 — 이 권한으로는 기록의 어떤 부분도 열 수 없어요",
+ "noSections": "대상 없음: 이 권한으로는 기록의 어떤 부분도 열 수 없어요",
"invited": "초대",
"accepted": "수락",
"expires": "만료",
@@ -11239,7 +11749,47 @@
"guardianErrorFailed": "초대를 보내지 못했어요.",
"errorMfaRequired": "여기에는 2단계 인증이 필요한데 계정에 아직 없어요. 설정 → 보안에서 등록한 뒤 다시 시도해 주세요.",
"rosterLoadError": "이 기록을 돌보는 사람을 불러오지 못했어요.",
- "errorRateLimit": "최근 한 시간 동안 만든 프로필이 너무 많아요. 잠시 기다렸다 다시 시도해 주세요."
+ "errorRateLimit": "최근 한 시간 동안 만든 프로필이 너무 많아요. 잠시 기다렸다 다시 시도해 주세요.",
+ "handover": {
+ "action": "넘겨주기",
+ "title": "{name} 님에게 넘겨주기",
+ "description": "{name} 님에게 자신의 로그인을 만들어 주세요. 아무것도 복사되거나 옮겨지지 않고, 기록이 그대로 그 사람의 계정이 됩니다.",
+ "guardiansLabel": "넘겨준 뒤의 접근 권한",
+ "guardiansHint": "내 제안입니다. {name} 님이 로그인한 뒤 보고 결정합니다.",
+ "access": {
+ "end": "접근 없음",
+ "read": "보기만",
+ "manage": "관리"
+ },
+ "expiryLabel": "링크 유효 기간",
+ "expiry1": "1일",
+ "expiry7": "7일",
+ "expiry14": "14일",
+ "minorHint": "{name} 님은 16세 미만입니다. 보기 권한을 유지하고 이 단계를 함께 이야기해 보는 것이 좋습니다.",
+ "irreversible": "{name} 님이 링크를 사용하면 넘겨주기를 되돌릴 수 없습니다. 기록은 그 사람의 것이 되고, 누가 접근 권한을 유지할지 직접 결정합니다.",
+ "afterwards": "{name} 님이 안내 사항과 AI 동의를 직접 확인합니다. 이 기록의 알림은 더 이상 나에게 오지 않습니다.",
+ "replaceNote": "새 링크를 만들면 열려 있는 링크는 철회됩니다.",
+ "create": "링크 만들기",
+ "creating": "만드는 중…",
+ "linkOnce": "이 링크는 지금만 표시됩니다. {name} 님에게 직접 또는 믿을 수 있는 방법으로 전달하거나 코드를 스캔하게 하세요.",
+ "qrAlt": "{name} 님을 위한 넘겨주기 링크 QR 코드",
+ "urlLabel": "넘겨주기 링크",
+ "copy": "복사",
+ "copied": "링크를 복사했습니다",
+ "copyError": "링크를 복사하지 못했습니다. 선택해서 직접 복사하세요.",
+ "linkExpires": "{date}까지 유효합니다.",
+ "done": "완료",
+ "pending": "{date}까지 넘겨주기 링크가 열려 있습니다.",
+ "replace": "새 링크",
+ "revoke": "링크 철회",
+ "revoked": "링크를 철회했습니다",
+ "revokeFailed": "링크를 철회하지 못했습니다. 다시 시도하세요.",
+ "errorFailed": "넘겨주기 링크를 만들지 못했습니다.",
+ "errorRosterChanged": "그 사이 기록을 돌보는 사람이 바뀌었습니다. 닫았다가 다시 여세요.",
+ "oidcOnly": "이 서버가 통합 로그인(SSO)만 허용하는 동안에는 프로필을 넘겨줄 수 없습니다.",
+ "loadError": "넘겨주기 상태를 불러오지 못했습니다.",
+ "entryHint": "{name} 님이 이 기록을 직접 관리할 준비가 되었을 때."
+ }
},
"toast": {
"savedTo": "{name} 님의 기록에 저장했어요"
@@ -11358,7 +11908,13 @@
"symptomEventDelete": "{name}님이 증상 기록을 삭제했습니다",
"symptomDefinitionCreate": "{name}님이 증상을 추가했습니다",
"symptomDefinitionUpdate": "{name}님이 증상을 변경했습니다",
- "symptomDefinitionDelete": "{name}님이 증상을 제거했습니다"
+ "symptomDefinitionDelete": "{name}님이 증상을 제거했습니다",
+ "customVaccineCreate": "{name} 님이 백신을 직접 추가했어요",
+ "customVaccineUpdate": "{name} 님이 직접 추가한 백신을 수정했어요",
+ "customVaccineDelete": "{name} 님이 직접 추가한 백신을 삭제했어요",
+ "lifeEventCreate": "{name} 님이 삶의 사건을 추가했어요",
+ "lifeEventUpdate": "{name} 님이 삶의 사건을 수정했어요",
+ "lifeEventDelete": "{name} 님이 삶의 사건을 삭제했어요"
},
"section": {
"measurements": {
@@ -11414,6 +11970,22 @@
},
"anamnesis": {
"relativeNote": "가족력 항목은 이 사람의 친척에 대한 내용이에요. 여기 기록되기를 상대가 원할 때만 추가하세요."
+ },
+ "handoverDecision": {
+ "title": "내 기록에 접근할 수 있는 사람",
+ "description": "기록을 관리해 온 사람들이 앞으로 무엇을 할 수 있을지 정하세요.",
+ "proposed": "제안: {access}",
+ "settled": "공유 접근에서 이미 변경됨",
+ "empty": "다른 사람은 내 기록에 접근할 수 없었습니다.",
+ "later": "설정, 공유 접근에서 언제든지 바꿀 수 있습니다.",
+ "save": "확인",
+ "saving": "저장하는 중…",
+ "decideLater": "나중에",
+ "errorFailed": "결정을 저장하지 못했습니다. 다시 시도하세요.",
+ "loadError": "기록을 관리해 온 사람들을 불러오지 못했습니다.",
+ "notificationsTitle": "내 알림 설정하기",
+ "notificationsBody": "지금까지 이 기록의 알림은 기록을 관리하던 사람들에게 갔습니다. 이제 알림 채널을 추가하면 바로 나에게 옵니다.",
+ "notificationsAction": "알림 설정 열기"
}
},
"encounters": {
@@ -11434,7 +12006,7 @@
"both": "양쪽",
"none": "지정 안 함"
},
- "bodySiteWithSide": "{site} · {side}",
+ "bodySiteWithSide": "{site} ({side})",
"sectionTitle": "진료",
"sectionDescription": "병원 방문과 잡아 둔 예약을 한곳에서 봐요.",
"add": "진료",
@@ -11449,7 +12021,7 @@
"created": "진료를 저장했어요",
"updated": "진료를 수정했어요",
"saveFailed": "진료를 저장하지 못했어요.",
- "checkupNotClosed": "진료를 저장했어요. 건강검진은 열린 채로 남아요 — 권한이 거기까지는 미치지 않아요.",
+ "checkupNotClosed": "진료를 저장했어요. 건강검진은 열린 채로 남아요. 권한이 거기까지는 미치지 않아요.",
"status": {
"planned": "예정",
"done": "방문함",
@@ -11466,7 +12038,7 @@
"dateDerivesStatusHint": "미래 날짜는 잡아 둔 예약으로 처리돼요.",
"status": "상태",
"practitioner": "병원 또는 의사",
- "practitionerHint": "선택 — 이름은 나중에 추가해도 돼요.",
+ "practitionerHint": "선택: 이름은 나중에 추가해도 돼요.",
"practitionerNone": "지정 안 함",
"practitionerSearch": "이름이나 병원 검색",
"practitionerNoMatch": "항목을 찾지 못했어요.",
@@ -11524,9 +12096,8 @@
},
"practitioners": {
"title": "병원과 의사",
- "description": "나만의 주소록이에요 — 병원을 한 번만 입력하면 어디서든 골라 쓸 수 있어요.",
+ "description": "나만의 주소록이에요. 병원을 한 번만 입력하면 어디서든 골라 쓸 수 있어요.",
"linkLabel": "주소록",
- "add": "항목",
"searchPlaceholder": "이름, 병원, 진료과 검색",
"loadError": "주소록을 불러오지 못했어요.",
"emptyTitle": "아직 항목이 없어요",
@@ -11623,7 +12194,6 @@
"unreadableLabel": "읽을 수 없는 증상",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "증상 기록",
"sheetDescription": "직접 정한 증상과 강도, 시각을 기록합니다.",
"symptom": "증상",
"symptomRequired": "증상을 선택하세요.",
@@ -11672,5 +12242,426 @@
"title": "기록된 증상",
"filed": "이 질병과 함께 기록됨"
}
+ },
+ "day": {
+ "scoreOfMax": "/ {max}",
+ "cycleDay": "주기 {n}일째",
+ "cyclePhaseLine": {
+ "MENSTRUAL": "주기 {n}일째, 월경기",
+ "FOLLICULAR": "주기 {n}일째, 난포기",
+ "OVULATORY": "주기 {n}일째, 배란기",
+ "LUTEAL": "주기 {n}일째, 황체기"
+ },
+ "previousDay": "전날",
+ "nextDay": "다음 날",
+ "close": "하루 닫기",
+ "hidePanel": "하루 숨기기",
+ "showPanel": "{date} 보기",
+ "openDay": "하루 열기",
+ "viewWholeDay": "하루 전체 보기",
+ "captureForDay": "이 날에 기록하기",
+ "empty": "이 날에는 기록이 없어요.",
+ "notShared": "일부 영역은 공유되지 않았어요.",
+ "inTimeline": "타임라인에서 보기",
+ "askCoach": "이 날에 대해 코치에게 묻기",
+ "loadFailed": "이 날을 불러오지 못했어요.",
+ "dayOf": "{m}일 중 {n}일째",
+ "dayN": "{n}일째",
+ "usualRange": "평소 {lo}~{hi}",
+ "groups": {
+ "scores": "점수",
+ "running": "그날 진행 중",
+ "values": "수치",
+ "happened": "있었던 일"
+ },
+ "notable": {
+ "extremeHigh": "{since} 이후 가장 높은 하루 값",
+ "extremeLow": "{since} 이후 가장 낮은 하루 값",
+ "firstValue": "이 종류의 첫 측정",
+ "gap": "{days}일 동안 측정 없음"
+ },
+ "previousDayShortcut": "전날 (Alt + 왼쪽 화살표)",
+ "nextDayShortcut": "다음 날 (Alt + 오른쪽 화살표)",
+ "captureShort": "기록",
+ "inTimelineShort": "타임라인",
+ "askCoachPrompt": "{date}에 무슨 일이 있었나요?",
+ "coachChips": "이 답변에 쓰인 날",
+ "description": "이 날에 대한 모든 기록이에요.",
+ "openDocument": "문서 열기",
+ "previousReading": "이전, {date}",
+ "sheetExpand": "전체 높이로 펼치기",
+ "sheetShrink": "줄이기",
+ "valuesLegend": "점은 이 날, 막대는 직전 30일",
+ "valuesAllOne": "모든 수치",
+ "valuesAllFew": "수치 {count}개 모두",
+ "valuesAllOther": "수치 {count}개 모두",
+ "valuesFewer": "수치 줄여 보기",
+ "countEntriesOne": "기록 1개",
+ "countEntriesFew": "기록 {count}개",
+ "countEntriesOther": "기록 {count}개",
+ "meanOfReadingsOne": "측정 1회 평균",
+ "meanOfReadingsFew": "측정 {count}회 평균",
+ "meanOfReadingsOther": "측정 {count}회 평균",
+ "sinceLastVisit": {
+ "title": "{date} 마지막 방문 이후",
+ "countOne": "기록 1개",
+ "countFew": "기록 {count}개",
+ "countOther": "기록 {count}개",
+ "allOne": "모두 보기",
+ "allFew": "{count}개 모두 보기",
+ "allOther": "{count}개 모두 보기",
+ "fewer": "줄여 보기",
+ "note": "평가 없이 기록에서 모았어요. 날짜를 누르면 그날 전체가 열려요.",
+ "report": "{date}부터 오늘까지 진료 보고서"
+ },
+ "travel": "여행 중",
+ "event": {
+ "started": "{label} 시작",
+ "ended": "{label} 끝남",
+ "paused": "{label} 일시 중지",
+ "resumed": "{label} 다시 시작",
+ "illnessBegan": "{label} 시작",
+ "illnessResolved": "{label} 나음"
+ },
+ "pickDate": "날짜 선택",
+ "pickerTitle": "날짜로 이동",
+ "today": "오늘",
+ "hasEntries": "기록 있음",
+ "dayNSince": "{n}일째, {date}부터",
+ "sinceDate": "{date}부터"
+ },
+ "timeline": {
+ "title": "타임라인",
+ "subtitle": "언제 무엇이 있었는지, 내 수치와 나란히.",
+ "standing": "지속",
+ "startUnknown": "시작일 미상",
+ "zoom": {
+ "all": "전체",
+ "year": "12개월",
+ "quarter": "3개월",
+ "range": "기간"
+ },
+ "lanes": {
+ "life": "삶",
+ "illness": "질환",
+ "allergies": "알레르기",
+ "medications": "약",
+ "vaccinations": "예방접종",
+ "visits": "진료와 시술",
+ "labs": "검사",
+ "documents": "문서",
+ "cycle": "생리 주기"
+ },
+ "readiness": {
+ "title": "타임라인에 이미 담긴 것",
+ "subtitle": "열기 전에 솔직하게 살펴보는 현황이에요.",
+ "menuEntry": "데이터 현황",
+ "hint": "빈 줄은 무언가 생길 때까지 숨겨져요. 꺼 둔 모듈은 나타나지 않아요. 이 현황은 나중에 타임라인의 데이터 현황에서 다시 볼 수 있어요.",
+ "later": "나중에",
+ "open": "타임라인 열기",
+ "verdict": {
+ "carries": "이미 충분히 채워졌어요.",
+ "thin": "아직 부족해요. 당분간 타임라인은 주로 수치를 보여줘요."
+ },
+ "status": {
+ "carries": "충분",
+ "thin": "부족",
+ "empty": "비어 있음"
+ },
+ "lanes": {
+ "values": "수치",
+ "mood": "기분",
+ "environment": "환경",
+ "life": "삶의 사건"
+ },
+ "meter": "{total}개 중 {carrying}개 줄이 채워졌어요",
+ "carriesDetail": "{total}개 중 {carrying}개 줄로 {since}부터 한눈에 볼 수 있어요.",
+ "loadFailed": "현황을 불러오지 못했어요.",
+ "card": {
+ "title": "타임라인에 이미 담긴 것",
+ "summary": "{since}까지 거슬러 올라가며, {total}개 중 {carrying}개 줄이 이미 채워졌어요.",
+ "dismiss": "숨기기"
+ },
+ "detail": {
+ "valuesOne": "{since}부터 측정 항목 {count}개",
+ "valuesFew": "{since}부터 측정 항목 {count}개",
+ "valuesOther": "{since}부터 측정 항목 {count}개",
+ "illnessOne": "에피소드 {count}개, 만성 {chronic}개",
+ "illnessFew": "에피소드 {count}개, 만성 {chronic}개",
+ "illnessOther": "에피소드 {count}개, 만성 {chronic}개",
+ "allergiesOne": "알레르기 {count}개",
+ "allergiesFew": "알레르기 {count}개",
+ "allergiesOther": "알레르기 {count}개",
+ "medicationsOne": "약 {count}개, 시작일 없음 {missingStart}개",
+ "medicationsFew": "약 {count}개, 시작일 없음 {missingStart}개",
+ "medicationsOther": "약 {count}개, 시작일 없음 {missingStart}개",
+ "medicationsDatedOne": "시작일 있는 약 {count}개",
+ "medicationsDatedFew": "시작일 있는 약 {count}개",
+ "medicationsDatedOther": "시작일 있는 약 {count}개",
+ "vaccinationsOne": "예방접종 {count}건",
+ "vaccinationsFew": "예방접종 {count}건",
+ "vaccinationsOther": "예방접종 {count}건",
+ "visitsOne": "진료 {count}건, 시술 {procedures}건",
+ "visitsFew": "진료 {count}건, 시술 {procedures}건",
+ "visitsOther": "진료 {count}건, 시술 {procedures}건",
+ "visitsPlannedOne": "예정된 진료만 {count}건",
+ "visitsPlannedFew": "예정된 진료만 {count}건",
+ "visitsPlannedOther": "예정된 진료만 {count}건",
+ "labsOne": "결과가 있는 날 {count}일",
+ "labsFew": "결과가 있는 날 {count}일",
+ "labsOther": "결과가 있는 날 {count}일",
+ "documentsOne": "날짜 있는 문서 {count}개",
+ "documentsFew": "날짜 있는 문서 {count}개",
+ "documentsOther": "날짜 있는 문서 {count}개",
+ "lifeOne": "삶의 사건 {count}개",
+ "lifeFew": "삶의 사건 {count}개",
+ "lifeOther": "삶의 사건 {count}개",
+ "cycleOne": "주기 {count}개",
+ "cycleFew": "주기 {count}개",
+ "cycleOther": "주기 {count}개",
+ "moodOne": "{since}부터 기록 {count}개",
+ "moodFew": "{since}부터 기록 {count}개",
+ "moodOther": "{since}부터 기록 {count}개",
+ "environmentOne": "환경 데이터가 있는 날 {count}일",
+ "environmentFew": "환경 데이터가 있는 날 {count}일",
+ "environmentOther": "환경 데이터가 있는 날 {count}일",
+ "empty": "아직 없음"
+ },
+ "gap": {
+ "valuesEmpty": "소스 연결",
+ "illnessEmpty": "에피소드 추가",
+ "medicationsWithoutStart": "채우기",
+ "visitsEmpty": "진료 추가",
+ "documentsUndated": "확인하기",
+ "labsEmpty": "결과 추가",
+ "vaccinationsEmpty": "예방접종 추가",
+ "lifeEventsEmpty": "추가"
+ },
+ "gapLine": {
+ "medicationsWithoutStartOne": "시작일 없는 약 {count}개",
+ "medicationsWithoutStartFew": "시작일 없는 약 {count}개",
+ "medicationsWithoutStartOther": "시작일 없는 약 {count}개",
+ "documentsUndatedOne": "날짜 없는 문서 {count}개",
+ "documentsUndatedFew": "날짜 없는 문서 {count}개",
+ "documentsUndatedOther": "날짜 없는 문서 {count}개",
+ "valuesEmpty": "아직 오랜 기간의 수치가 없어요",
+ "illnessEmpty": "아직 시작일이 있는 질환이 없어요",
+ "visitsEmpty": "아직 완료된 진료가 없어요",
+ "labsEmpty": "아직 검사 결과가 없어요",
+ "vaccinationsEmpty": "아직 예방접종이 없어요",
+ "lifeEventsEmpty": "아직 삶의 사건이 없어요"
+ }
+ },
+ "legendSpan": "기간",
+ "legendEvent": "사건",
+ "legendMeanQuarter": "분기 평균",
+ "legendMeanMonth": "월평균",
+ "legendMeanWeek": "주간 평균",
+ "legendThin": "측정 3회 미만",
+ "legendGap": "측정 없는 짧은 공백",
+ "today": "오늘",
+ "noDataYet": "아직 데이터 없음",
+ "layers": "레이어",
+ "layersLanes": "줄",
+ "zoomLabel": "보기 범위",
+ "chartLabel": "{from}부터 {to}까지의 타임라인",
+ "keyboardHint": "화살표 키로 선택을 옮기고, Home과 End로 양 끝으로 이동하며, Enter로 그날을 열어요.",
+ "loadFailed": "타임라인을 불러오지 못했어요.",
+ "empty": {
+ "title": "아직 타임라인에 아무것도 없어요",
+ "description": "날짜가 있는 기록이 생기면 여기에 나타나요."
+ },
+ "values": {
+ "menuTitle": "레인 아래 수치",
+ "trigger": "수치 ({count})",
+ "noneInWindow": "이 기간에 수치 없음",
+ "series": {
+ "BLOOD_PRESSURE_SYS": "수축기 혈압",
+ "BLOOD_PRESSURE_DIA": "이완기 혈압"
+ },
+ "noValue": "값 없음",
+ "bloodPressure": "혈압",
+ "readingsOne": "측정 1회",
+ "readingsFew": "측정 {count}회 평균",
+ "readingsOther": "측정 {count}회 평균"
+ },
+ "table": {
+ "caption": "타임라인 기록",
+ "lane": "줄",
+ "entry": "기록",
+ "start": "시작",
+ "end": "끝",
+ "seriesCaption": "기간별 평균",
+ "period": "기간"
+ },
+ "selection": {
+ "empty": "이 기간에는 항목이 없습니다",
+ "since": "{date}부터",
+ "range": "{from}~{to}",
+ "mean": "Ø {values}",
+ "week": "{date} 주",
+ "hintClick": "시점을 클릭하면 여기에 표시됩니다",
+ "hintTap": "시점을 탭하면 여기에 표시됩니다"
+ },
+ "chronicle": {
+ "months": "월",
+ "years": "연도",
+ "groupingLabel": "묶기",
+ "empty": "{from}~{to} 기록 없음",
+ "emptySingle": "{period} 기록 없음",
+ "ended": "{label} 끝남",
+ "afterDaysOne": "{count}일 후",
+ "afterDaysFew": "{count}일 후",
+ "afterDaysOther": "{count}일 후",
+ "notable": {
+ "extremeHigh": "오랜만의 최고값",
+ "extremeLow": "오랜만의 최저값",
+ "firstValue": "측정 항목의 첫 수치",
+ "gap": "수치에 긴 공백"
+ },
+ "notableMeta": "자세한 내용은 그날을 열어 보세요"
+ },
+ "standingAllergiesOne": "알레르기 {count}개",
+ "standingAllergiesFew": "알레르기 {count}개",
+ "standingAllergiesOther": "알레르기 {count}개",
+ "legendMeanDay": "일 평균",
+ "range": {
+ "from": "시작",
+ "to": "종료",
+ "label": "선택한 기간",
+ "open": "기간 선택",
+ "clear": "전체 보기",
+ "apply": "적용"
+ },
+ "item": {
+ "labValuesOne": "검사 수치 {count}개",
+ "labValuesFew": "검사 수치 {count}개",
+ "labValuesOther": "검사 수치 {count}개"
+ }
+ },
+ "lifeEvents": {
+ "title": "삶의 사건",
+ "add": "삶의 사건",
+ "privacy": "제목과 메모는 암호화되어 저장돼요. 어떤 AI에도 보내지 않아요.",
+ "emptyHint": "이사, 출산, 새 직장 같은 일이 몇 년에 기준점을 만들어 줘요.",
+ "save": "저장",
+ "cancel": "취소",
+ "delete": "삶의 사건 삭제",
+ "saved": "삶의 사건을 저장했어요",
+ "deleted": "삶의 사건을 삭제했어요",
+ "fields": {
+ "title": "무슨 일이 있었나요?",
+ "category": "종류",
+ "when": "언제",
+ "precision": "정확도",
+ "addEnd": "기간이었다면 종료일",
+ "endDate": "종료",
+ "note": "메모",
+ "optionalTag": "선택",
+ "notePlaceholder": "나중에 알고 싶은 내용",
+ "removeEnd": "종료일 삭제"
+ },
+ "category": {
+ "FAMILY": "가족",
+ "HOME": "주거",
+ "WORK": "일",
+ "LOSS": "상실",
+ "OTHER": "기타"
+ },
+ "precision": {
+ "DAY": "일",
+ "MONTH": "월",
+ "YEAR": "연"
+ },
+ "edit": "삶의 사건 편집",
+ "errors": {
+ "titleRequired": "사건의 이름을 적어 주세요.",
+ "titleTooLong": "최대 120자까지예요.",
+ "categoryRequired": "종류를 골라 주세요.",
+ "dateRequired": "올바른 날짜를 입력해 주세요.",
+ "endBeforeStart": "끝이 시작보다 앞서요.",
+ "noteTooLong": "최대 2000자까지예요."
+ },
+ "saveFailed": "저장하지 못했어요.",
+ "deleteFailed": "삭제하지 못했어요.",
+ "deleteConfirm": {
+ "title": "이 삶의 사건을 삭제할까요?",
+ "body": "제목과 메모가 사라져요."
+ }
+ },
+ "palette": {
+ "title": "검색",
+ "description": "페이지, 설정, 모듈, 작업 또는 내 기록의 항목을 찾습니다.",
+ "placeholder": "페이지, 설정, 내 기록 검색…",
+ "results": "결과",
+ "noResults": "“{query}”에 대한 결과가 없습니다.",
+ "moduleOn": "켜짐",
+ "moduleOff": "꺼짐",
+ "hintMove": "이동",
+ "hintOpen": "열기",
+ "hintClose": "닫기",
+ "groups": {
+ "recent": "최근",
+ "pages": "페이지",
+ "insights": "인사이트",
+ "settings": "설정",
+ "modules": "모듈",
+ "actions": "작업",
+ "records": "내 기록에서"
+ },
+ "actions": {
+ "capture": "새 항목",
+ "coach": "코치에게 묻기",
+ "backup": "백업 만들기",
+ "today": "오늘 열기",
+ "shortcuts": "키보드 단축키 보기"
+ },
+ "records": {
+ "loading": "내 기록을 검색하는 중…",
+ "empty": "내 기록에서 “{query}”에 대한 결과가 없습니다.",
+ "error": "내 기록을 검색할 수 없습니다. 잠시 후 다시 시도하세요.",
+ "truncated": "최근 항목만 검색했습니다.",
+ "kind": {
+ "visit": "진료",
+ "condition": "질환",
+ "document": "문서",
+ "vaccination": "예방접종",
+ "lab": "검사 결과",
+ "medication": "약",
+ "symptom": "증상",
+ "workout": "운동",
+ "note": "메모"
+ },
+ "source": {
+ "mood": "기분 메모",
+ "measurement": "측정값 메모",
+ "illness": "질병 일지"
+ }
+ }
+ },
+ "shortcuts": {
+ "title": "키보드 단축키",
+ "description": "입력란에 입력하는 중이 아니면 앱 어디에서나 작동하는 키입니다.",
+ "menuEntry": "키보드 단축키",
+ "then": "다음",
+ "search": "검색",
+ "help": "이 목록 보기",
+ "close": "대화 상자나 패널 닫기",
+ "previousDay": "이전 날",
+ "nextDay": "다음 날",
+ "toggleDay": "하루 열기 또는 닫기",
+ "dayHint": "이전 날과 다음 날은 하루가 열려 있을 때 작동합니다.",
+ "groups": {
+ "navigation": "탐색",
+ "actions": "작업",
+ "day": "하루"
+ }
+ },
+ "aboutPage": {
+ "eyebrow": "정보",
+ "title": "HealthLog 정보",
+ "lastUpdated": "마지막 업데이트: {date}",
+ "project": "프로젝트",
+ "credits": "크레딧"
}
}
diff --git a/messages/pl.json b/messages/pl.json
index caa7f9245..a8d64dd6c 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -49,7 +49,9 @@
"dismiss": "Odrzuć",
"moreInfo": "Więcej informacji",
"durationHoursMinutes": "{hours} godz. {minutes} min",
- "durationMinutes": "{minutes} min"
+ "durationMinutes": "{minutes} min",
+ "durationClockMinutes": "{time} min",
+ "durationClockHours": "{time} godz."
},
"apiErrors": {
"measurement": {
@@ -165,7 +167,7 @@
"anamnesisShiftSchedule": "Harmonogram pracy",
"anamnesisColFact": "Pozycja profilu",
"anamnesisColValue": "Zapisana wartość",
- "subtitle": "HealthLog — Osobisty raport zdrowotny",
+ "subtitle": "HealthLog: osobisty raport zdrowotny",
"patient": "Pacjent",
"dateOfBirth": "Data urodzenia",
"gender": "Płeć",
@@ -175,18 +177,18 @@
"height": "Wzrost",
"period": "Okres raportu",
"createdOn": "Utworzono",
- "vitalsTitle": "Parametry życiowe — Podsumowanie",
+ "vitalsTitle": "Parametry życiowe: podsumowanie",
"colParameter": "Parametr",
"colCurrent": "Aktualny",
"colAvgPeriod": "Śr. okresu",
"colMin": "Min",
"colMax": "Maks",
"colN": "n",
- "bpClassificationTitle": "Ciśnienie tętnicze — klasyfikacja ESH (2023)",
+ "bpClassificationTitle": "Ciśnienie tętnicze: klasyfikacja ESH (2023)",
"avgBp": "Średnie ciśnienie tętnicze",
"classification": "Klasyfikacja",
"bmiTitle": "Wskaźnik masy ciała (BMI)",
- "bmiRow": "BMI:{bmi} kg/m² — Classification: {class} (WHO)",
+ "bmiRow": "BMI:{bmi} kg/m², Classification: {class} (WHO)",
"complianceTitle": "Przestrzeganie leków",
"glp1Title": "Terapia GLP-1",
"glp1WeightSummary": "Waga w okresie raportu: {start} kg → {end} kg ({delta} kg)",
@@ -204,7 +206,7 @@
"colExpected": "Oczekiwane",
"colComplianceRate": "Wskaźnik przestrzegania",
"moodTitle": "Przebieg nastroju",
- "moodSummary": "Średnia:{avg} / 5.0 · Entries: {count} · Range: {min}–{max}",
+ "moodSummary": "Średnia {avg} / 5.0, liczba wpisów {count}, zakres {min}–{max}",
"colMood": "Nastrój",
"colCount": "Liczba",
"colShare": "Udział",
@@ -234,8 +236,8 @@
"typeGlucoseRandom": "Glukoza (dowolna)",
"typeGlucoseBedtime": "Glukoza (przed snem)",
"typeGlucoseUnspecified": "Glukoza (bez oznaczenia)",
- "glucoseClassificationTitle": "Glikemia — zakresy referencyjne (ADA 2024 / DDG)",
- "glucoseRow": "{label}: śr. {avg} {unit} (cel {rangeMin}–{rangeMax}) — {class}",
+ "glucoseClassificationTitle": "Glikemia: zakresy referencyjne (ADA 2024 / DDG)",
+ "glucoseRow": "{label}: śr. {avg} {unit} (cel {rangeMin}–{rangeMax}), {class}",
"glucoseInTarget": "w celu",
"glucoseBelowTarget": "poniżej celu",
"glucoseAboveTarget": "powyżej celu",
@@ -287,7 +289,7 @@
"gmi": "Wskaźnik zarządzania glukozą (GMI)",
"eA1c": "Szacowane HbA1c",
"cv": "Zmienność (CV%)",
- "spotCaveat": "To szacunki z pojedynczych pomiarów, nie profil z monitora ciągłego — czytaj je jako kierunek zmian, a nie kliniczny werdykt."
+ "spotCaveat": "To szacunki z pojedynczych pomiarów, nie profil z monitora ciągłego. Czytaj je jako kierunek zmian, a nie kliniczny werdykt."
},
"illnessTitle": "Schorzenia i choroby",
"illnessOngoing": "Trwająca",
@@ -376,12 +378,12 @@
"backToDashboard": "Powrót do panelu"
},
"offlineBanner": {
- "message": "Brak połączenia — zmian nie można zapisać, dopóki nie wrócisz online.",
- "saveFailed": "Nie udało się zapisać — jesteś offline. Spróbuj ponownie, gdy wrócisz online.",
+ "message": "Brak połączenia: zmian nie można zapisać, dopóki nie wrócisz online.",
+ "saveFailed": "Nie udało się zapisać, jesteś offline. Spróbuj ponownie, gdy wrócisz online.",
"showingCached": "Wyświetlane są ostatnio zsynchronizowane dane."
},
"demoBanner": {
- "message": "Tryb demo — zmiany nie są zapisywane."
+ "message": "Tryb demo: zmiany nie są zapisywane."
},
"nav": {
"dashboard": "Panel",
@@ -407,6 +409,7 @@
"mood": "Nastrój",
"coach": "Coach",
"skipToContent": "Przejdź do treści",
+ "breadcrumb": "Jesteś tutaj",
"sidebar": "Pasek boczny",
"mainNavigation": "Nawigacja główna",
"mobileNavigation": "Nawigacja mobilna",
@@ -426,7 +429,9 @@
"workout": "Trening",
"workoutDescription": "Sesja, której nie zapisały twoje urządzenia",
"symptom": "Objaw",
- "symptomDescription": "Jeden z twoich objawów, z nasileniem"
+ "symptomDescription": "Jeden z twoich objawów, z nasileniem",
+ "lifeEventDescription": "Przeprowadzka, narodziny, nowa praca",
+ "lifeEvent": "Wydarzenie życiowe"
},
"cycle": "Cykl",
"labs": "Wyniki badań",
@@ -435,7 +440,8 @@
"illness": "Choroba",
"documents": "Dokumenty",
"vaccinations": "Szczepienia",
- "mentalWellbeing": "Dobrostan psychiczny"
+ "mentalWellbeing": "Dobrostan psychiczny",
+ "timeline": "Oś czasu"
},
"auth": {
"login": "Zaloguj się",
@@ -452,14 +458,13 @@
"hasAccount": "Masz już konto?",
"loginFailed": "Logowanie nie powiodło się",
"registerFailed": "Rejestracja nie powiodła się",
- "invitedBanner": "Otrzymano zaproszenie — utwórz konto, nawet jeśli rejestracja jest zamknięta.",
+ "invitedBanner": "Otrzymano zaproszenie. Utwórz konto, nawet jeśli rejestracja jest zamknięta.",
"alreadySignedIn": {
"title": "Masz już zalogowaną sesję",
"body": "Masz zalogowaną sesję jako {username}. Zaproszenie tworzy nowe konto. Wyloguj się, aby je przyjąć.",
"signOut": "Wyloguj się i przyjmij zaproszenie"
},
"createAccount": "Utwórz konto",
- "newAccount": "Utwórz nowe konto",
"backToPasskey": "Wróć do passkey",
"passkeySetupLater": "Passkey możesz skonfigurować później w ustawieniach.",
"passwordMinLength": "Min. 12 znaków",
@@ -512,6 +517,37 @@
"fixLan": "Serwer w sieci domowej bez TLS: ustaw SESSION_COOKIE_SECURE=false w jego ustawieniach i uruchom go ponownie.",
"fixHttps": "Serwer za proxy HTTPS: otwórz zamiast tego adres https:// i upewnij się, że proxy przekazuje X-Forwarded-Proto: https.",
"localhostNote": "Ta strona działa na http://localhost, a serwer ustawia ciasteczko logowania tylko dla https://. Większość przeglądarek akceptuje to na localhost; jeśli po zalogowaniu wracasz tutaj, ustaw SESSION_COOKIE_SECURE=false."
+ },
+ "claim": {
+ "title": "Przejmij swoją kartę zdrowia",
+ "titleNamed": "Witaj, {name}",
+ "description": "Do tej pory ktoś prowadził Twoją kartę zdrowia za Ciebie. Wybierz sposób logowania, a stanie się ona Twoim własnym kontem. Nic nie jest kopiowane ani przenoszone.",
+ "loading": "Sprawdzanie linku…",
+ "invalid": "Ten link już nie działa",
+ "invalidHint": "Jest nieznany, wygasł, został wycofany albo już użyty. Poproś osobę, która go wysłała, o nowy.",
+ "oidcOnly": "Ten serwer pozwala tylko na logowanie jednokrotne (SSO), więc profilu nie można tu jeszcze przejąć.",
+ "signedInTitle": "Najpierw się wyloguj",
+ "signedInBody": "Masz zalogowaną sesję jako {username}. Ten link tworzy logowanie dla kogoś innego, więc wyloguj się, zanim go użyjesz.",
+ "signOut": "Wyloguj i kontynuuj",
+ "guardiansTitle": "Kto na razie zachowuje dostęp",
+ "guardiansHint": "Zaproponowane przez osoby, które prowadziły Twoją kartę. Zdecydujesz zaraz po zalogowaniu.",
+ "guardianUnnamed": "Opiekun",
+ "usernameHint": "Od 3 do 30 liter, cyfr, _ lub -.",
+ "usernameReserved": "Nazwy użytkownika zaczynające się od managed- są zarezerwowane.",
+ "emailHint": "Wymagany, żebyś mógł wrócić, jeśli zapomnisz hasła.",
+ "consentNote": "Zastrzeżenie i każde użycie AI potwierdzasz sam. Do tego czasu nic z Twojej karty nie trafia do dostawcy AI.",
+ "expires": "Ten link jest ważny do {date}.",
+ "submit": "Przejmij",
+ "submitting": "Przejmowanie…",
+ "errorTaken": "Ta nazwa użytkownika lub adres e-mail jest już zajęty.",
+ "errorRateLimit": "Zbyt wiele prób. Odczekaj kilka minut i spróbuj ponownie.",
+ "errorFailed": "Nie udało się. Spróbuj ponownie za chwilę.",
+ "errorOffline": "Nie udało się połączyć z serwerem. Sprawdź połączenie i spróbuj ponownie."
+ },
+ "inviteInvalid": {
+ "title": "Nieprawidłowe zaproszenie",
+ "description": "Ten link z zaproszeniem jest niekompletny lub został źle skopiowany. Poproś osobę, która cię zaprosiła, o nowy link.",
+ "toLogin": "Przejdź do logowania"
}
},
"dashboard": {
@@ -543,17 +579,17 @@
"sleepShort": "Sen",
"stepsShort": "Kroki",
"vo2MaxShort": "VO₂ max",
- "bpInTargetWindow90": "RR-cel · 90 d",
- "bpInTargetWindowDynamic": "RR-cel · {days} d",
+ "bpInTargetWindow90": "RR-cel (90 d)",
+ "bpInTargetWindowDynamic": "RR-cel ({days} d)",
"bpInTargetCollecting": "zbieranie danych",
"medications": "Leki",
"quickAddMeasurement": "Zarejestruj pomiar",
"quickAddMood": "Zarejestruj nastrój",
"quickAddMedicationIntake": "Zarejestruj przyjęcie",
"quickAddWorkout": "Zarejestruj trening",
+ "quickAddLifeEvent": "Zapisz wydarzenie życiowe",
"customizeDashboard": "Dostosuj pulpit",
"medicationIntakeQuickAdd": {
- "sheetTitle": "Zarejestruj przyjęcie leku",
"sheetDescription": "Zapisz dawkę, godzinę i lek w kilku stuknięciach.",
"medicationLabel": "Lek",
"medicationPlaceholder": "Wybierz lek",
@@ -666,7 +702,8 @@
"body": "Bez kopii klucza Twoich danych nie da się odzyskać po ponownej instalacji. To zajmie minutę.",
"action": "Otwórz krok"
},
- "quickAddSymptom": "Zapisz objaw"
+ "quickAddSymptom": "Zapisz objaw",
+ "todayCardVisible": "Pokaż górną kartę"
},
"measurements": {
"title": "Pomiary",
@@ -738,7 +775,7 @@
"formReset": "Resetuj formularz",
"saveError": "Błąd zapisu",
"duplicateTimestamp": "Pomiar tego typu już istnieje dla wybranej daty.",
- "serverOwnedSource": "Wartości z połączonego źródła nie można edytować — znacznik czasu i notatka pozostają edytowalne.",
+ "serverOwnedSource": "Wartości z połączonego źródła nie można edytować. Znacznik czasu i notatka pozostają edytowalne.",
"deleteError": "Błąd usuwania",
"deletedToast": "Pomiar usunięty",
"restoredToast": "Pomiar przywrócony",
@@ -833,11 +870,12 @@
"durationHoursOnly": "{hours} godz.",
"durationMinutesOnly": "{minutes} min",
"durationInvalid": "Wpisz czas trwania, np. 7,5, 7:30 lub 7h 30m.",
- "durationTooLong": "Jeden wpis nie może przekraczać 24 godzin."
+ "durationTooLong": "Jeden wpis nie może przekraczać 24 godzin.",
+ "sourceHealthConnect": "Health Connect"
},
"metricCatalog": {
"title": "Co HealthLog potrafi śledzić",
- "description": "Wszystkie obszary metryk obsługiwane przez HealthLog — również te, dla których nie masz jeszcze danych — oraz co je dostarcza.",
+ "description": "Wszystkie obszary metryk obsługiwane przez HealthLog (również te, dla których nie masz jeszcze danych) oraz co je dostarcza.",
"statusPresent": "Śledzone",
"statusAbsent": "Jeszcze nieśledzone",
"statusModuleOff": "Wyłączone",
@@ -845,11 +883,11 @@
"connectCta": "Połącz źródło",
"viewCta": "Zobacz",
"ecg": {
- "source": "Jednoodprowadzeniowe zapisy EKG z kompatybilnego zegarka — obecnie Withings ScanWatch — synchronizowane automatycznie."
+ "source": "Jednoodprowadzeniowe zapisy EKG z kompatybilnego zegarka (obecnie Withings ScanWatch), synchronizowane automatycznie."
},
"watchdog": {
"title": "Automatyczny nadzór",
- "description": "HealthLog dyskretnie obserwuje też dane w tle — powiadomienia o nieregularnym rytmie i zaburzeniach oddychania z podłączonego zegarka, alerty o odchyleniu od Twojej wartości bazowej oraz zauważalne zmiany między dwoma ostatnimi panelami badań laboratoryjnych — i pokazuje kartę tylko wtedy, gdy coś zwraca uwagę. Jeśli podłączone jest co najmniej jedno z tych źródeł, brak karty to dobry znak, a nie brakująca funkcja; jeśli żadne nie jest jeszcze podłączone, po prostu nic nie jest jeszcze monitorowane."
+ "description": "HealthLog dyskretnie obserwuje też dane w tle (powiadomienia o nieregularnym rytmie i zaburzeniach oddychania z podłączonego zegarka, alerty o odchyleniu od Twojej wartości bazowej oraz zauważalne zmiany między dwoma ostatnimi panelami badań laboratoryjnych) i pokazuje kartę tylko wtedy, gdy coś zwraca uwagę. Jeśli podłączone jest co najmniej jedno z tych źródeł, brak karty to dobry znak, a nie brakująca funkcja; jeśli żadne nie jest jeszcze podłączone, po prostu nic nie jest jeszcze monitorowane."
}
},
"mood": {
@@ -1006,7 +1044,7 @@
"archive": "Archiwizuj",
"restore": "Przywróć",
"archivedTitle": "Zarchiwizowane tagi",
- "archivedDescription": "Zarchiwizowane tagi pozostają w Twoich wcześniejszych wpisach — znikają tylko z wyboru.",
+ "archivedDescription": "Zarchiwizowane tagi pozostają w Twoich wcześniejszych wpisach, znikają tylko z wyboru.",
"archivedEmpty": "Nic nie zarchiwizowano.",
"purgeTitle": "Usunąć tag na zawsze?",
"purgeBody": "Tag zostanie usunięty z {count} wcześniejszych wpisów. Tej operacji nie można cofnąć.",
@@ -1031,7 +1069,7 @@
},
"sectionMoreTags": "Więcej tagów",
"sectionNoteDetails": "Notatka i szczegóły",
- "quickTagsHelp": "Twoje częste tagi — jedno dotknięcie",
+ "quickTagsHelp": "Twoje częste tagi: jedno dotknięcie",
"quickTagsExpand": "Więcej",
"loadError": "Nie udało się załadować wpisów nastroju.",
"sectionLevelA": "Samopoczucie",
@@ -1191,7 +1229,8 @@
"bodyLogged": "Zapisano w dzienniku choroby: {count} objawów",
"bodyNotLogged": "Na ten dzień nic nie zapisano w dzienniku choroby",
"openIllness": "Otwórz"
- }
+ },
+ "sourceNotification": "Powiadomienie"
},
"medications": {
"title": "Leki",
@@ -1209,8 +1248,8 @@
"dialogDescription": "Te leki są teraz do przyjęcia. Potwierdź, aby zarejestrować je wszystkie jako przyjęte.",
"confirm": "Przyjmij wszystkie ({count})",
"successToast": "Zarejestrowano przyjęcia: {count}",
- "partialToast": "Zarejestrowano: {taken}, nieudane: {failed} – spróbuj ponownie",
- "failedToast": "Nie zarejestrowano żadnego przyjęcia – spróbuj ponownie"
+ "partialToast": "Zarejestrowano: {taken}, nieudane: {failed}. Spróbuj ponownie",
+ "failedToast": "Nie zarejestrowano żadnego przyjęcia. Spróbuj ponownie"
},
"logIntake": {
"dialogTitle": "Zarejestruj przyjęcie",
@@ -1284,6 +1323,14 @@
"disabledToast": "Rejestrowanie przyjęć wyłączone",
"toggleFailed": "Nie udało się zmienić rejestrowania przyjęć."
},
+ "autoMarkTaken": {
+ "label": "Zapisuj przyjęcia automatycznie",
+ "helper": "HealthLog zapisuje każdą zaplanowaną dawkę jako przyjętą o jej porze. Nadal możesz ręcznie zmienić lub pominąć dawkę. Przypomnienia nie są wysyłane.",
+ "cardHint": "Zapisywane automatycznie",
+ "enabledToast": "Przyjęcia są teraz zapisywane automatycznie",
+ "disabledToast": "Przyjęcia nie są już zapisywane automatycznie",
+ "toggleFailed": "Nie udało się zmienić automatycznego zapisu."
+ },
"withoutNotification": "Bez powiadomienia",
"pausedSince": "Wstrzymane od",
"takeNow": "Przyjmij teraz",
@@ -1299,7 +1346,7 @@
"complianceInfoLabel": "Co oznacza przestrzeganie zaleceń?",
"lastIntake": "Ostatnie przyjęcie:",
"nextIntake": "Następne przyjęcie:",
- "nextIntakeOverdue": "Zaległa · {time} — wciąż można przyjąć",
+ "nextIntakeOverdue": "Zaległa od {time}, wciąż można przyjąć",
"dayStreak": "dni serii",
"dayStreakOne": "dzień serii",
"cycleDueToday": "Termin dzisiaj",
@@ -1308,10 +1355,10 @@
"skipped": "Pominięte",
"intakeToastTaken": "{name} oznaczone jako przyjęte",
"intakeToastSkipped": "{name} pominięte",
- "intakeToastFailed": "Nie udało się zapisać {name} — spróbuj ponownie",
+ "intakeToastFailed": "Nie udało się zapisać {name}. Spróbuj ponownie",
"intakeUndo": "Cofnij",
"intakeUndone": "Wpis cofnięty",
- "intakeUndoFailed": "Nie udało się cofnąć — popraw w historii",
+ "intakeUndoFailed": "Nie udało się cofnąć. Popraw w historii",
"glp1LastInjection": "{label}",
"glp1NextInjectionDays": "{label} (za {days} dni)",
"glp1NextInjectionToday": "dziś",
@@ -1393,10 +1440,10 @@
"error": {
"empty": "Najpierw wpisz opis.",
"tooLong": "Utrzymuj opis poniżej 2000 znaków.",
- "rateLimit": "Zbyt wiele żądań — poczekaj chwilę i spróbuj ponownie.",
- "noProvider": "Brak skonfigurowanego dostawcy AI — skonfiguruj go w Ustawieniach.",
- "budget": "Osiągnięto dzienny limit AI — spróbuj jutro.",
- "network": "Nie udało się połączyć z dostawcą AI — spróbuj ponownie.",
+ "rateLimit": "Zbyt wiele żądań. Poczekaj chwilę i spróbuj ponownie.",
+ "noProvider": "Brak skonfigurowanego dostawcy AI. Skonfiguruj go w Ustawieniach.",
+ "budget": "Osiągnięto dzienny limit AI. Spróbuj jutro.",
+ "network": "Nie udało się połączyć z dostawcą AI. Spróbuj ponownie.",
"consent": "Odczyt Twojego opisu przez AI wymaga najpierw Twojej zgody.",
"unavailable": "Odczyt opisów przez AI jest tu teraz niedostępny."
}
@@ -1412,7 +1459,7 @@
"everyNMonths": "Co N miesięcy w określonym dniu",
"yearly": "Raz w roku",
"rolling": "Co N dni od ostatniego przyjęcia (elastycznie)",
- "rollingExplainer": "Liczy od ostatniego zarejestrowanego przyjęcia — wstrzymuje się, jeśli pominiesz dawkę.",
+ "rollingExplainer": "Liczy od ostatniego zarejestrowanego przyjęcia i wstrzymuje się, jeśli pominiesz dawkę.",
"oneShot": "Dawka jednorazowa"
},
"weekdays": {
@@ -1470,7 +1517,7 @@
"night": "Noc"
},
"max": {
- "reached": "Osiągnięto maksimum — usuń jedną godzinę przed dodaniem kolejnej."
+ "reached": "Osiągnięto maksimum. Usuń jedną godzinę przed dodaniem kolejnej."
}
},
"courseWindow": {
@@ -1520,7 +1567,7 @@
},
"step3": {
"title": "Jaka jest dawka?",
- "subline": "Tabletka, wziew, kropla — w zależności od formy.",
+ "subline": "Tabletka, wziew, kropla: w zależności od formy.",
"amountLabel": "Ilość",
"amountPlaceholder": "np. 5",
"unitLabel": "Jednostka",
@@ -1546,7 +1593,7 @@
},
"dosesPerUnitLabel": "Dawki na jednostkę",
"dosesPerUnitPlaceholder": "np. 4",
- "dosesPerUnitHint": "Dawki na wstrzykiwacz / fiolkę — zostaw puste, aby nie śledzić zapasów.",
+ "dosesPerUnitHint": "Dawki na wstrzykiwacz / fiolkę. Zostaw puste, aby nie śledzić zapasów.",
"short": "Dawka",
"unitsPerDoseLabel": "Jednostki na dawkę",
"unitsPerDoseHint": "np. 2 tabletki po 2 mg na dawkę 4 mg lub ½ tabletki przy dzielonej dawce"
@@ -1558,7 +1605,7 @@
},
"step5": {
"title": "Jak często go przyjmujesz?",
- "subline": "Wybierz częstotliwość — szczegóły w następnym kroku.",
+ "subline": "Wybierz częstotliwość, szczegóły w następnym kroku.",
"short": "Częstotliwość"
},
"step6": {
@@ -1580,7 +1627,7 @@
},
"step7": {
"title": "Kiedy go przyjmujesz?",
- "subline": "Jedna lub więcej pór dnia — przypomnienia opierają się na tej liście.",
+ "subline": "Jedna lub więcej pór dnia. Przypomnienia opierają się na tej liście.",
"presetsLabel": "Ustawienia wstępne pory dnia",
"presets": {
"morning": "Rano",
@@ -1588,7 +1635,7 @@
"evening": "Wieczór",
"night": "Noc"
},
- "overrideHint": "Opcjonalnie — ustaw inną dawkę lub liczbę jednostek tylko dla tej pory. Pozostaw puste, aby użyć wartości domyślnej leku.",
+ "overrideHint": "Opcjonalnie: ustaw inną dawkę lub liczbę jednostek tylko dla tej pory. Pozostaw puste, aby użyć wartości domyślnej leku.",
"doseOverrideLabel": "Dawka dla tej pory (opcjonalnie)",
"doseOverridePlaceholder": "np. pół tabletki",
"unitsOverrideLabel": "Jednostki zużyte o tej porze",
@@ -1596,11 +1643,11 @@
"short": "Godziny"
},
"step8": {
- "title": "Gotowe — czy HealthLog ma Ci przypominać?",
+ "title": "Gotowe. Czy HealthLog ma Ci przypominać?",
"subline": "Sprawdź wszystko poniżej przed zapisaniem.",
"remindersLabel": "Przypomnienia włączone",
- "remindersDescription": "Push, Telegram lub ntfy — w zależności od ustawień.",
- "noChannelHint": "Przypomnienia wymagają kanału powiadomień — obecnie żaden nie jest aktywny.",
+ "remindersDescription": "Push, Telegram lub ntfy, w zależności od ustawień.",
+ "noChannelHint": "Przypomnienia wymagają kanału powiadomień, a obecnie żaden nie jest aktywny.",
"noChannelHintLink": "Skonfiguruj powiadomienia",
"short": "Gotowe"
}
@@ -1649,7 +1696,7 @@
},
"asNeeded": {
"label": "W razie potrzeby",
- "description": "Bez stałego planu — zapisz przyjęcie, gdy go potrzebujesz (np. lek przeciwbólowy)."
+ "description": "Bez stałego planu: zapisz przyjęcie, gdy go potrzebujesz (np. lek przeciwbólowy)."
}
},
"summary": {
@@ -1684,7 +1731,7 @@
}
},
"errors": {
- "submitFailed": "Nie udało się zapisać leku — spróbuj ponownie."
+ "submitFailed": "Nie udało się zapisać leku. Spróbuj ponownie."
}
},
"doseStrength": {
@@ -1700,11 +1747,11 @@
"empty": "Nie zarejestrowano jeszcze kroków zwiększania dawki. Dodaj datowane zmiany dawki, aby nakreślić plan.",
"youAreHere": "Jesteś tutaj",
"doseStep": "{dose} {unit}",
- "plannedFor": "Zaplanowano · {date}"
+ "plannedFor": "Zaplanowano na {date}"
},
"injectionSiteBodyOutlineAriaLabel": "Sylwetka ciała",
"injectionSitePickerAriaLabel": "Wybór miejsca iniekcji",
- "injectionSiteRecommendedAriaLabel": "{site} — zalecane następne miejsce",
+ "injectionSiteRecommendedAriaLabel": "{site}, zalecane następne miejsce",
"injectionSiteLegendAriaLabel": "Legenda oznaczeń",
"injectionSiteLegendRecommended": "Zalecane następne miejsce",
"injectionSiteLegendLastUsed": "Ostatnio użyte miejsce",
@@ -1740,6 +1787,7 @@
"sourceImport": "Import",
"sourceAppleHealth": "Apple Health",
"intakeSourceVia": "przez {origin}",
+ "intakeRecordedAutomatically": "Zapisano automatycznie",
"intakeHistoryEmptyTitle": "Brak przyjęć",
"intakeHistoryEmptyDescription": "Gdy tylko zarejestrujesz przyjęcie, pojawi się tu wraz z godziną i źródłem.",
"intakeHistoryEmptyAction": "Zarejestruj przyjęcie",
@@ -1751,7 +1799,7 @@
"intakeHistoryStatusSkipped": "Pominięty",
"intakeHistorySortByTaken": "Sortuj wg czasu przyjęcia",
"intakeHistorySortByScheduled": "Sortuj wg czasu zaplanowanego",
- "intakeHistoryPageInfo": "Strona {page} z {total} · {count} wpisów",
+ "intakeHistoryPageInfo": "Strona {page} z {total}, {count} wpisów",
"intakeHistoryPrev": "Poprzednia",
"intakeHistoryNext": "Następna",
"glp1": {
@@ -1792,7 +1840,7 @@
"cadence": {
"oneShotOn": "Dawka jednorazowa w dniu {date}.",
"oneShotPending": "Dawka jednorazowa oczekuje.",
- "asNeeded": "W razie potrzeby — bez stałego planu."
+ "asNeeded": "W razie potrzeby, bez stałego planu."
},
"intake": {
"title": "Historia przyjęć",
@@ -1819,7 +1867,7 @@
"dialogTitle": "Edytuj przyjęcie",
"takenAtLabel": "Przyjęto o",
"scheduledForHint": "Zaplanowano: {dateTime}",
- "farFromScheduledWarning": "Znacznie odbiega od zaplanowanego terminu — sprawdź datę.",
+ "farFromScheduledWarning": "Znacznie odbiega od zaplanowanego terminu. Sprawdź datę.",
"skippedLabel": "Pominięto",
"save": "Zapisz",
"cancel": "Anuluj",
@@ -1851,7 +1899,7 @@
"markAsTaken": "Oznacz jako przyjętą",
"markAsSkipped": "Oznacz jako pominiętą",
"adHocTag": "poza planem",
- "takenDetail": "planowo {planned} · przyjęto {taken}",
+ "takenDetail": "planowo {planned}, przyjęto {taken}",
"doseTaken": "Dawka: {dose}",
"status": {
"taken_on_time": "na czas",
@@ -1863,10 +1911,10 @@
},
"addDialog": {
"title": "Dodaj przyjęcie",
- "description": "Zarejestruj dawkę — także wstecznie lub poza planem.",
+ "description": "Zarejestruj dawkę, także wstecznie lub poza planem.",
"takenAtLabel": "Przyjęto o",
"skippedLabel": "Pominięte",
- "nudgeHint": "Blisko slotu {slot} — przy zapisie zapytamy, czy przypisać.",
+ "nudgeHint": "Blisko slotu {slot}. Przy zapisie zapytamy, czy przypisać.",
"cancel": "Anuluj",
"submit": "Zapisz"
},
@@ -1915,7 +1963,7 @@
},
"codes": {
"label": "Kody kliniczne (ATC / RxNorm)",
- "note": "Opcjonalne. Używane w eksporcie FHIR dokumentacji zdrowotnej. Kody są zapisywane dokładnie tak, jak wprowadzone — nigdy nie są zgadywane.",
+ "note": "Opcjonalne. Używane w eksporcie FHIR dokumentacji zdrowotnej. Kody są zapisywane dokładnie tak, jak wprowadzone, i nigdy nie są zgadywane.",
"atcLabel": "ATC",
"rxNormLabel": "RxNorm",
"saved": "Kody kliniczne zapisane",
@@ -1935,7 +1983,7 @@
"title": "Zakończ lek",
"helper": "Zatrzymuje przypomnienia i oznacza lek jako zakończony. Historia pozostaje widoczna.",
"button": "Zakończ",
- "dialogTitle": "Zakończ {name} — zatrzymuje przypomnienia, historia pozostaje widoczna",
+ "dialogTitle": "Zakończ {name}: zatrzymuje przypomnienia, historia pozostaje widoczna",
"dialogBody": "Brak kolejnych przypomnień. Stare wpisy pozostają na osi czasu.",
"toast": "Lek zakończony",
"failed": "Nie udało się zakończyć leku."
@@ -1973,7 +2021,7 @@
"copyFailed": "Nie udało się skopiować.",
"tokenCopied": "Token skopiowany",
"copyToken": "Kopiuj token",
- "mintedHint": "Skopiuj ten token teraz — jest pokazywany tylko raz."
+ "mintedHint": "Skopiuj ten token teraz. Jest pokazywany tylko raz."
},
"advanced": {
"dataPortability": {
@@ -2002,7 +2050,6 @@
"bestand": "Zapas",
"verlauf": "Historia",
"injektion": "Iniekcja",
- "api": "API",
"erweitert": "Zaawansowane",
"wirkung": "Działanie"
},
@@ -2014,9 +2061,9 @@
"statusTitle": "Status",
"nextDoseLabel": "Następna dawka",
"nextDoseNone": "Brak nadchodzącej dawki.",
- "nextDoseAsNeeded": "W razie potrzeby — brak zaplanowanej dawki.",
+ "nextDoseAsNeeded": "W razie potrzeby, brak zaplanowanej dawki.",
"nextDoseNotTracked": "Przyjęcia nie są rejestrowane, tylko zapis.",
- "pausedHint": "Wstrzymano — brak przypomnień.",
+ "pausedHint": "Wstrzymano, brak przypomnień.",
"reminderLabel": "Przypomnienie",
"reminderOn": "Włączone",
"reminderOff": "Wyłączone",
@@ -2087,7 +2134,7 @@
"packagingDefaultPack": "Domyślny rozmiar opakowania: {units} jedn.",
"packagingNoDefaultPack": "Brak domyślnego rozmiaru opakowania",
"packagingEdit": "Edytuj",
- "packagingDescription": "Określ, ile jednostek zużywa jedna dawka i jaki jest typowy rozmiar nowego opakowania — np. po zmianie producenta z innym blistrem.",
+ "packagingDescription": "Określ, ile jednostek zużywa jedna dawka i jaki jest typowy rozmiar nowego opakowania, np. po zmianie producenta z innym blistrem.",
"packagingUnitsPerDoseLabel": "Jednostki na dawkę",
"packagingUnitsPerDoseHelper": "Np. dwie tabletki 2 mg na dawkę 4 mg lub 1,5 dla półtorej (powyżej 0, do 100). Zarejestrowane dawki pozostają bez zmian.",
"packagingDefaultPackLabel": "Domyślny rozmiar opakowania (jednostki)",
@@ -2138,11 +2185,11 @@
"hideEras": "Ukryj wcześniejsze okresy",
"addEra": "Dodaj wcześniejszy okres",
"addEraTitle": "Dodaj wcześniejszy okres",
- "addEraDescription": "Zapisz, o których godzinach ten lek był wcześniej przyjmowany — np. zanim zmieniono go tutaj.",
+ "addEraDescription": "Zapisz, o których godzinach ten lek był wcześniej przyjmowany, np. zanim zmieniono go tutaj.",
"editAria": "Edytuj okres",
"editEraTitle": "Edytuj okres",
"editEraDescription": "Dostosuj godziny przyjmowania i zakres dat tego okresu.",
- "archivedEditHint": "Ten okres został zapisany automatycznie — Twoja zmiana koryguje zarejestrowaną historię; oryginał pozostaje wewnętrznie jako zapis.",
+ "archivedEditHint": "Ten okres został zapisany automatycznie. Twoja zmiana koryguje zarejestrowaną historię; oryginał pozostaje wewnętrznie jako zapis.",
"updatedToast": "Okres zaktualizowany",
"updateError": "Nie udało się zaktualizować okresu. Nie może nakładać się na inne okresy i musi kończyć się przed aktualnym planem.",
"timesLabel": "Godziny przyjmowania",
@@ -2168,7 +2215,7 @@
"saved": "Zapisano harmonogram",
"failed": "Nie udało się zapisać harmonogramu",
"window": {
- "intro": "Dla każdej dawki ustal, kiedy przyjęcie jest jeszcze punktualne — dokładna godzina (±1 h) lub własne okno, np. 07:00–09:00.",
+ "intro": "Dla każdej dawki ustal, kiedy przyjęcie jest jeszcze punktualne: dokładna godzina (±1 h) lub własne okno, np. 07:00–09:00.",
"explicitToggle": "Własne okno",
"startLabel": "Początek okna",
"endLabel": "Koniec okna",
@@ -2181,10 +2228,10 @@
}
},
"logInjectionSiteTitle": "Zarejestruj miejsce iniekcji",
- "logInjectionSiteDescription": "Gdzie wstrzyknięto {name}? Opcjonalnie — możesz to pominąć.",
+ "logInjectionSiteDescription": "Gdzie wstrzyknięto {name}? Opcjonalnie, możesz to pominąć.",
"logInjectionSiteSkip": "Pomiń",
"logInjectionSiteConfirm": "Zapisz miejsce",
- "logInjectionSiteNoneAvailable": "Brak dostępnego miejsca iniekcji — wszystkie miejsca są wykluczone w Twoich ustawieniach.",
+ "logInjectionSiteNoneAvailable": "Brak dostępnego miejsca iniekcji: wszystkie miejsca są wykluczone w Twoich ustawieniach.",
"logInjectionSiteSaveFailed": "Nie udało się zapisać miejsca iniekcji. Spróbuj ponownie.",
"perSlotUnits": "{units} na dawkę",
"trackInjectionSitesToggle": "Rejestruj miejsca iniekcji",
@@ -2209,16 +2256,15 @@
"tableStockDoses": "Dawki: {count}",
"tableStockRunway": "≈ {days} dni zapasu",
"cardLowStockRunway": "Zapas: ≈ {days} dni",
- "takeActionFor": "Przyjmij – {name}",
- "skipActionFor": "Pomiń – {name}",
- "reorderTitle": "Zmień kolejność",
- "reorderDescription": "Ustal kolejność swoich leków – obowiązuje w widoku kart i w tabeli.",
+ "takeActionFor": "Przyjmij: {name}",
+ "skipActionFor": "Pomiń: {name}",
+ "reorderDescription": "Ustal kolejność swoich leków w widoku kart i w tabeli.",
"reorderSaved": "Kolejność zapisana",
"reorderSaveFailed": "Nie udało się zapisać kolejności",
"customize": "Dostosuj leki",
"efficacy": {
"title": "Działanie",
- "intro": "Jak zmieniały się pomiary, na które ma wpływać ten lek, w okolicach rozpoczęcia jego przyjmowania. Pokazuje to, co zbiega się w czasie — powiązanie, nie przyczynę — i nigdy porady dotyczącej leczenia ani dawki.",
+ "intro": "Jak zmieniały się pomiary, na które ma wpływać ten lek, w okolicach rozpoczęcia jego przyjmowania. Pokazuje to, co zbiega się w czasie (powiązanie, nie przyczynę), i nigdy porady dotyczącej leczenia ani dawki.",
"notEligible": "Brak pomiaru, który można powiązać z tym lekiem.",
"startFallback": "Nie ustawiono daty rozpoczęcia, więc porównanie używa pierwszego zapisanego pomiaru jako punktu odniesienia.",
"noStart": "Ustaw datę rozpoczęcia, aby porównać pomiary sprzed i po rozpoczęciu.",
@@ -2231,7 +2277,7 @@
"levelShift": "Wykryto zmianę poziomu w okolicach {date}.",
"plateau": {
"title": "Plateau masy ciała przy obecnej dawce",
- "evidence": "Zmiana masy ciała o {delta} {unit} w ciągu ostatnich {window} dni ({readings} pomiarów) — {days} dni na dawce {dose}."
+ "evidence": "Zmiana masy ciała o {delta} {unit} w ciągu ostatnich {window} dni ({readings} pomiarów), {days} dni na dawce {dose}."
},
"retarget": {
"label": "To nie ten pomiar?",
@@ -2241,16 +2287,16 @@
"error": "Nie udało się zapisać tej zmiany. Spróbuj ponownie."
},
"beforeAfter": {
- "summary": "Ø {before} przedtem → Ø {after} od tego czasu — zmiana o {delta}.",
+ "summary": "Ø {before} przedtem → Ø {after} od tego czasu, zmiana o {delta}.",
"counts": "Na podstawie {beforeCount} pomiarów przedtem i {afterCount} od tego czasu.",
- "needBefore": "Za mało pomiarów przed rozpoczęciem — do porównania potrzeba około {weeks} tygodni przed i po.",
- "needAfter": "Za mało pomiarów od rozpoczęcia — do porównania potrzeba około {weeks} tygodni przed i po.",
+ "needBefore": "Za mało pomiarów przed rozpoczęciem. Do porównania potrzeba około {weeks} tygodni przed i po.",
+ "needAfter": "Za mało pomiarów od rozpoczęcia. Do porównania potrzeba około {weeks} tygodni przed i po.",
"needStart": "Ustaw datę rozpoczęcia, aby porównać pomiary sprzed i po rozpoczęciu.",
- "needData": "Za mało pomiarów — do porównania potrzeba około {weeks} tygodni przed i po rozpoczęciu."
+ "needData": "Za mało pomiarów. Do porównania potrzeba około {weeks} tygodni przed i po rozpoczęciu."
},
"insights": {
"title": "Działanie leków",
- "subtitle": "Co zbiega się w czasie z pomiarami, na które celują Twoje leki — tylko powiązanie, nigdy werdykt.",
+ "subtitle": "Co zbiega się w czasie z pomiarami, na które celują Twoje leki: tylko powiązanie, nigdy werdykt.",
"link": "Otwórz widok działania",
"empty": "Za mało danych, aby powiązać Twoje leki z docelowymi pomiarami."
},
@@ -2284,7 +2330,7 @@
"reasonMissingMedication": "wiersz nie nazywa żadnego leku",
"reasonUnreadableDosage": "dawka nie jest liczbą",
"reasonUnreadableRow": "wiersz ma mniej kolumn niż nagłówek",
- "reasonStatusNoDoseInformation": "plik nie zapisuje nic o tej dawce — nikt nie odpowiedział i nic nie zostało zanotowane",
+ "reasonStatusNoDoseInformation": "plik nie zapisuje nic o tej dawce: nikt nie odpowiedział i nic nie zostało zanotowane",
"reasonStatusReminderEvent": "wiersz dotyczy odłożonego przypomnienia, co nic nie mówi o dawce",
"reasonStatusNotificationNotSent": "wiersz zapisuje, że nie wysłano powiadomienia, co dotyczy aplikacji, a nie dawki"
},
@@ -2356,7 +2402,7 @@
},
"charts": {
"loadingLabel": "Ładowanie wykresu…",
- "loadingSlowHint": "Obliczanie analiz — to może chwilę potrwać.",
+ "loadingSlowHint": "Obliczanie analiz, to może chwilę potrwać.",
"weight": "Waga",
"bloodPressure": "Ciśnienie tętnicze",
"pulse": "Tętno",
@@ -2378,6 +2424,7 @@
"days7Label": "7 dni",
"days7Title": "Ostatnie 7 dni",
"days30Label": "30 dni",
+ "daysNLabel": "{days} dni",
"days30Title": "Ostatnie 30 dni",
"days90Label": "90 dni",
"days90Title": "Ostatnie 90 dni",
@@ -2437,13 +2484,15 @@
"loadErrorTitle": "Nie udało się załadować wykresu",
"loadErrorReload": "Załaduj ponownie",
"sparseDataCaption": "Więcej dni dopełni trend.",
+ "tooltipHourlyMean": "{metric} (średnia z godzin)",
"dataTable": {
"toggle": "Punkty danych ({count})",
"columnDate": "Data",
"noValue": "Brak wartości",
- "captionDay": "Jeden wiersz na dzień dla {metric} — dokładnie te punkty, które rysuje wykres.",
- "captionWeek": "Jeden wiersz na tydzień dla {metric}, średnia z dni tego tygodnia — dokładnie te punkty, które rysuje wykres.",
- "captionMonth": "Jeden wiersz na miesiąc dla {metric}, średnia z dni tego miesiąca — dokładnie te punkty, które rysuje wykres."
+ "captionDay": "Jeden wiersz na dzień dla {metric}: dokładnie te punkty, które rysuje wykres.",
+ "captionWeek": "Jeden wiersz na tydzień dla {metric}, średnia z dni tego tygodnia: dokładnie te punkty, które rysuje wykres.",
+ "captionMonth": "Jeden wiersz na miesiąc dla {metric}, średnia z dni tego miesiąca: dokładnie te punkty, które rysuje wykres.",
+ "hourlyMeanNote": "{metric} to średnia z każdego dnia, w której każda godzina liczy się raz, dzięki czemu trening nie przeważa nad resztą dnia."
},
"personalBaseline": "Twoja średnia",
"deltaVsBaseline": "{delta} vs. twoja średnia",
@@ -2471,7 +2520,8 @@
"moodHeatmapEmpty": "Mapa cieplna nastroju, brak zarejestrowanych dni.",
"complianceHeatmap": "Mapa cieplna przyjmowania leków od {start} do {end}, {days} aktywnych dni.",
"complianceHeatmapEmpty": "Mapa cieplna przyjmowania leków, brak aktywnych dni."
- }
+ },
+ "medicationsEmptyAction": "Dodaj lek"
},
"chart": {
"overlay": {
@@ -2502,8 +2552,8 @@
},
"tooltipPrior": "Poprzedni okres",
"unavailable": {
- "lastMonth": "Porównanie niedostępne — brak danych z poprzedniego miesiąca",
- "lastYear": "Porównanie niedostępne — brak danych z poprzedniego roku"
+ "lastMonth": "Porównanie niedostępne: brak danych z poprzedniego miesiąca",
+ "lastYear": "Porównanie niedostępne: brak danych z poprzedniego roku"
},
"notAvailableForCompliance": "Porównanie niedostępne dla przestrzegania"
},
@@ -2515,7 +2565,9 @@
"dismissError": "Nie udało się odrzucić tego wzorca.",
"restoreError": "Nie udało się przywrócić tego wzorca.",
"loadError": "Nie udało się wczytać wzorców.",
- "empty": "Żaden aktualny wzorzec nie przekroczył progu dowodów."
+ "empty": "Żaden aktualny wzorzec nie przekroczył progu dowodów.",
+ "seasonalNote": "Od wersji 1.42 HealthLog usuwa porę roku i długoterminowy trend, zanim porówna wartości. Zobaczysz mniej zależności, ale te, które zostały, są pewniejsze. Wiele wcześniejszych wzorców wynikało po prostu z kalendarza.",
+ "seasonalNoteDismiss": "Zamknij uwagę"
},
"correlation": {
"direction": {
@@ -2547,29 +2599,37 @@
"ENV_DAYLIGHT": "światło dzienne",
"ENV_PRECIP": "opady",
"ENV_PRESSURE_MEAN": "ciśnienie atmosferyczne",
- "ENV_PRESSURE_DELTA": "dobowa zmiana ciśnienia"
+ "ENV_PRESSURE_DELTA": "dobowa zmiana ciśnienia",
+ "ENV_PM25": "pył drobny (PM2,5)",
+ "ENV_OZONE_8H": "ozon",
+ "ENV_POLLEN_MAX": "pyłki"
},
"daily": {
- "high": "Gdy {behaviour} rośnie, {outcome} następnego dnia zwykle wykazuje tendencję do {direction} w Twoich danych — wzorzec wart obserwacji, nie przyczyna.",
- "moderate": "Gdy {behaviour} rośnie, {outcome} następnego dnia wydaje się wykazywać tendencję do {direction}, na podstawie dostępnych danych — wzorzec wart obserwacji, nie przyczyna.",
- "faint": "Gdy {behaviour} rośnie, w Twoich danych pojawia się co najwyżej słaba wskazówka, że {outcome} następnego dnia wykazuje tendencję do {direction} — zbyt słaba, by na niej polegać, nigdy przyczyna."
+ "high": "Gdy {behaviour} rośnie, {outcome} następnego dnia zwykle wykazuje tendencję do {direction} w Twoich danych. To wzorzec wart obserwacji, nie przyczyna.",
+ "moderate": "Gdy {behaviour} rośnie, {outcome} następnego dnia wydaje się wykazywać tendencję do {direction}, na podstawie dostępnych danych. To wzorzec wart obserwacji, nie przyczyna.",
+ "faint": "Gdy {behaviour} rośnie, w Twoich danych pojawia się co najwyżej słaba wskazówka, że {outcome} następnego dnia wykazuje tendencję do {direction}. To zbyt mało, by na tym polegać, i nigdy nie jest to przyczyna."
},
"lab": {
- "default": "Wyższe wartości {lab} idą w parze z tym, że {outcome} wykazuje tendencję do {direction} w tych samych okresach w Twoich danych — zależność, którą warto obserwować z lekarzem, nigdy przyczyna.",
- "faint": "Wyższe wartości {lab} co najwyżej słabo sugerują, że {outcome} wykazuje tendencję do {direction} w tych samych okresach w Twoich danych — zbyt słabo, by na tym polegać, zależność do omówienia z lekarzem, nigdy przyczyna."
+ "default": "Wyższe wartości {lab} idą w parze z tym, że {outcome} wykazuje tendencję do {direction} w tych samych okresach w Twoich danych, zależność, którą warto obserwować z lekarzem, nigdy przyczyna.",
+ "faint": "Wyższe wartości {lab} co najwyżej słabo sugerują, że {outcome} wykazuje tendencję do {direction} w tych samych okresach w Twoich danych. To zbyt mało, by na tym polegać, i nigdy nie jest to przyczyna, ale warto omówić to z lekarzem."
},
"hypothesis": {
"bpCompliance": {
- "support": "Lepsze przyjmowanie leków idzie w parze z niższymi wartościami skurczowymi — wzorzec wart obserwacji.",
- "surprising": "Przyjmowanie leków i ciśnienie skurczowe zmieniają się razem w Twoich danych — zaskakujące; skonsultuj się z lekarzem, zanim coś zmienisz.",
+ "support": "Lepsze przyjmowanie leków idzie w parze z niższymi wartościami skurczowymi, wzorzec wart obserwacji.",
+ "surprising": "Przyjmowanie leków i ciśnienie skurczowe zmieniają się razem w Twoich danych, co jest zaskakujące. Skonsultuj się z lekarzem, zanim coś zmienisz.",
"none": "Przyjmowanie leków i ciśnienie skurczowe nie zmieniają się wyraźnie razem w tym okresie."
},
"moodPulse": {
- "support": "Dni z gorszym nastrojem idą w parze z wyższym tętnem spoczynkowym — wzorzec wart obserwacji.",
- "surprising": "Dni z lepszym nastrojem idą w parze z wyższym tętnem spoczynkowym w Twoich danych — zaskakujące.",
+ "support": "Dni z gorszym nastrojem idą w parze z wyższym tętnem spoczynkowym, wzorzec wart obserwacji.",
+ "surprising": "Dni z lepszym nastrojem idą w parze z wyższym tętnem spoczynkowym w Twoich danych, co jest zaskakujące.",
"none": "Nastrój i tętno spoczynkowe nie zmieniają się wyraźnie razem w tym okresie."
},
- "weightWeekday": "Waga w {weekday} jest {delta} kg {direction} Twojej średniej z pozostałych dni — wzorzec wart obserwacji."
+ "weightWeekday": "Waga w {weekday} jest {delta} kg {direction} Twojej średniej z pozostałych dni, wzorzec wart obserwacji."
+ },
+ "dailySameDay": {
+ "high": "Więcej {behaviour} tego samego dnia i dzień wcześniej idzie w twoich danych zwykle w parze z {direction} {outcome}. To wzorzec wart obserwacji, nie przyczyna.",
+ "moderate": "Więcej {behaviour} tego samego dnia i dzień wcześniej wydaje się, na podstawie dotychczasowych danych, iść w parze z {direction} {outcome}. To wzorzec wart obserwacji, nie przyczyna.",
+ "faint": "Więcej {behaviour} tego samego dnia i dzień wcześniej pokazuje w twoich danych co najwyżej słaby ślad {direction} {outcome}. To za mało, by na tym polegać, i nigdy nie jest to przyczyna."
}
},
"noAnalysisYet": "Brak oceny.",
@@ -2616,7 +2676,7 @@
"narrativeWeek": "Ten tydzień",
"narrativeMonth": "Ten miesiąc",
"narrativeUpdating": "Aktualizowanie…",
- "heroFallbackSubtitle": "Codzienne spojrzenie na twoje trendy — prosto z liczb, które rejestrujesz.",
+ "heroFallbackSubtitle": "Codzienne spojrzenie na twoje trendy, prosto z liczb, które rejestrujesz.",
"heroGreetingMorning": "Dzień dobry",
"heroGreetingAfternoon": "Dzień dobry",
"heroGreetingEvening": "Dobry wieczór",
@@ -2681,7 +2741,7 @@
"referenceSleep": "Co najmniej {low} minut na noc; dłuższy sen nie obniża wyniku",
"referenceAdiposity": "Stosunek obwodu talii do wzrostu poniżej {high}",
"referenceHba1c": "HbA1c od {low} do {high}%",
- "referenceDetail": "{label} · {source} · {kind} · od {low} do {high}",
+ "referenceDetail": "{label}: od {low} do {high} ({kind}, {source})",
"openBound": "otwarty",
"laboratoryReferenceSource": "Zakresy referencyjne laboratorium",
"personalTargetSource": "Osobisty cel",
@@ -2716,7 +2776,8 @@
"coverage": {
"recommendedMet": "Pokrywa trzy obszary zalecane przez wynik",
"areas": "{present} z {required} obszarów zdrowia"
- }
+ },
+ "historyLink": "Przebieg"
},
"dailyBriefing": {
"title": "Briefing dnia",
@@ -2726,7 +2787,7 @@
"emptyDescription": "Wygeneruj dzisiejszy briefing, aby zobaczyć podsumowanie trendów.",
"emptyAction": "Wygeneruj briefing",
"loadingLabel": "Przygotowanie briefingu",
- "refreshFailedHint": "Nie udało się odświeżyć briefingu — pokazujemy poprzedni.",
+ "refreshFailedHint": "Nie udało się odświeżyć briefingu, pokazujemy poprzedni.",
"retryAction": "Spróbuj ponownie",
"failedTitle": "Nie udało się wygenerować briefingu",
"failedDescription": "Dostawca AI przekroczył limit czasu lub zwrócił błąd. Spróbuj ponownie albo zwiększ limit czasu odpowiedzi w ustawieniach AI dla wolnego modelu lokalnego.",
@@ -2775,7 +2836,7 @@
"experimentCta": "Uruchom 7-dniowy eksperyment",
"experimentTooltip": "Wkrótce",
"emptyTitle": "Potrzeba więcej danych, aby pokazać tę korelację",
- "sourceChip": "na podstawie {n} sparowanych pomiarów · {window}",
+ "sourceChip": "na podstawie {n} sparowanych pomiarów ({window})",
"sourceWindowLast30": "Ostatnie 30 dni",
"confidenceHigh": "Wysoka pewność",
"confidenceModerate": "Średnia pewność",
@@ -2790,13 +2851,14 @@
}
},
"coach": {
- "refusalOutOfScope": "Mogę pomóc tylko w sprawach danych zdrowotnych z twojego logu — pomiarów, leków, nastroju, trendów. Pytań spoza tego zakresu nie podejmuję.",
- "refusalInjection": "Twoja wiadomość zawiera instrukcje, które próbują nadpisać moje wytyczne. Jestem coachem HealthLog i podsumowuję wyłącznie twoje własne dane zdrowotne — przeformułuj pytanie w tych ramach.",
- "errorBudget": "Osiągnięto dzienny limit coacha. Limit resetuje się o północy (UTC).",
+ "refusalOutOfScope": "Mogę pomóc tylko w sprawach danych zdrowotnych z twojego logu: pomiarów, leków, nastroju, trendów. Pytań spoza tego zakresu nie podejmuję.",
+ "refusalInjection": "Twoja wiadomość zawiera instrukcje, które próbują nadpisać moje wytyczne. Jestem coachem HealthLog i podsumowuję wyłącznie twoje własne dane zdrowotne. Przeformułuj pytanie w tych ramach.",
+ "errorBudget": "Osiągnięto dzienny limit coacha. Limit resetuje się o północy w Twojej strefie czasowej.",
"errorProvider": "Coach nie mógł teraz skontaktować się z dostawcą analizy. Spróbuj za chwilę.",
- "errorNoProvider": "Coach czyta Twoje zapisane dane zdrowotne i odpowiada na pytania o nie (to nie porada medyczna). HealthLog potrzebuje do tego dostawcy AI — połącz własny klucz lub model lokalny.",
+ "errorDemo": "W wersji demo Coach nie odpowiada.",
+ "errorNoProvider": "Coach czyta Twoje zapisane dane zdrowotne i odpowiada na pytania o nie (to nie porada medyczna). HealthLog potrzebuje do tego dostawcy AI: połącz własny klucz lub model lokalny.",
"errorNoProviderAction": "Połącz dostawcę AI",
- "errorNetwork": "Brak połączenia z Internetem — spróbuj ponownie, gdy wrócisz online.",
+ "errorNetwork": "Brak połączenia z Internetem. Spróbuj ponownie, gdy wrócisz online.",
"prefsSaveError": "Nie udało się zapisać ustawień Coacha. Spróbuj ponownie.",
"errorCredentialExpired": "Połączenie z dostawcą AI wygasło. Połącz je ponownie w Ustawieniach, aby Coach działał dalej.",
"tagline": "Osobisty coach zdrowia, oparty na twoich danych.",
@@ -2815,7 +2877,7 @@
"thinking": "Myślę…",
"unverifiedFiguresNotice": "Niektórych liczb nie udało się zweryfikować z Twoimi danymi, więc je pominięto.",
"composerPlaceholder": "Zapytaj mnie …",
- "nudgeBubbleLabel": "Nowa wiadomość od trenera — otwórz czat",
+ "nudgeBubbleLabel": "Nowa wiadomość od trenera: otwórz czat",
"fabLabel": "Otwórz trenera",
"readAloud": "Czytaj na głos",
"readAloudStop": "Zatrzymaj czytanie",
@@ -2833,14 +2895,13 @@
},
"rememberMessage": {
"action": "Zapamiętaj",
- "done": "Zapisano w informacjach o sobie",
- "duplicate": "Już znajduje się w informacjach o sobie",
- "failed": "Nie udało się zapisać. Spróbuj ponownie."
+ "failed": "Nie udało się zapisać. Spróbuj ponownie.",
+ "alreadyKept": "Coach już to pamięta."
},
"guided": {
"offerTitle": "Coach ma {count} pytania do Ciebie",
"offerTitleOne": "Coach ma pytanie do Ciebie",
- "offerBody": "Krótkie odpowiedzi doprecyzują Twoje informacje o sobie — jedno pytanie po drugim, tutaj na czacie.",
+ "offerBody": "Krótkie odpowiedzi doprecyzują Twoje informacje o sobie: jedno pytanie po drugim, tutaj na czacie.",
"offerStart": "Odpowiedz teraz",
"offerLater": "Później",
"offerDismiss": "Nie pytaj więcej",
@@ -2850,7 +2911,7 @@
"later": "Później",
"dismissRemaining": "Nie pytaj więcej",
"answerPlaceholder": "Twoja odpowiedź…",
- "summaryTitle": "Gotowe — dzięki za odpowiedzi!",
+ "summaryTitle": "Gotowe, dzięki za odpowiedzi!",
"summaryAnswered": "odpowiedziano na {answered} z {total}",
"summaryAdopted": "{adopted} dodano do informacji o sobie",
"summaryAdoptedNone": "nie dodano nic nowego do informacji o sobie",
@@ -2862,6 +2923,7 @@
"historyEmpty": "Twoje rozmowy pojawią się tutaj. Zadaj pytanie, aby zacząć.",
"historyDeleteAria": "Usuń rozmowę",
"historyDeleted": "Rozmowa usunięta",
+ "historyDeleteFailed": "Nie udało się usunąć rozmowy.",
"rename": {
"action": "Zmień nazwę rozmowy",
"input": "Tytuł rozmowy",
@@ -2875,7 +2937,6 @@
"sourcesFresh": "Aktualne",
"sourcesStale": "Nieaktualne",
"sourcesFooter": "Coach czyta tylko dane, które podłączyłeś.",
- "evidenceLabel": "Na czym to się opiera?",
"metric": {
"bp": "Ciśnienie krwi",
"weight": "Waga",
@@ -2921,14 +2982,6 @@
"skin_temp": "Temperatura skóry",
"workouts": "Treningi"
},
- "window": {
- "last7days": "Ostatnie 7 dni",
- "last30days": "Ostatnie 30 dni",
- "last90days": "Ostatnie 90 dni",
- "lastYear": "ostatnie 12 miesięcy",
- "allTime": "cały okres (12 miesięcy szczegółowo)"
- },
- "windowLabel": "Okres",
"settingsAriaLabel": "Ustawienia coacha",
"settingsTitle": "Ustawienia coacha",
"settingsDescription": "Ustaw, jak coach do ciebie mówi.",
@@ -2940,9 +2993,10 @@
"settingsVerbosityBrief": "Krótka",
"settingsVerbosityDefault": "Standardowa",
"settingsVerbosityDetailed": "Szczegółowa",
- "settingsVerbosityConciseHint": "Zwięzły ton i tak skraca odpowiedzi — szczegółowość pozostaje ustawiona na „Krótko”, dopóki jest aktywny.",
- "settingsSourcesPointer": "Tym, z jakich grup danych może korzystać coach, oraz domyślnym oknem analizy zarządzasz bezpośrednio w czacie coacha (panel „Z czego korzystam”).",
- "settingsSourcesPointerLink": "Otwórz coacha",
+ "settingsVerbosityConciseHint": "Zwięzły ton i tak skraca odpowiedzi, więc szczegółowość pozostaje ustawiona na „Krótko”, dopóki jest aktywny.",
+ "settingsSourcesSummary": "Coach widzi {count} z {total} obszarów danych i patrzy {window} wstecz.",
+ "settingsSourcesSummaryAll": "Coach widzi {count} z {total} obszarów danych i całą historię.",
+ "settingsSourcesPointerLink": "Otwórz ustawienia coacha",
"settingsExcludeLabel": "Wyklucz metryki",
"settingsExcludeHint": "Coach nie zobaczy danych wybranych metryk.",
"settingsContextLabel": "Wysyłaj kontekst",
@@ -2954,34 +3008,30 @@
"settingsSave": "Zapisz",
"settingsSaved": "Zapisano",
"settingsCancel": "Anuluj",
- "feedbackHelpful": "Przydatne",
- "feedbackUnhelpful": "Mało przydatne",
- "feedbackThanks": "Dzięki za sygnał.",
- "feedbackError": "Nie udało się zapisać opinii",
"dailyLimitTitle": "Osiągnięto dzienny limit",
- "dailyLimitBody": "Osiągnięto dzienny limit. Limit resetuje się o północy (UTC).",
+ "dailyLimitBody": "Osiągnięto dzienny limit. Limit resetuje się o północy w Twojej strefie czasowej.",
"providerRateLimitTitle": "Dostawca z ograniczeniem",
"providerRateLimitBody": "Dostawca jest tymczasowo przeciążony; reset za ~5 min.",
"cluster": {
"cardio": {
"label": "Sercowo-naczyniowe",
- "hint": "Ciśnienie · tętno · HRV · tętno spoczynkowe i marszowe"
+ "hint": "Ciśnienie, tętno, HRV, tętno spoczynkowe i marszowe"
},
"body": {
"label": "Skład ciała",
- "hint": "Waga · tłuszcz · mięśnie · BMI"
+ "hint": "Waga, tłuszcz, mięśnie i BMI"
},
"activity": {
"label": "Aktywność",
- "hint": "Kroki · energia · dystans · VO₂ max"
+ "hint": "Kroki, energia, dystans i VO₂ max"
},
"workouts": {
"label": "Treningi",
- "hint": "Biegi · przejażdżki · sesje"
+ "hint": "Biegi, przejażdżki i sesje"
},
"sleep": {
"label": "Sen",
- "hint": "Czas trwania · fazy"
+ "hint": "Czas trwania i fazy"
},
"mood": {
"label": "Nastrój",
@@ -2993,56 +3043,68 @@
},
"medication": {
"label": "Leki",
- "hint": "Przestrzeganie · kontekst GLP-1"
+ "hint": "Przestrzeganie i kontekst GLP-1"
},
"mobility": {
"label": "Mobilność i chód",
- "hint": "Stabilność · asymetria · chód"
+ "hint": "Stabilność, asymetria i chód"
},
"environment": {
"label": "Środowisko",
- "hint": "Hałas · światło · temp. skóry"
+ "hint": "Hałas, światło dzienne i temperatura skóry"
}
},
"settingsClustersLabel": "Dane widziane przez Coacha",
"settingsClustersHint": "Wybierz, które grupy danych Coach może czytać. Więcej danych to bogatsze odpowiedzi i większy prompt.",
"settingsClusterNoData": "Brak danych",
- "sourcesMemberCount": "{count} wskaźników",
+ "sourcesMemberCountOne": "1 wskaźnik",
+ "sourcesMemberCountFew": "{count} wskaźniki",
+ "sourcesMemberCountOther": "{count} wskaźników",
+ "lookback": {
+ "label": "Jak daleko wstecz może patrzeć Coach",
+ "option": {
+ "last7days": "{days} dni",
+ "last30days": "{days} dni",
+ "last90days": "{days} dni",
+ "lastYear": "{months} miesięcy",
+ "allTime": "Wszystko"
+ },
+ "detailDays": "Coach nie czyta niczego starszego niż {days} dni, w żadnej odpowiedzi.",
+ "detailMonths": "Coach nie czyta niczego starszego niż {months} miesięcy, w żadnej odpowiedzi.",
+ "detailAll": "Wszystko, co zostało zapisane: {dailyMonths} miesięcy z dokładnością do dnia, starsze pomiary jako wartości miesięczne do {years} lat wstecz, wyniki badań z ostatnich {labMonths} miesięcy.",
+ "note": "Twój profil, obecne leki, trwająca choroba i nadchodzące wizyty pozostają widoczne; podsumowania wymagające dłuższej historii są skracane lub pomijane. Odpowiedzi, których Coach udzielił wcześniej w starszych rozmowach, mogą nadal przytaczać starsze wartości."
+ },
"showConversations": "Pokaż rozmowy",
"hideConversations": "Ukryj rozmowy",
"heroGreeting": "Zapytaj mnie o cokolwiek na temat twoich danych",
"heroSubline": "Zapytaj o swoje trendy, leki lub pomiary.",
- "thinkingElapsed": "{seconds}s",
"thinkingDuration": "Myślał przez {seconds}s",
"thinkingExpandAria": "Pokaż, co Coach wziął pod uwagę",
"thinkingDetail": "Odpowiedź na podstawie Twojego podsumowania zdrowia.",
"tokensUsed": "{count} tokenów",
- "tokensUsedWithModel": "{count} tokenów · {model}",
"askAboutThis": "Zapytaj trenera o to",
"readStrip": {
"label": "Typowy zakres",
"baselineWithin": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} mieści się w nim.",
"baselineAbove": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} jest powyżej niego.",
"baselineBelow": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} jest poniżej niego.",
- "insufficient": "Wciąż poznaję twój zwykły zakres — rejestruj jeszcze przez kilka dni.",
+ "sameHoursWithin": "Do tej pory dnia zwykle masz {low}–{high} {unit}; dziś jak dotąd {value}, czyli w zakresie.",
+ "sameHoursAbove": "Do tej pory dnia zwykle masz {low}–{high} {unit}; dziś jak dotąd {value}, czyli powyżej.",
+ "sameHoursBelow": "Do tej pory dnia zwykle masz {low}–{high} {unit}; dziś jak dotąd {value}, czyli poniżej.",
+ "steadyWithin": "Twoje pomiary zwykle wynoszą {low} {unit}; dzisiejsza wartość {value} jest taka sama.",
+ "steadyAbove": "Twoje pomiary zwykle wynoszą {low} {unit}; dzisiejsza wartość {value} jest wyższa.",
+ "steadyBelow": "Twoje pomiary zwykle wynoszą {low} {unit}; dzisiejsza wartość {value} jest niższa.",
+ "latestWithin": "Twój zwykły zakres to {low}–{high} {unit}; ostatni pomiar, z {date}, wyniósł {value} i mieścił się w nim.",
+ "latestAbove": "Twój zwykły zakres to {low}–{high} {unit}; ostatni pomiar, z {date}, wyniósł {value} i był powyżej niego.",
+ "latestBelow": "Twój zwykły zakres to {low}–{high} {unit}; ostatni pomiar, z {date}, wyniósł {value} i był poniżej niego.",
+ "steadyLatestWithin": "Twoje pomiary zwykle wynoszą {low} {unit}; ostatni pomiar, z {date}, wyniósł {value} i był taki sam.",
+ "steadyLatestAbove": "Twoje pomiary zwykle wynoszą {low} {unit}; ostatni pomiar, z {date}, wyniósł {value} i był wyższy.",
+ "steadyLatestBelow": "Twoje pomiary zwykle wynoszą {low} {unit}; ostatni pomiar, z {date}, wyniósł {value} i był niższy.",
+ "insufficient": "Wciąż poznaję twój zwykły zakres. Rejestruj jeszcze przez kilka dni.",
"driver": "{note}"
},
"scope": {
- "prefix": "Coach jest już przy {metric}",
- "tap": "Dotknij, aby zapytać",
- "question": "Wytłumacz mi tę wartość — na co powinienem zwrócić uwagę?"
- },
- "seeded": {
- "prefix": "Dziś warto rzucić okiem: {signal}",
- "signal": {
- "readiness": "gotowość",
- "recovery": "regeneracja"
- },
- "question": {
- "readiness": "Moja gotowość jest dziś nietypowa — z czego to wynika?",
- "recovery": "Moja regeneracja się zmieniła — co za tym stoi?"
- },
- "dismiss": "Odrzuć"
+ "prefix": "Coach jest już przy {metric}"
},
"settings": "Ustawienia",
"history": {
@@ -3052,33 +3114,33 @@
"groupEarlier": "Wcześniej"
},
"seed": {
- "bloodPressure": "Przejdź ze mną przez trend mojego ciśnienia krwi z ostatnich 30 dni — czy jest coś, na co powinienem zwrócić uwagę?",
+ "bloodPressure": "Przejdź ze mną przez trend mojego ciśnienia krwi z ostatnich 30 dni. Czy jest coś, na co powinienem zwrócić uwagę?",
"weight": "Jak zmieniała się ostatnio moja waga i czy ten kierunek wymaga reakcji?",
"bmi": "Co mówi mi mój trend BMI i jak powinienem go odczytywać?",
- "pulse": "Przejdź ze mną przez moje odczyty tętna — czy coś odbiega od normy?",
+ "pulse": "Przejdź ze mną przez moje odczyty tętna. Czy coś odbiega od normy?",
"restingHr": "Jak zmieniało się moje tętno spoczynkowe i co mówi o mojej kondycji?",
"hrv": "Co moja zmienność rytmu serca mówi mi o regeneracji i stresie w ostatnim czasie?",
- "sleep": "Przejdź ze mną przez mój ostatni sen — długość, regularność i wszystko, co warto zmienić.",
+ "sleep": "Przejdź ze mną przez mój ostatni sen: długość, regularność i wszystko, co warto zmienić.",
"mood": "Jakie wzorce widzisz w moim nastroju w ciągu ostatnich kilku tygodni?",
"medications": "Jak wygląda moje przestrzeganie zaleceń lekowych i czy pokrywa się z tym, jak się czułem?",
"steps": "Jak aktywny byłem ostatnio na podstawie moich kroków i jak wypada to na tle zdrowego punktu odniesienia?",
"activeEnergy": "Co mój trend aktywnej energii mówi o moim poziomie aktywności w ostatnim czasie?",
"cardioFitness": "Co oznacza trend mojej wydolności krążeniowo-oddechowej (VO₂ max) i jak skierować go we właściwą stronę?",
- "bloodGlucose": "Przejdź ze mną przez moje ostatnie odczyty glukozy — czy mieszczą się w zdrowym zakresie?",
+ "bloodGlucose": "Przejdź ze mną przez moje ostatnie odczyty glukozy. Czy mieszczą się w zdrowym zakresie?",
"oxygenSaturation": "Co mówi mi mój trend saturacji krwi tlenem (SpO₂)?",
"respiratoryRate": "Co mój trend częstości oddechów mówi o moim zdrowiu?",
- "workouts": "Przejdź ze mną przez moje ostatnie treningi — obciążenie, częstotliwość i jak regeneruję się między nimi.",
+ "workouts": "Przejdź ze mną przez moje ostatnie treningi: obciążenie, częstotliwość i jak regeneruję się między nimi.",
"recoveryPage": "Dlaczego moja regeneracja jest teraz na takim poziomie i co na nią wpływa?",
- "briefing": "Przejdź ze mną przez dzisiejszy briefing — na czym powinienem się skupić i czy jest coś, co warto podjąć?",
+ "briefing": "Przejdź ze mną przez dzisiejszy briefing. Na czym powinienem się skupić i czy jest coś, co warto podjąć?",
"healthScore": "Dlaczego mój wynik zdrowia wynosi {score} na 100 i co poprawiłoby go najbardziej?",
- "periodWeek": "Przejdź ze mną przez to, jak minął mój ostatni tydzień w moich wskaźnikach zdrowia — co się wyróżnia?",
- "periodMonth": "Przejdź ze mną przez to, jak minął mój ostatni miesiąc w moich wskaźnikach zdrowia — co się wyróżnia?",
+ "periodWeek": "Przejdź ze mną przez to, jak minął mój ostatni tydzień w moich wskaźnikach zdrowia. Co się wyróżnia?",
+ "periodMonth": "Przejdź ze mną przez to, jak minął mój ostatni miesiąc w moich wskaźnikach zdrowia. Co się wyróżnia?",
"correlationBpCompliance": "Jak moje przestrzeganie zaleceń lekowych wiąże się z moim ciśnieniem krwi? Wyjaśnij mi, co ta korelacja oznacza dla mnie.",
"correlationMoodPulse": "Czy istnieje rzeczywisty związek między moim nastrojem a tętnem? Pomóż mi odczytać tę korelację.",
"correlationWeightWeekday": "Czy moja waga podlega cotygodniowemu wzorcowi? Co mówi mi ta korelacja według dnia tygodnia?",
- "recommendation": "Powiedz mi więcej o tej rekomendacji: \"{text}\" — dlaczego mnie dotyczy i co powinienem z nią zrobić?"
+ "recommendation": "Powiedz mi więcej o tej rekomendacji: \"{text}\". Dlaczego mnie dotyczy i co powinienem z nią zrobić?"
},
- "assessmentPrompt": "Wyjaśnij mi moją ocenę {metric} — co oznacza i co powinienem z tym zrobić?",
+ "assessmentPrompt": "Wyjaśnij mi moją ocenę {metric}: co oznacza i co powinienem z tym zrobić?",
"attach": {
"menuChooseFromDocuments": "Wybierz z dokumentów",
"menuUploadNew": "Prześlij nowy dokument",
@@ -3092,13 +3154,13 @@
"pickerConfirm": "Załącz ({count})",
"pillRemove": "Usuń {title}",
"indexing": "Indeksowanie…",
- "indexingHint": "Dokument jest jeszcze indeksowany – wyślesz, gdy będzie gotowy.",
- "fencedHint": "Ta rozmowa działa bez dostępu do Twoich danych zdrowotnych – coach czyta tylko załączone dokumenty.",
+ "indexingHint": "Dokument jest jeszcze indeksowany. Wyślesz, gdy będzie gotowy.",
+ "fencedHint": "Ta rozmowa działa bez dostępu do Twoich danych zdrowotnych: coach czyta tylko załączone dokumenty.",
"documentsAttachedOne": "1 załączony dokument",
"documentsAttachedOther": "Załączone dokumenty: {count}",
"railBadge": "Odizolowany czat dokumentu",
"uploadError": "Przesyłanie nie powiodło się. Spróbuj ponownie.",
- "notFoundError": "Ten link do dokumentu jest już nieprawidłowy – zamiast tego rozpoczynamy zwykły czat."
+ "notFoundError": "Ten link do dokumentu jest już nieprawidłowy, więc zamiast tego rozpoczynamy zwykły czat."
},
"interrupted": {
"note": "Ta odpowiedź została przerwana przed ukończeniem.",
@@ -3111,7 +3173,102 @@
"errorNotPermitted": "Coach nie jest dostępny w kartotece, którą ktoś Ci udostępnił.",
"errorUnavailable": "Coach nie mógł teraz sprawdzić swoich ustawień. Spróbuj ponownie za chwilę.",
"errorOperatorOff": "AI jest wyłączone na tym serwerze, więc Coach nie może odpowiedzieć.",
- "errorCoachOff": "Coach jest wyłączony. Możesz go ponownie włączyć w Ustawieniach, Moduły."
+ "errorCoachOff": "Coach jest wyłączony. Możesz go ponownie włączyć w Ustawieniach, Moduły.",
+ "frame": {
+ "showPanel": "Pokaż rozmowy",
+ "hidePanel": "Ukryj rozmowy",
+ "panelDescription": "Twoje rozmowy z coachem, od najnowszej.",
+ "rowActions": "Działania dla {title}",
+ "rename": "Zmień nazwę",
+ "delete": "Usuń",
+ "modelSection": "Model",
+ "provider": "Odpowiada przez",
+ "modelManaged": "Model tego dostawcy ustawisz w ustawieniach AI.",
+ "noProvider": "Brak dostępnego dostawcy",
+ "saving": "Zapisywanie…",
+ "saveFailed": "Nie udało się zapisać zmiany.",
+ "allSettings": "Wszystkie ustawienia AI",
+ "providerNotActive": "{provider} nie mógł przejąć odpowiedzi: brak działającego logowania lub klucza. Skonfiguruj go w ustawieniach AI.",
+ "operatorCodex": "ChatGPT (udostępniony przez operatora)",
+ "operatorManaged": "Tym połączeniem zarządza operator Twojego serwera. Gdy skonfigurujesz własnego dostawcę, to on odpowie jako pierwszy."
+ },
+ "answer": {
+ "scrollToBottom": "Przejdź do najnowszej wiadomości",
+ "rememberDone": "Zapamiętano",
+ "timeYesterday": "wczoraj {time}",
+ "timeEarlier": "{date}, {time}",
+ "info": "Szczegóły odpowiedzi",
+ "infoModel": "Model: {model}"
+ },
+ "activity": {
+ "thinking": "Myśli…",
+ "fetching": "Pobiera {domain}, {window}…",
+ "digestOne": "Podsumowuje {count} pomiar…",
+ "digestFew": "Podsumowuje {count} pomiary…",
+ "digestOther": "Podsumowuje {count} pomiarów…",
+ "digestDoneOne": "{count} pomiar z: {areas}",
+ "digestDoneFew": "{count} pomiary z: {areas}",
+ "digestDoneOther": "{count} pomiarów z: {areas}",
+ "areasOne": "{count} obszar",
+ "areasFew": "{count} obszary",
+ "areasOther": "{count} obszarów",
+ "memoryOne": "Pamięta {count} rzecz, którą mu powiedziałeś",
+ "memoryFew": "Pamięta {count} rzeczy, które mu powiedziałeś",
+ "memoryOther": "Pamięta {count} rzeczy, które mu powiedziałeś",
+ "remember": "Zapamiętuje coś…",
+ "plan": "Przygotowuje plan…",
+ "asking": "Dopytuje…",
+ "answer": "Pisze odpowiedź…",
+ "stop": {
+ "budget": "Budżet wyczerpany, odpowiada na podstawie tego, co ma",
+ "time": "Limit czasu osiągnięty, odpowiada na podstawie tego, co ma",
+ "cap": "Wiele kroków, teraz podsumowuje",
+ "noProgress": "Brak nowych danych, podsumowuje"
+ },
+ "thoughtProcessOne": "Tok myślenia ({count} krok)",
+ "thoughtProcessFew": "Tok myślenia ({count} kroki)",
+ "thoughtProcessOther": "Tok myślenia ({count} kroków)",
+ "awaitingAnswer": "Potrzebna odpowiedź"
+ },
+ "memory": {
+ "saved": "Zapamiętane: {fact}",
+ "undo": "Cofnij",
+ "accept": "Tak, zapamiętaj",
+ "decline": "Nie",
+ "confirmed": "Jasne, zapamiętam.",
+ "declined": "Dobrze, nie zapiszę tego."
+ },
+ "plan": {
+ "accept": "Przyjmij ten plan",
+ "decline": "Nie teraz",
+ "confirmed": "Zanotowane. Wrócimy do tego razem za {days} dni.",
+ "declined": "Dobrze, nie teraz."
+ },
+ "assumption": {
+ "line": "Założenie: {value}",
+ "change": "Zmień"
+ },
+ "suggestedReplies": {
+ "groupLabel": "Proponowane odpowiedzi"
+ },
+ "chart": {
+ "compare": {
+ "periods": "{a} wobec {b}"
+ }
+ },
+ "reasoning": {
+ "label": "Głębokość namysłu",
+ "hint": "Głębszy namysł oznacza dokładniejsze odpowiedzi i trochę dłuższe czekanie.",
+ "off": "Wyłączone",
+ "minimal": "Minimalna",
+ "low": "Niska",
+ "medium": "Średnia",
+ "high": "Wysoka",
+ "capped": "(ograniczone przez administratora)",
+ "disabled": "Administrator wyłączył głębszy namysł."
+ },
+ "memoryLink": "Pamięć ({count})",
+ "newConversation": "Nowa rozmowa"
},
"relativeJustNow": "przed chwilą",
"relativeMinutesAgoOne": "{count} minutę temu",
@@ -3174,7 +3331,7 @@
},
"hrv": {
"title": "HRV",
- "description": "Zmienność rytmu serca w czasie — sygnał regeneracji układu nerwowego, rejestrowany nocą przez Apple Watch i inne zgodne wearable.",
+ "description": "Zmienność rytmu serca w czasie: sygnał regeneracji układu nerwowego, rejestrowany nocą przez Apple Watch i inne zgodne wearable.",
"chartTitle": "Zmienność rytmu serca",
"emptyState": {
"title": "Brak wartości HRV",
@@ -3183,17 +3340,17 @@
},
"restingHr": {
"title": "Tętno spoczynkowe",
- "description": "Dzienna linia bazowa rytmu serca — różna od tętna punktowego na stronie Pulse. Apple Zdrowie podaje jedną wartość na dobę.",
+ "description": "Dzienna linia bazowa rytmu serca, różna od tętna punktowego na stronie Pulse. Apple Zdrowie podaje jedną wartość na dobę.",
"chartTitle": "Tętno spoczynkowe",
"emptyState": {
"title": "Brak tętna spoczynkowego",
"description": "Tętno spoczynkowe pojawi się, gdy Apple Watch lub zgodne urządzenie Withings spędzi spokojny dzień na nadgarstku."
},
- "coachPrefill": "Nie zarejestrowałem jeszcze tętna spoczynkowego — dlaczego dzienna wartość spoczynkowa różni się od pojedynczego pomiaru tętna i jak ją poprawić?"
+ "coachPrefill": "Nie zarejestrowałem jeszcze tętna spoczynkowego. Dlaczego dzienna wartość spoczynkowa różni się od pojedynczego pomiaru tętna i jak ją poprawić?"
},
"oxygenSaturation": {
"title": "Saturacja tlenu",
- "description": "Tlen we krwi (SpO₂) — kliniczny zakres normy 95–100 %, ze strefami docelowymi zacienionymi na wykresie.",
+ "description": "Tlen we krwi (SpO₂): kliniczny zakres normy 95–100 %, ze strefami docelowymi zacienionymi na wykresie.",
"chartTitle": "Saturacja tlenu",
"emptyState": {
"title": "Brak próbek SpO₂",
@@ -3206,13 +3363,13 @@
"chartTitle": "Temperatura ciała",
"emptyState": {
"title": "Brak temperatury ciała",
- "description": "Zarejestruj pomiar lub pozwól wearable zsynchronizować jeden — trend pojawi się tutaj.",
+ "description": "Zarejestruj pomiar lub pozwól wearable zsynchronizować jeden, a trend pojawi się tutaj.",
"cta": "Zarejestruj temperaturę ciała"
}
},
"steps": {
"title": "Kroki",
- "description": "Dzienna liczba kroków — jeden wiersz na dobę, z Apple Zdrowie lub podłączonego urządzenia aktywności.",
+ "description": "Dzienna liczba kroków: jeden wiersz na dobę, z Apple Zdrowie lub podłączonego urządzenia aktywności.",
"chartTitle": "Kroki dziennie",
"emptyState": {
"title": "Brak danych o krokach",
@@ -3221,7 +3378,7 @@
},
"activeEnergy": {
"title": "Energia czynna",
- "description": "Dzienna suma energii czynnej w kilokaloriach — jeden wiersz na dobę, z Apple Zdrowie lub aktywności Withings.",
+ "description": "Dzienna suma energii czynnej w kilokaloriach: jeden wiersz na dobę, z Apple Zdrowie lub aktywności Withings.",
"chartTitle": "Energia czynna",
"emptyState": {
"title": "Brak danych o energii czynnej",
@@ -3241,11 +3398,11 @@
},
"explainer": {
"bloodPressureBody": "Ciśnienie krwi to siła, z jaką krew napiera na ściany tętnic, zapisywana jako skurczowe do rozkurczowego (szczyt podczas uderzenia serca do wartości spoczynkowej między uderzeniami). Stale wysokie wartości z czasem obciążają serce i naczynia.",
- "pulseBody": "Tętno to liczba uderzeń serca na minutę. Pojedynczy pomiar odzwierciedla to, co robiłeś w danej chwili — w spoczynku wynosi zwykle 60–100 uderzeń na minutę i naturalnie rośnie przy ruchu, stresie lub kofeinie.",
+ "pulseBody": "Tętno to liczba uderzeń serca na minutę. Pojedynczy pomiar odzwierciedla to, co robiłeś w danej chwili. W spoczynku wynosi zwykle 60–100 uderzeń na minutę i naturalnie rośnie przy ruchu, stresie lub kofeinie.",
"weightBody": "Masa ciała to jeden z najprostszych sygnałów długoterminowego bilansu energetycznego i ogólnego zdrowia. Trend na przestrzeni tygodni liczy się znacznie bardziej niż pojedynczy odczyt, który zmienia się wraz z nawodnieniem, jedzeniem i porą dnia.",
- "bmiBody": "BMI to waga podzielona przez wzrost do kwadratu — jedna liczba przypisująca Cię do szerokiej kategorii wagowej. To badanie przesiewowe dla populacji, a nie diagnoza, i nie odróżnia mięśni od tłuszczu.",
+ "bmiBody": "BMI to waga podzielona przez wzrost do kwadratu: jedna liczba przypisująca Cię do szerokiej kategorii wagowej. To badanie przesiewowe dla populacji, a nie diagnoza, i nie odróżnia mięśni od tłuszczu.",
"sleepBody": "Długość snu to czas, który faktycznie przespałeś w nocy, podzielony na fazy lekką, głęboką i REM. Większość dorosłych potrzebuje około siedmiu do dziewięciu godzin; stała ilość i jakość wspierają regenerację, nastrój i metabolizm.",
- "moodBody": "Śledzenie nastroju rejestruje Twoje samopoczucie w czasie na prostej skali. Regularne zapisywanie ujawnia wzorce — jak sen, aktywność czy leki łączą się z samopoczuciem — których pojedynczy dzień nigdy nie pokaże.",
+ "moodBody": "Śledzenie nastroju rejestruje Twoje samopoczucie w czasie na prostej skali. Regularne zapisywanie ujawnia wzorce (jak sen, aktywność czy leki łączą się z samopoczuciem), których pojedynczy dzień nigdy nie pokaże.",
"medicationsBody": "Przestrzeganie zaleceń to rzetelność przyjmowania leków zgodnie z zaleceniami, pokazana tu jako odsetek oczekiwanych dawek, które zarejestrowałeś. Regularne przyjmowanie często decyduje o tym, czy terapia działa zgodnie z zamierzeniem.",
"restingHrBody": "Tętno spoczynkowe to częstość uderzeń serca w pełnym spoczynku, zwykle mierzona w nocy. Niższe tętno zwykle wskazuje na lepszą wydolność krążeniową, a rosnący trend może być wczesnym sygnałem stresu lub choroby.",
"hrvBody": "HRV to drobna zmienność czasu między kolejnymi uderzeniami serca. Wyższa zmienność zwykle odzwierciedla dobrze zregenerowany, elastyczny układ nerwowy, a utrzymujący się spadek może sygnalizować zmęczenie, stres lub niewystarczającą regenerację.",
@@ -3256,12 +3413,12 @@
"respiratoryRateBody": "Częstość oddechów to liczba wdechów na minutę. W spoczynku większość dorosłych oddycha około dwunastu do dwudziestu razy na minutę; utrzymujący się wzrost może towarzyszyć chorobie, wysiłkowi lub stresowi.",
"bodyWaterBody": "Całkowita woda w organizmie to udział płynów w masie ciała, wewnątrz i wokół komórek. Zmienia się wraz z nawodnieniem, posiłkami i aktywnością, więc trend w ciągu tygodni mówi więcej niż pojedynczy pomiar.",
"boneMassBody": "Masa kostna to szacowana waga minerałów w szkielecie. Pomiary z wagi są przybliżone, ale stabilny lub rosnący trend sprzyja długotrwałej wytrzymałości kości.",
- "fatFreeMassBody": "Masa beztłuszczowa to wszystko w ciele, co nie jest tłuszczem — mięśnie, kości, narządy i woda. Utrzymanie lub jej budowanie przy spadku wagi zwykle wskazuje na zdrowszy skład ciała.",
- "fatMassBody": "Masa tłuszczowa to łączna waga tłuszczu w ciele. Pewna ilość tłuszczu jest niezbędna dla hormonów i magazynowania energii, ale jego nadmiar — zwłaszcza w okolicy brzucha — zwiększa długoterminowe ryzyko zdrowotne (WHO 2024).",
+ "fatFreeMassBody": "Masa beztłuszczowa to wszystko w ciele, co nie jest tłuszczem: mięśnie, kości, narządy i woda. Utrzymanie lub jej budowanie przy spadku wagi zwykle wskazuje na zdrowszy skład ciała.",
+ "fatMassBody": "Masa tłuszczowa to łączna waga tłuszczu w ciele. Pewna ilość tłuszczu jest niezbędna dla hormonów i magazynowania energii, ale jego nadmiar (zwłaszcza w okolicy brzucha) zwiększa długoterminowe ryzyko zdrowotne (WHO 2024).",
"bodyFatBody": "Procent tkanki tłuszczowej to część masy ciała, którą stanowi tłuszcz. Zdrowe zakresy zależą od płci i wieku, a wagi szacują go metodą bioimpedancji, dlatego trend z kilku tygodni mówi więcej niż pojedynczy pomiar.",
"muscleMassBody": "Masa mięśniowa to szacowana waga mięśni w ciele. Więcej mięśni wspiera siłę, metabolizm i sprawność, a ich utrzymanie z wiekiem zwykle wymaga aktywnego wysiłku.",
"visceralFatBody": "Tłuszcz trzewny to tłuszcz gromadzony głęboko w jamie brzusznej wokół narządów. W odróżnieniu od tłuszczu podskórnego jego wysoki poziom jest ściśle powiązany z chorobami serca i cukrzycą typu 2, więc im niżej, tym lepiej.",
- "leanBodyMassBody": "Beztłuszczowa masa ciała to całkowita waga pomniejszona o tłuszcz — mięśnie, kości, narządy i woda razem. Śledzenie jej obok wagi pokazuje, czy zmiany wynikają z mięśni, czy z tłuszczu.",
+ "leanBodyMassBody": "Beztłuszczowa masa ciała to całkowita waga pomniejszona o tłuszcz: mięśnie, kości, narządy i woda razem. Śledzenie jej obok wagi pokazuje, czy zmiany wynikają z mięśni, czy z tłuszczu.",
"flightsClimbedBody": "Pokonane piętra liczą kondygnacje, które pokonujesz w górę każdego dnia, gdzie jedno piętro to około trzech metrów wzniesienia. To prosty wskaźnik codziennej aktywności i wysiłku nóg.",
"walkingDistanceBody": "Dystans marszu i biegu to droga, jaką pokonujesz pieszo każdego dnia. Uzupełnia liczbę kroków, pokazując, jak daleko się poruszałeś, a nie tylko jak często stawiałeś kroki.",
"walkingSteadinessBody": "Stabilność chodu szacuje, jak stabilny i zrównoważony jest twój sposób chodzenia, w skali od dobrej do niskiej. Niski poziom wskazuje na większe ryzyko upadku i warto omówić go z lekarzem.",
@@ -3278,28 +3435,28 @@
"daylightBody": "Czas w świetle dziennym to ilość czasu spędzanego w świetle na zewnątrz każdego dnia. Regularne światło dzienne wspiera zdrowy rytm snu i czuwania oraz nastrój (CDC 2024), a czas na świeżym powietrzu wiąże się ze zdrowiem oczu.",
"bloodGlucoseBody": "Poziom glukozy we krwi to ilość cukru we krwi, głównego źródła energii organizmu. Rośnie po jedzeniu i ustala się między posiłkami; wartości utrzymujące się wysoko mogą sygnalizować cukrzycę lub stan przedcukrzycowy.",
"skinTemperatureBody": "Temperatura skóry to tutaj niewielka nocna zmiana temperatury nadgarstka względem twojej własnej wartości bazowej, a nie bezwzględny pomiar gorączki. Zmiany mogą odzwierciedlać sen, otoczenie, chorobę lub fazę cyklu menstruacyjnego.",
- "cardioFitnessBody": "VO₂ max to ilość tlenu, jaką organizm potrafi wykorzystać przy maksymalnym wysiłku — najlepszy laboratoryjny wskaźnik wydolności tlenowej, który urządzenia noszone szacują pośrednio. Wyższy lub rosnący trend to mocny sygnał dobrego zdrowia układu krążenia.",
+ "cardioFitnessBody": "VO₂ max to ilość tlenu, jaką organizm potrafi wykorzystać przy maksymalnym wysiłku. To najlepszy laboratoryjny wskaźnik wydolności tlenowej, który urządzenia noszone szacują pośrednio. Wyższy lub rosnący trend to mocny sygnał dobrego zdrowia układu krążenia.",
"cardioRecoveryBody": "Regeneracja serca to liczba uderzeń na minutę, o jaką spada tętno w pierwszej minucie po zakończeniu intensywnego wysiłku. Większy spadek oznacza, że serce szybciej się uspokaja, co świadczy o dobrej kondycji.",
"wristTemperatureBody": "Temperatura nadgarstka jest mierzona podczas snu i podawana jako różnica względem Twojej wartości bazowej, a nie jako wartość bezwzględna. Stabilny odczyt jest normalny, a utrzymujący się wzrost może wskazywać na chorobę, intensywny trening lub zmianę cyklu.",
"fallsBody": "Tutaj liczone są poważne upadki wykryte przez urządzenie noszone. Celem jest zero; rosnąca liczba to sygnał ryzyka związanego z mobilnością, który warto omówić z lekarzem, zwłaszcza u osób starszych (CDC STEADI 2024).",
- "sixMinuteWalkBody": "To szacunek, jak daleko mógłbyś przejść w sześć minut — klasyczny test kliniczny mobilności i wydolności krążeniowo-oddechowej. Wartość z urządzenia noszonego to przybliżenie, a nie nadzorowany test kliniczny; liczy się stały lub rosnący trend.",
- "stairAscentSpeedBody": "To prędkość, z jaką pokonujesz bieg schodów. To czuły wskaźnik siły nóg i mobilności — stałe lub rosnące tempo to dobry znak, a spadające warto obserwować.",
- "stairDescentSpeedBody": "To prędkość, z jaką schodzisz bieg schodów. Razem z prędkością wchodzenia odzwierciedla kontrolę nóg i równowagę — stałe tempo to dobry znak, a spadające warto obserwować.",
- "breathingDisturbancesBody": "Urządzenie noszone potrafi oszacować, jak często Twój oddech był przerywany podczas snu. Niższy wskaźnik jest lepszy; poziom oznaczony jako podwyższony — zwłaszcza przez wiele nocy — warto omówić z lekarzem. To przybliżenie przesiewowe, a nie diagnoza bezdechu sennego.",
- "stepsBody": "Liczba kroków to łączna liczba kroków wykonanych każdego dnia — najprostsza miara codziennego ruchu. Stały nawyk liczy się bardziej niż pojedynczy intensywny dzień, a okrągły cel 10 000 kroków to hasło reklamowe, a nie wynik badań.",
- "readinessBody": "Gotowość to pojedynczy dzienny odczyt tego, jak przygotowane jest Twoje ciało, by zmierzyć się z dniem — łączy tętno spoczynkowe, zmienność rytmu serca i sen. To przybliżenie z urządzenia względem Twojej własnej bazy, nie pomiar kliniczny: wyższy wynik oznacza wypoczęcie, niższy to delikatna wskazówka, by zwolnić tempo.",
- "sleepScoreBody": "Twój wynik snu streszcza całą noc w jednej liczbie — długość, efektywność, regularność pór oraz proporcje snu lekkiego, głębokiego i REM. Obserwuj wzorzec w ujęciu tygodni, zamiast gonić za idealnym wynikiem jednej nocy.",
- "recoveryBody": "Regeneracja odzwierciedla, jak dobrze Twoje ciało się odbudowało — opiera się na nocnych sygnałach, takich jak zmienność rytmu serca i tętno spoczynkowe. Wysoki wynik to zielone światło, by przyspieszyć; niższy sugeruje, że organizm wciąż się regeneruje.",
- "strainBody": "Obciążenie ujmuje, ile wysiłku sercowo-naczyniowego i fizycznego nałożyłeś na swoje ciało — od treningów po aktywny dzień na nogach. Gdy zbierze się wystarczająco danych, jest oceniane względem Twojego własnego typowego wysiłku, a nie cudzej miary.",
+ "sixMinuteWalkBody": "To szacunek, jak daleko mógłbyś przejść w sześć minut, czyli klasyczny test kliniczny mobilności i wydolności krążeniowo-oddechowej. Wartość z urządzenia noszonego to przybliżenie, a nie nadzorowany test kliniczny; liczy się stały lub rosnący trend.",
+ "stairAscentSpeedBody": "To prędkość, z jaką pokonujesz bieg schodów. To czuły wskaźnik siły nóg i mobilności: stałe lub rosnące tempo to dobry znak, a spadające warto obserwować.",
+ "stairDescentSpeedBody": "To prędkość, z jaką schodzisz bieg schodów. Razem z prędkością wchodzenia odzwierciedla kontrolę nóg i równowagę: stałe tempo to dobry znak, a spadające warto obserwować.",
+ "breathingDisturbancesBody": "Urządzenie noszone potrafi oszacować, jak często Twój oddech był przerywany podczas snu. Niższy wskaźnik jest lepszy; poziom oznaczony jako podwyższony, zwłaszcza przez wiele nocy, warto omówić z lekarzem. To przybliżenie przesiewowe, a nie diagnoza bezdechu sennego.",
+ "stepsBody": "Liczba kroków to łączna liczba kroków wykonanych każdego dnia, najprostsza miara codziennego ruchu. Stały nawyk liczy się bardziej niż pojedynczy intensywny dzień, a okrągły cel 10 000 kroków to hasło reklamowe, a nie wynik badań.",
+ "readinessBody": "Gotowość to pojedynczy dzienny odczyt tego, jak przygotowane jest Twoje ciało, by zmierzyć się z dniem. Łączy tętno spoczynkowe, zmienność rytmu serca i sen. To przybliżenie z urządzenia względem Twojej własnej bazy, nie pomiar kliniczny: wyższy wynik oznacza wypoczęcie, niższy to delikatna wskazówka, by zwolnić tempo.",
+ "sleepScoreBody": "Twój wynik snu streszcza całą noc w jednej liczbie: długość, efektywność, regularność pór oraz proporcje snu lekkiego, głębokiego i REM. Obserwuj wzorzec w ujęciu tygodni, zamiast gonić za idealnym wynikiem jednej nocy.",
+ "recoveryBody": "Regeneracja odzwierciedla, jak dobrze Twoje ciało się odbudowało. Opiera się na nocnych sygnałach, takich jak zmienność rytmu serca i tętno spoczynkowe. Wysoki wynik to zielone światło, by przyspieszyć; niższy sugeruje, że organizm wciąż się regeneruje.",
+ "strainBody": "Obciążenie ujmuje, ile wysiłku sercowo-naczyniowego i fizycznego nałożyłeś na swoje ciało, od treningów po aktywny dzień na nogach. Gdy zbierze się wystarczająco danych, jest oceniane względem Twojego własnego typowego wysiłku, a nie cudzej miary.",
"stressBody": "Stres jest tu szacunkiem fizjologicznego obciążenia Twojego układu nerwowego, odczytywanym głównie ze wzorców zmienności rytmu serca, a nie z tego, jak się świadomie czujesz. Niższy wynik wskazuje na spokojny, dobrze regulowany stan; wyższy to zachęta, by wpleść regenerację.",
- "recoveryPageBody": "Regeneracja łączy nocne sygnały rejestrowane przez Twoje urządzenie – recharge, obciążenie, load oraz dzienne sumy tętna i energii – aby pokazać, jak dobrze Twój organizm się zregenerował. Patrz na wzorzec w ciągu kilku dni, a nie na pojedynczą liczbę; niższy wynik to zaproszenie do zwolnienia tempa, a nie alarm.",
+ "recoveryPageBody": "Regeneracja łączy nocne sygnały rejestrowane przez Twoje urządzenie (recharge, obciążenie, load oraz dzienne sumy tętna i energii), aby pokazać, jak dobrze Twój organizm się zregenerował. Patrz na wzorzec w ciągu kilku dni, a nie na pojedynczą liczbę; niższy wynik to zaproszenie do zwolnienia tempa, a nie alarm.",
"pulsePressureBody": "Ciśnienie tętna to różnica między wartością skurczową a rozkurczową (skurczowe minus rozkurczowe). Jako ogólna wskazówka, około 40 mmHg jest typowe w spoczynku; szeroka różnica powyżej około 60 mmHg w średnim wieku może odzwierciedlać usztywnienie tętnic, a wąska różnica poniżej około 25 mmHg może towarzyszyć niskiej objętości wyrzutowej (StatPearls/NIH 2023). To powód do rozmowy, nie diagnoza.",
"meanArterialPressureBody": "Średnie ciśnienie tętnicze to przeciętne ciśnienie, jakie widzą Twoje narządy w trakcie uderzenia serca, w przybliżeniu rozkurczowe plus jedna trzecia różnicy do skurczowego. Jako ogólna wskazówka, wartość spoczynkowa mieści się w okolicy 70–100 mmHg (StatPearls/NIH 2023). Często przywoływany próg ~60 mmHg to pojęcie z medycyny ostrej, nie alarm do samodzielnego pomiaru.",
"cycleEducationBody": "Twój cykl przebiega przez cztery fazy sterowane hormonami, a jego długość i regularność to dostępne okno na ogólny stan zdrowia. Jako ogólna wskazówka, u osób dorosłych cykle trwają zwykle 21–35 dni (źródła kliniczne podają 24–38), z krwawieniem około pięciu dni (ACOG 2015). Faza lutealna jest stałą kotwicą, dlatego owulację najlepiej szacować, licząc wstecz od następnej spodziewanej miesiączki, nigdy według stałego „14. dnia”. Przewidziane okno płodne to oszacowanie, które wymaga kilku zarejestrowanych cykli i pozostaje przybliżone, a podstawowa temperatura ciała potwierdza owulację po fakcie, a nie ją przewiduje. To nie jest metoda antykoncepcji.",
"sleepHygieneBody": "Kilka opartych na dowodach nawyków wspiera sen: stałe pory kładzenia się i wstawania także w weekendy, jasne światło rano i przyciemnione wieczory, chłodna, ciemna i cicha sypialnia oraz unikanie kofeiny po południu, alkoholu późnym wieczorem i obfitych posiłków przed snem (CDC 2024). Regularność przewiduje zdrowie co najmniej tak silnie jak długość snu, a całkowity czas snu z urządzenia jest znacznie bardziej wiarygodny niż jego podział na sen głęboki, lekki i REM."
},
"bpDerived": {
- "pulsePressure": "Ciśnienie tętna (różnica między Twoimi wartościami) wynosi około {value} mmHg – około 40 jest typowe w spoczynku.",
+ "pulsePressure": "Ciśnienie tętna (różnica między Twoimi wartościami) wynosi około {value} mmHg; około 40 jest typowe w spoczynku.",
"meanArterialPressure": "Średnie ciśnienie tętnicze (średnia, jaką widzą Twoje narządy) wynosi około {value} mmHg, zwykle w okolicy 70–100.",
"caveat": "Te wartości są wyliczone z ostatniego pomiaru jako ogólna wskazówka, nie jako diagnoza."
},
@@ -3311,7 +3468,7 @@
"mean": "Średnia"
},
"showAllValues": "Pokaż wszystkie pomiary",
- "valuesPageTitle": "{metric} — wszystkie pomiary",
+ "valuesPageTitle": "{metric}: wszystkie pomiary",
"valuesPageDescription": "Każdy pomiar stojący za tą metryką. Edytuj lub usuń wpis bezpośrednio.",
"diversity": {
"trigger": "Wskazówka pomiarowa",
@@ -3351,11 +3508,11 @@
"inBed": "W łóżku",
"nap": "Drzemka",
"napCount": "{count} drzemki/ek",
- "unavailable": "Brak danych o fazach — fazy snu pojawią się tutaj, gdy tylko dostarczy je połączone źródło."
+ "unavailable": "Brak danych o fazach. Fazy snu pojawią się tutaj, gdy tylko dostarczy je połączone źródło."
},
"empty": {
"title": "Brak danych o śnie",
- "description": "Połącz źródło takie jak Apple Health lub urządzenie noszone — albo zapisuj sen ręcznie — a ten widok wypełni się automatycznie.",
+ "description": "Połącz źródło takie jak Apple Health lub urządzenie noszone albo zapisuj sen ręcznie, a ten widok wypełni się automatycznie.",
"action": "Zarządzaj urządzeniami"
},
"debt": {
@@ -3364,21 +3521,21 @@
"caption": "Pozostały bilans z ostatnich {nights} nocy",
"clearValue": "Na bieżąco",
"clearCaption": "Nic do nadrobienia w ostatnich {nights} nocach",
- "learning": "Wciąż analizujemy Twój sen — {counted} nocy do tej pory, jeszcze {remaining}, aby uzyskać wiarygodny bilans.",
+ "learning": "Wciąż analizujemy Twój sen: {counted} nocy do tej pory, jeszcze {remaining}, aby uzyskać wiarygodny bilans.",
"computedInfo": "To własne oszacowanie HealthLog: ruchomy bilans potrzeby snu minus sen z ostatnich nocy, który sen nadrabiający zmniejsza. Może różnić się od natywnego wskaźnika długu snu z urządzenia, które korzysta z własnego modelu zapotrzebowania."
},
"average": {
"title": "Średnia na noc",
"value": "{hours} godz. {minutes} min",
"caption": "Średni sen w ciągu ostatnich {nights} nocy",
- "learning": "Wciąż analizujemy Twój sen — {counted} nocy do tej pory, jeszcze {remaining}, aby uzyskać wiarygodną średnią."
+ "learning": "Wciąż analizujemy Twój sen: {counted} nocy do tej pory, jeszcze {remaining}, aby uzyskać wiarygodną średnią."
},
"chronotype": {
"title": "Chronotyp",
- "learning": "Wciąż poznajemy Twój rytm — {counted} z {total} wolnych nocy. Chronotyp wymaga kilku poranków bez budzika, aby coś znaczył.",
+ "learning": "Wciąż poznajemy Twój rytm: {counted} z {total} wolnych nocy. Chronotyp wymaga kilku poranków bez budzika, aby coś znaczył.",
"advancedToggle": "Zaawansowane",
"socialJetlagValue": "{hours} godz. {minutes} min jet lagu społecznego",
- "socialJetlagCaption": "Różnica między porą snu w dni robocze i wolne — jak niewielka cotygodniowa zmiana strefy czasowej.",
+ "socialJetlagCaption": "Różnica między porą snu w dni robocze i wolne, jak niewielka cotygodniowa zmiana strefy czasowej.",
"msfScValue": "Skorygowany środek snu {clock}",
"msfScCaption": "Skorygowany o sen nadrabiany w weekend (MSFsc).",
"dayTypeNote": "Dni wolne przyjmuje się jako weekendy, dopóki nie zostanie dodane śledzenie typu dnia.",
@@ -3457,7 +3614,7 @@
"navRecovery": "Regeneracja",
"workouts": {
"title": "Treningi",
- "description": "Ostatnie biegi, jazdy, marsze i treningi siłowe — zdeduplikowane między Apple Watch a Withings.",
+ "description": "Ostatnie biegi, jazdy, marsze i treningi siłowe, zdeduplikowane między Apple Watch a Withings.",
"emptyState": {
"title": "Brak treningów",
"description": "Treningi trafiają tu z Apple Zdrowie na iPhonie i z Withings. Możesz też wpisać trening ręcznie."
@@ -3567,7 +3724,10 @@
"endInFuture": "Trening kończyłby się w przyszłości. Ustaw początek na godzinę, o której się zaczął.",
"distanceInvalid": "Podaj dystans jako liczbę w realistycznym zakresie.",
"energyInvalid": "Podaj energię czynną jako liczbę do 20 000 kcal."
- }
+ },
+ "editTitle": "Edytuj trening",
+ "editAction": "Edytuj",
+ "updated": "Trening zaktualizowany"
}
},
"bloodGlucose": {
@@ -3582,11 +3742,11 @@
"clinical": {
"title": "Przegląd glukozy",
"subtitle": "Spojrzenie na ostatnie {days} dni, na podstawie {count} pomiarów.",
- "spotCaveat": "To szacunki z pojedynczych pomiarów punktowych, a nie profil z czujnika ciągłego — traktuj je jako kierunek, nie jako diagnozę.",
+ "spotCaveat": "To szacunki z pojedynczych pomiarów punktowych, a nie profil z czujnika ciągłego. Traktuj je jako kierunek, nie jako diagnozę.",
"labsLink": "Wyniki laboratoryjne? HbA1c lub glukozę na czczo z pobrania krwi zapisz w sekcji Laboratorium.",
"learningTitle": "Wciąż poznajemy Twoją glukozę",
- "learningBody": "Na razie to {count} w ciągu {days}. Przy nieco dłuższej historii obraz się wyostrza — rejestruj dalej lub pozwól uzupełnić to podłączonemu urządzeniu.",
- "learningBodyEmpty": "Gdy pojawi się kilka pomiarów — wpisanych przez Ciebie lub zsynchronizowanych z urządzenia — czas w zakresie, średnia i zmienność nabiorą tu kształtu.",
+ "learningBody": "Na razie to {count} w ciągu {days}. Przy nieco dłuższej historii obraz się wyostrza, więc rejestruj dalej lub pozwól uzupełnić to podłączonemu urządzeniu.",
+ "learningBodyEmpty": "Gdy pojawi się kilka pomiarów (wpisanych przez Ciebie lub zsynchronizowanych z urządzenia), czas w zakresie, średnia i zmienność nabiorą tu kształtu.",
"readingsCount": "{count} pomiarów",
"readingsCountOne": "1 pomiar",
"daysCount": "{days} dni",
@@ -3624,7 +3784,7 @@
"intro": "Złożone miary o poziomie badawczym dla bliższego spojrzenia. Każda sprowadza Twoje pomiary do jednej liczby ważonej ryzykiem.",
"jIndex": {
"label": "Indeks J",
- "help": "Pojedynczy wynik łączący średnią i jej rozrzut — niższy oznacza ściślejszą kontrolę."
+ "help": "Pojedynczy wynik łączący średnią i jej rozrzut. Niższy oznacza ściślejszą kontrolę."
},
"lbgi": {
"label": "Ryzyko niedocukrzeń (LBGI)",
@@ -3741,7 +3901,7 @@
"navSkinTemperature": "Temp. skóry",
"pulseWaveVelocity": {
"title": "Prędkość fali tętna",
- "description": "Prędkość fali tętna tętniczego w czasie, w m/s — wyższe wartości sugerują sztywniejsze tętnice.",
+ "description": "Prędkość fali tętna tętniczego w czasie, w m/s. Wyższe wartości sugerują sztywniejsze tętnice.",
"chartTitle": "Prędkość fali tętna",
"emptyState": {
"title": "Brak danych",
@@ -3891,7 +4051,6 @@
"navWalkingSpeed": "Prędkość chodu",
"mood": {
"logLink": "Zapisz nastrój",
- "mentalWellbeingLink": "Wykonaj sprawdzian dobrostanu psychicznego",
"heatmapTitle": "Kalendarz nastroju",
"heatmapNoEntry": "Brak wpisu",
"legendGreat": "Świetnie",
@@ -3902,7 +4061,7 @@
"distributionDaysUnit": "dni",
"weekdayTitle": "Średnia wg dnia tygodnia",
"weekdayNoData": "Brak danych",
- "weekdayBestWorst": "Najlepszy dzień: {best} · Najtrudniejszy dzień: {worst}",
+ "weekdayBestWorst": "Najlepszy dzień: {best}, najtrudniejszy dzień: {worst}",
"tagsTitle": "Tagi i wyzwalacze",
"tagAvgMood": "Średni nastrój z tym tagiem",
"correlationsTitle": "Nastrój a inne wskaźniki",
@@ -3918,7 +4077,7 @@
"strengthModerate": "Umiarkowana",
"strengthWeak": "Słaba",
"strengthNone": "Brak",
- "source": "{n} sparowanych dni · r = {r}",
+ "source": "{n} sparowanych dni, r = {r}",
"emptyTitle": "Za mało sparowanych danych",
"emptyDescription": "Rejestruj nastrój i ten wskaźnik w te same dni, aby zobaczyć korelację.",
"pendingDescription": "{n} z {needed} sparowanych dni. Korelacja pojawi się, gdy dni wystarczy, aby odróżnić wzorzec od przypadku.",
@@ -3935,7 +4094,7 @@
"inTargetCaption": "ostatnich dni w zakresie dobrego nastroju",
"narrative": {
"title": "Co się wyróżnia",
- "weekdayDip": "Twój nastrój najbardziej spada w {weekday} — {delta} poniżej twojego tygodnia.",
+ "weekdayDip": "Twój nastrój najbardziej spada w {weekday}: {delta} poniżej twojego tygodnia.",
"weekdayPeak": "{weekday} to zawsze twój najlepszy dzień, średnio {value}.",
"trendUp": "Twój nastrój rośnie w ciągu ostatnich {days} dni (+{delta} względem poprzedniego okresu).",
"trendDown": "Twój nastrój spada w ciągu ostatnich {days} dni (-{delta} względem poprzedniego okresu).",
@@ -3944,7 +4103,7 @@
"tagLift": "Dni z tagiem „{tag}” są średnio o {delta} powyżej twojego ogólnego nastroju.",
"tagDrop": "Dni z tagiem „{tag}” są o {delta} poniżej twojej średniej.",
"inTarget": "Przez {pct}% ostatnich 30 dni byłeś w zakresie dobrego nastroju.",
- "streak": "Rejestrujesz nastrój od {days} dni z rzędu — tak trzymaj.",
+ "streak": "Rejestrujesz nastrój od {days} dni z rzędu. Tak trzymaj.",
"timeOfDay": "Najlepiej czujesz się {bucket}, średnio {value}."
},
"timeOfDay": {
@@ -3977,7 +4136,7 @@
"crosstab": {
"title": "Tagi a Twoje wskaźniki zdrowia",
"description": "Jak wskaźnik zdrowia różni się w dniach z danym tagiem w porównaniu z dniami bez niego.",
- "pairLabel": "{tag} · {metric}",
+ "pairLabel": "{metric} w dni z: {tag}",
"metricActiveEnergy": "Aktywna energia",
"metricSleepDuration": "Sen",
"metricNextDayRecovery": "Regeneracja następnego dnia",
@@ -3994,7 +4153,7 @@
"factorCrosstab": {
"title": "Twoje ocenione czynniki i parametry życiowe",
"description": "Jak parametr życiowy różni się w dniach, w których oceniłeś dany czynnik nisko, w porównaniu z dniami, w których oceniłeś go wysoko.",
- "pairLabel": "{factor} · {metric}",
+ "pairLabel": "{metric} w dni z: {factor}",
"metricSleepDuration": "Sen",
"metricSteps": "Kroki",
"metricRestingHeartRate": "Tętno spoczynkowe",
@@ -4015,12 +4174,12 @@
"title": "Co się wyróżnia",
"subheading": "Wykryte relacje",
"footer": "Wyłonione z {tested} par dzień po dniu testowanych na wszystkich Twoich wskaźnikach, po korekcie fałszywych odkryć.",
- "disclaimer": "Przefiltrowane statystycznie, ale wciąż obserwacyjne — wzorzec warty uwagi, nie przyczyna.",
+ "disclaimer": "Przefiltrowane statystycznie, ale wciąż obserwacyjne: wzorzec warty uwagi, nie przyczyna.",
"moodUp": "Wyższy {factor} zwykle wiąże się z lepszym nastrojem następnego dnia w Twoich danych.",
"moodDown": "Wyższy {factor} zwykle wiąże się z gorszym nastrojem następnego dnia w Twoich danych.",
"outcomeUp": "Lepszy nastrój zwykle wiąże się z wyższym {factor} następnego dnia w Twoich danych.",
"outcomeDown": "Lepszy nastrój zwykle wiąże się z niższym {factor} następnego dnia w Twoich danych.",
- "stat": "{n} sparowanych dni · r = {r} · q = {q}",
+ "stat": "{n} sparowanych dni, r = {r}, q = {q}",
"statLabel": "Jak to obliczono",
"explainerLabel": "Jak to obliczono"
},
@@ -4054,8 +4213,8 @@
"deviationAbove": "Twoja własna ocena leży {amount} punktów powyżej tego zakresu.",
"deviationBelow": "Twoja własna ocena leży {amount} punktów poniżej tego zakresu.",
"weightedTitle": "Co najbardziej zaważyło w porównaniu tego dnia",
- "weightedUp": "{label} — liczyło się w stronę wyższej oczekiwanej wartości",
- "weightedDown": "{label} — liczyło się w stronę niższej oczekiwanej wartości",
+ "weightedUp": "{label}: liczyło się w stronę wyższej oczekiwanej wartości",
+ "weightedDown": "{label}: liczyło się w stronę niższej oczekiwanej wartości",
"caveat": "To porównanie z twoimi wcześniejszymi dniami, nie stwierdzenie, jak doszło do takiego przebiegu dnia.",
"learning": "Faza uczenia: zapisano {entries} z {threshold} dni. Porównanie pojawi się, gdy będzie ich dość.",
"noPattern": "W zapisanych dotąd {entries} dniach nic nie jest na tyle regularne, by porównać z nimi dzień. Gdy zapisujesz też kontekst dnia, więcej da się porównać.",
@@ -4064,15 +4223,15 @@
},
"derived": {
"coverage": {
- "summary": "{present} z {required} danych · {percent}%",
+ "summary": "{present} z {required} danych ({percent}%)",
"ratioLabel": "{present}/{required}",
"historyDays": "Na podstawie {count} dni historii",
"missing": "Zapisuj te dane, aby uściślić wartość: {list}",
- "summaryLabelled": "{label} · {percent}%"
+ "summaryLabelled": "{label} ({percent}%)"
},
"scoreRing": {
"aria": "Wynik {score} na 100, {band}",
- "ariaProvisional": "Wynik jeszcze niedostępny — za mało danych",
+ "ariaProvisional": "Wynik jeszcze niedostępny, za mało danych",
"provisionalCaption": "Wciąż za mało danych",
"band": {
"green": "dobry",
@@ -4086,7 +4245,7 @@
"inputsLabel": "Dane wejściowe",
"noInputs": "Żadne dane nie potwierdzają jeszcze tej wartości.",
"methodLabel": "Metoda",
- "window": "Z ostatnich {days} dni · dane {source}",
+ "window": "Z ostatnich {days} dni danych {source}",
"asOf": "na dzień {date}",
"standardLink": "Standard: {name}"
},
@@ -4096,7 +4255,7 @@
"loadingLabel": "Wczytywanie parametrów życiowych",
"loadError": "Nie udało się teraz wczytać Twoich parametrów życiowych.",
"infoLabel": "Jak to jest obliczane?",
- "building": "Budowanie Twojego typowego zakresu — {count} z {target} dni",
+ "building": "Budowanie Twojego typowego zakresu: {count} z {target} dni",
"typicalRange": "Typowe dla Ciebie: {low}–{high}",
"fitnessYounger": "Kondycja jak u osoby ~{years} lat młodszej",
"fitnessOlder": "Kondycja ~{years} lat powyżej Twojego wieku",
@@ -4128,9 +4287,8 @@
"anatomy": {
"outOf": "/100",
"contributorsLabel": "Czynniki",
- "contributorValue": "{value} · {weight}% wagi",
+ "contributorValue": "{value}, waga {weight}%",
"contributorMissing": "brak danych",
- "disclaimer": "Orientacyjne — to nie ocena kliniczna.",
"insufficient": "Za mało danych, by to ocenić. Rejestruj dalej, a wynik pojawi się tutaj."
},
"composite": {
@@ -4149,77 +4307,75 @@
"READINESS": {
"title": "Gotowość",
"description": "Codzienny wskaźnik samopoczucia oparty na tym, jak twoje sygnały wypadają względem twojej własnej bazy.",
- "method": "Mieszanka 0–100 z odchylenia tętna spoczynkowego, odchylenia HRV (SDNN), jakości snu, stabilności częstości oddechu i stabilności nastroju — każde mierzone względem twojej osobistej bazy. Wagi są renormalizowane na komponentach wspieranych przez twoje dane. Wskaźnik samopoczucia, nie wynik regeneracji ani kliniczny.",
+ "method": "Mieszanka 0–100 z odchylenia tętna spoczynkowego, odchylenia HRV (SDNN), jakości snu, stabilności częstości oddechu i stabilności nastroju, każde mierzone względem twojej osobistej bazy. Wagi są renormalizowane na komponentach wspieranych przez twoje dane. Wskaźnik samopoczucia, nie wynik regeneracji ani kliniczny.",
"component": {
"rhr": "Tętno spoczynkowe",
"hrv": "HRV (SDNN)",
"sleep": "Sen",
"respiratory": "Częstość oddechu",
"mood": "Stabilność nastroju"
- },
- "caveat": "Wskaźnik dobrostanu, nie ocena regeneracji ani wynik kliniczny."
+ }
},
"VITALS_BASELINE": {
- "method": "Twój osobisty typowy zakres to mediana Twoich dziennych wartości w danym okresie ± odporne rozproszenie (skalowane medianowe odchylenie bezwzględne). Śledzi TWOJE odczyty, a nie normę populacyjną — wartość w obrębie pasma jest więc typowa dla Ciebie."
+ "method": "Twój osobisty typowy zakres to mediana Twoich dziennych wartości w danym okresie ± odporne rozproszenie (skalowane medianowe odchylenie bezwzględne). Śledzi TWOJE odczyty, a nie normę populacyjną, więc wartość w obrębie pasma jest typowa dla Ciebie."
},
"HRV_BALANCE": {
"method": "Twoja niedawna średnia HRV (SDNN) na tle Twojej własnej ruchomej linii bazowej (mediana ± odporne rozproszenie). Zrównoważone oznacza, że mieści się w Twoim osobistym zakresie; pasmo budowane jest z Twoich danych, nigdy z progu populacyjnego."
},
"BMI": {
- "method": "Masa ÷ wzrost², sklasyfikowane według kategorii WHO dla dorosłych (niedowaga / norma / nadwaga / otyłość). To tylko wskaźnik przesiewowy — nie odróżnia mięśni od tłuszczu."
+ "method": "Masa ÷ wzrost², sklasyfikowane według kategorii WHO dla dorosłych (niedowaga / norma / nadwaga / otyłość). To tylko wskaźnik przesiewowy, który nie odróżnia mięśni od tłuszczu."
},
"FITNESS_AGE": {
- "method": "Twoje VO₂max odniesione do percentyli wydolności krążeniowo-oddechowej według wieku i płci, wyrażone jako różnica wieku sprawnościowego. VO₂max to szacunek Twojego urządzenia — tutaj nigdy nie jest przeliczany ponownie."
+ "method": "Twoje VO₂max odniesione do percentyli wydolności krążeniowo-oddechowej według wieku i płci, wyrażone jako różnica wieku sprawnościowego. VO₂max to szacunek Twojego urządzenia, tutaj nigdy nie jest przeliczany ponownie."
},
"VASCULAR_AGE_DELTA": {
"method": "Szacunek wieku naczyniowego / prędkości fali tętna z Twojego urządzenia porównany z Twoim wiekiem metrykalnym, wyrażony jako różnica w latach. Szacunek pochodzi z urządzenia; HealthLog jedynie go przedstawia.",
- "caveat": "Własny szacunek Twojego urządzenia, jedynie przedstawiony — opisowy, nie kliniczna ocena naczyniowa.",
+ "caveat": "Własny szacunek Twojego urządzenia, jedynie przedstawiony: opisowy, nie kliniczna ocena naczyniowa.",
"description": "Jak szacunek wieku naczyniowego Twojego urządzenia wypada na tle Twojego wieku metrykalnego."
},
"COINCIDENT_DEVIATION": {
"method": "Sygnał pojawiający się, gdy co najmniej dwa Twoje parametry życiowe wykraczają tego samego dnia poza swój osobisty typowy zakres. Każde pasmo to odporna mediana ± k·MAD Twojej historii, więc dostosowuje się do Ciebie, a nie do progu populacyjnego.",
- "caveat": "Sygnał z Twoich własnych linii bazowych — opisowy, nie diagnoza ani alarm kliniczny."
+ "caveat": "Sygnał z Twoich własnych linii bazowych: opisowy, nie diagnoza ani alarm kliniczny."
},
"RECOVERY_SCORE": {
- "method": "Nocny wskaźnik regeneracji 0–100 łączący Twoje HRV, tętno spoczynkowe i sen względem Twojej linii bazowej. Wyższy oznacza lepszą regenerację. Liczony raz na noc i tu tylko odczytywany — nigdy nieprzeliczany na bieżąco.",
- "caveat": "Opisowy wskaźnik dobrostanu, nie kliniczna ani treningowa miara regeneracji.",
+ "method": "Nocny wskaźnik regeneracji 0–100 łączący Twoje HRV, tętno spoczynkowe i sen względem Twojej linii bazowej. Wyższy oznacza lepszą regenerację. Liczony raz na noc i tu tylko odczytywany, nigdy nieprzeliczany na bieżąco.",
"description": "Nocny wskaźnik regeneracji z Twojego HRV, tętna spoczynkowego i snu."
},
"STRESS_SCORE": {
"method": "Nocny wskaźnik stresu 0–100 na podstawie kształtu Twojego HRV w ciągu dnia względem linii bazowej. Wyższy oznacza większe obciążenie fizjologiczne.",
- "caveat": "Wskaźnik pochodzący z HRV, a NIE pomiar stresu metodą EDA / reakcji skórno-galwanicznej — Twój zegarek nie ma czujnika EDA. Opisowy, nie kliniczny.",
"description": "Nocny wskaźnik stresu na podstawie kształtu Twojego HRV w ciągu dnia."
},
"STRAIN_SCORE": {
"method": "Nocny wskaźnik obciążenia 0–100 na podstawie Twojego obciążenia aktywnością ważonego tętnem (model impulsu treningowego typu TRIMP). Wyższy oznacza cięższy dzień.",
- "anchorPersonal": "Zakotwiczony w Twoim własnym niedawnym obciążeniu z dni treningowych — oceniany względem Twojego typowego wysiłku, więc naprawdę ciężki dzień dla Ciebie wypada wysoko.",
+ "anchorPersonal": "Zakotwiczony w Twoim własnym niedawnym obciążeniu z dni treningowych. Oceniany względem Twojego typowego wysiłku, więc naprawdę ciężki dzień dla Ciebie wypada wysoko.",
"anchorPopulation": "Oceniany względem ogólnego punktu odniesienia, gdy dopiero budujesz historię treningów; gdy zbierzesz jej wystarczająco, przełącza się na Twój własny typowy wysiłek.",
- "caveat": "Opisowy wskaźnik obciążenia aktywnością, nie kliniczna ani treningowa miara obciążenia.",
- "description": "Nocny wskaźnik obciążenia aktywnością z Twojego wysiłku ważonego tętnem, względem Twojego typowego wysiłku."
+ "description": "Nocny wskaźnik obciążenia aktywnością z Twojego wysiłku ważonego tętnem, względem Twojego typowego wysiłku.",
+ "deviceCaption": "Dzienne obciążenie z Twojego urządzenia: {value} z {max}",
+ "deviceSource": "Dopóki HealthLog nie obliczy w nocy własnej wartości, karta pokazuje dzienne obciążenie z Twojego urządzenia, przeliczone na 0–100."
},
"WRIST_TEMPERATURE_BASELINE": {
- "method": "Twoja typowa nocna temperatura nadgarstka to mediana Twoich nocnych odczytów w danym okresie, ± solidny rozrzut (przeskalowane medianowe odchylenie bezwzględne). Śledzi TWOJE odczyty — ostatnia noc jest pokazana jako odchylenie od Twojej własnej bazy, a nie od normy populacyjnej.",
- "caveat": "Tylko osobiste pasmo odchylenia — opisowe, nie sygnał choroby, gorączki ani fazy cyklu. Interpretujesz je samodzielnie."
+ "method": "Twoja typowa nocna temperatura nadgarstka to mediana Twoich nocnych odczytów w danym okresie, ± solidny rozrzut (przeskalowane medianowe odchylenie bezwzględne). Śledzi TWOJE odczyty: ostatnia noc jest pokazana jako odchylenie od Twojej własnej bazy, a nie od normy populacyjnej.",
+ "caveat": "Tylko osobiste pasmo odchylenia: opisowe, nie sygnał choroby, gorączki ani fazy cyklu. Interpretujesz je samodzielnie."
},
"STAIR_ASCENT_SPEED_BASELINE": {
- "method": "Twoje typowe tempo wchodzenia po schodach to mediana Twoich dziennych wartości w danym okresie, ± solidny rozrzut (przeskalowane medianowe odchylenie bezwzględne). Pasmo jest budowane z Twoich danych — nie ma populacyjnego progu prędkości na schodach, bo tempo zależy od geometrii schodów i długości nóg.",
- "caveat": "Osobiste pasmo trendu mobilności — opisowe, nie ocena kruchości ani ryzyka upadku."
+ "method": "Twoje typowe tempo wchodzenia po schodach to mediana Twoich dziennych wartości w danym okresie, ± solidny rozrzut (przeskalowane medianowe odchylenie bezwzględne). Pasmo jest budowane z Twoich danych. Nie ma populacyjnego progu prędkości na schodach, bo tempo zależy od geometrii schodów i długości nóg.",
+ "caveat": "Osobiste pasmo trendu mobilności: opisowe, nie ocena kruchości ani ryzyka upadku."
},
"STAIR_DESCENT_SPEED_BASELINE": {
- "method": "Twoje typowe tempo schodzenia po schodach to mediana Twoich dziennych wartości w danym okresie, ± solidny rozrzut (przeskalowane medianowe odchylenie bezwzględne). Pasmo jest budowane z Twoich danych — nie ma populacyjnego progu prędkości na schodach, bo tempo zależy od geometrii schodów i długości nóg.",
- "caveat": "Osobiste pasmo trendu mobilności — opisowe, nie ocena kruchości ani ryzyka upadku."
+ "method": "Twoje typowe tempo schodzenia po schodach to mediana Twoich dziennych wartości w danym okresie, ± solidny rozrzut (przeskalowane medianowe odchylenie bezwzględne). Pasmo jest budowane z Twoich danych. Nie ma populacyjnego progu prędkości na schodach, bo tempo zależy od geometrii schodów i długości nóg.",
+ "caveat": "Osobiste pasmo trendu mobilności: opisowe, nie ocena kruchości ani ryzyka upadku."
},
"SIX_MINUTE_WALK_BAND": {
- "method": "SZACOWANY przez Twoje urządzenie dystans sześciominutowego marszu odniesiony do wzorca Enrighta i Sherrilla dla Twojego wieku, wzrostu, wagi i płci, jako procent wartości przewidywanej. Dystans to szacunek urządzenia — nigdy nie jest tu przeliczany. Bez Twojego wieku, wzrostu, wagi i płci procent jest ukryty i pokazywane są tylko dystans i trend.",
- "caveat": "Szacunek odniesiony do opublikowanego wzorca — sygnał wydolności funkcjonalnej, nie kliniczny test sześciominutowego marszu ani diagnoza. Wzorzec opracowano dla wieku 40–80 lat; poza tym zakresem wartość przewidywana jest ekstrapolacją."
+ "method": "SZACOWANY przez Twoje urządzenie dystans sześciominutowego marszu odniesiony do wzorca Enrighta i Sherrilla dla Twojego wieku, wzrostu, wagi i płci, jako procent wartości przewidywanej. Dystans to szacunek urządzenia i nigdy nie jest tu przeliczany. Bez Twojego wieku, wzrostu, wagi i płci procent jest ukryty i pokazywane są tylko dystans i trend.",
+ "caveat": "Szacunek odniesiony do opublikowanego wzorca: sygnał wydolności funkcjonalnej, nie kliniczny test sześciominutowego marszu ani diagnoza. Wzorzec opracowano dla wieku 40–80 lat; poza tym zakresem wartość przewidywana jest ekstrapolacją."
},
"TRAJECTORY": {
"method": "Prosta dopasowana metodą najmniejszych kwadratów do Twoich ostatnich wartości dziennych i rzutowana w przód na krótki horyzont, z klasycznym przedziałem predykcji, który poszerza się tym bardziej, im dalej sięga. Obliczana tylko wtedy, gdy ostatni trend jest dość wyraźny, a Twoja historia wystarczająco długa i aktualna; w przeciwnym razie linia nie jest rysowana.",
- "caveat": "Rzut Twojego własnego ostatniego trendu — nie prognoza, cel ani kliniczne przewidywanie. Sprawdza się tylko, jeśli ostatni wzorzec się utrzyma; zacieniony pas pokazuje, jak szybko ta pewność maleje."
+ "caveat": "Rzut Twojego własnego ostatniego trendu, nie prognoza, cel ani kliniczne przewidywanie. Sprawdza się tylko, jeśli ostatni wzorzec się utrzyma; zacieniony pas pokazuje, jak szybko ta pewność maleje."
},
"SAME_TIME_BASELINE": {
"method": "Twoja dzisiejsza suma narastająca zestawiona z medianą tej samej godziny lokalnej z ostatnich czterech tygodni, z odpornym pasmem mediana ± MAD wokół niej. Liczą się tylko dni z wystarczającą liczbą zapisów, a porównanie nie pojawia się, dopóki nie ma ich co najmniej czternastu.",
- "caveat": "Porównanie z twoją własną niedawną historią, a nie cel. Mówi, gdzie dziś jesteś względem swojego zwykłego wzorca o tej godzinie — i nic o tym, czy ten wzorzec jest dla ciebie właściwy."
+ "caveat": "Porównanie z twoją własną niedawną historią, a nie cel. Mówi, gdzie dziś jesteś względem swojego zwykłego wzorca o tej godzinie, i nic o tym, czy ten wzorzec jest dla ciebie właściwy."
}
},
"scores": {
@@ -4230,7 +4386,9 @@
"recovery": "Wynik regeneracji",
"recoveryCrossLink": "Zobacz dane regeneracji i obciążenia z Twojego urządzenia",
"stress": "Stres",
- "strain": "Obciążenie"
+ "strain": "Obciążenie",
+ "historyTitle": "Przebieg",
+ "historyWindow": "Ostatnie {days} dni"
},
"coincident": {
"label": "Kilka parametrów poza bazą",
@@ -4240,31 +4398,31 @@
"allClear": "Dziś wszystkie Twoje parametry życiowe mieszczą się w osobistym zakresie.",
"allClearMeta": "Sprawdzono {count} Twoich parametrów względem Twojego typowego zakresu.",
"watch": "Dziś jeden parametr jest poza swoim zwykłym zakresem.",
- "watchVital": "Poza zakresem: {vital}. Często to jednorazowy odczyt — warto obserwować przez kilka dni.",
+ "watchVital": "Poza zakresem: {vital}. Często to jednorazowy odczyt, warto obserwować przez kilka dni.",
"firedHeadline": "Dziś {count} Twoich parametrów życiowych jest poza osobistym zakresem.",
- "factors": "Możliwe czynniki — nigdy przyczyna: intensywny trening, słaby sen, alkohol, wysokość, stres lub choroba. To wskazówka z Twoich własnych baz, nie diagnoza.",
- "building": "Budujemy Twoje osobiste bazy — zarejestruj jeszcze kilka dni, a to się pojawi.",
+ "factors": "Możliwe czynniki (nigdy przyczyna): intensywny trening, słaby sen, alkohol, wysokość, stres lub choroba. To wskazówka z Twoich własnych baz, nie diagnoza.",
+ "building": "Budujemy Twoje osobiste bazy. Zarejestruj jeszcze kilka dni, a to się pojawi.",
"illnessExplained": "Te odchylenia prawdopodobnie wynikają z zapisanego schorzenia."
},
"trajectory": {
"cardTitle": "Krótkoterminowy rzut",
- "headline": "Jeśli ten wzorzec się utrzyma — następne {days} dni",
+ "headline": "Jeśli ten wzorzec się utrzyma: następne {days} dni",
"range": "Rzutowane na około {low}–{high} {unit} za {days} dni.",
"caveat": "Rzut Twojego własnego ostatniego trendu, nie prognoza. Zacieniony pas poszerza się, pokazując, jak szybko rośnie niepewność.",
- "insufficient": "Jeszcze brak wystarczająco wyraźnego trendu do rzutowania — rejestruj dalej, a rzut pojawi się, gdy ostatni wzorzec będzie dość wyraźny.",
+ "insufficient": "Jeszcze brak wystarczająco wyraźnego trendu do rzutowania. Rejestruj dalej, a rzut pojawi się, gdy ostatni wzorzec będzie dość wyraźny.",
"daysNeeded": "Potrzeba jeszcze około {count} dni rejestrowania, zanim pojawi się rzut."
},
"sameTime": {
"sectionTitle": "Dziś do tej pory",
"sectionSubtitle": "Twoje sumy narastające względem tego, co o tej godzinie jest u ciebie zwykłe",
- "learning": "Uczę się twojego zwykłego dnia — {count} z {target} dni",
+ "learning": "Uczę się twojego zwykłego dnia: {count} z {target} dni",
"framing": "Zwykle do {time}: {typical} {unit}",
"comparisonLabel": "Twój zwykły dzień"
}
},
"cardioFitness": {
"title": "Wydolność",
- "description": "Twoje VO₂ max w czasie — główna miara wydolności tlenowej.",
+ "description": "Twoje VO₂ max w czasie, główna miara wydolności tlenowej.",
"chartTitle": "VO₂ max (mL/(kg·min))",
"emptyState": {
"title": "Brak danych o wydolności",
@@ -4344,7 +4502,7 @@
"navBreathingDisturbances": "Oddychanie",
"rhythmEvents": {
"sectionTitle": "Powiadomienia zdrowotne urządzenia",
- "sectionIntro": "Powiadomienia, które zegarek wysłał samodzielnie. HealthLog jedynie je wyświetla — nie wykrywa ich, nie potwierdza ani nie interpretuje.",
+ "sectionIntro": "Powiadomienia, które zegarek wysłał samodzielnie. HealthLog jedynie je wyświetla: nie wykrywa ich, nie potwierdza ani nie interpretuje.",
"loadError": "Nie udało się wczytać powiadomień urządzenia.",
"disclaimer": "To alerty wygenerowane przez Twoje urządzenie za pomocą jego wbudowanego wykrywania. Są tu pokazywane wyłącznie dla Twojej informacji. To nie jest ocena medyczna wykonana przez HealthLog, a HealthLog nie diagnozuje żadnych schorzeń. Jeśli powiadomienie Cię niepokoi, skontaktuj się z lekarzem.",
"event": {
@@ -4389,13 +4547,13 @@
"detail": "Ukryte strony szczegółów znikają również z górnej nawigacji."
},
"customize": "Dostosuj Analizy",
- "regenerateRateLimited": "Osiągnięto limit odświeżeń — spróbuj ponownie później.",
- "regenerateUnavailable": "Nie otrzymano nowej analizy — spróbuj ponownie później.",
- "regenerateError": "Trwa odświeżanie — spróbuj ponownie za chwilę.",
+ "regenerateRateLimited": "Osiągnięto limit odświeżeń. Spróbuj ponownie później.",
+ "regenerateUnavailable": "Nie otrzymano nowej analizy. Spróbuj ponownie później.",
+ "regenerateError": "Trwa odświeżanie. Spróbuj ponownie za chwilę.",
"regenerateSettling": "Ocena jest wciąż przygotowywana",
"regenerateSettleFailed": "Nie udało się ukończyć odświeżania. Spróbuj ponownie później.",
"deviceScore": {
- "learning": "Wciąż poznajemy Twoją linię bazową – kilka kolejnych nocy uzupełni trend.",
+ "learning": "Wciąż poznajemy Twoją linię bazową. Kilka kolejnych nocy uzupełni trend.",
"average": "Średnia z 30 dni {value}",
"unitScore": "pkt",
"unitMinutes": "min",
@@ -4408,14 +4566,14 @@
},
"recovery": {
"title": "Regeneracja",
- "description": "Jak dobrze Twój organizm się zregenerował – sygnały regeneracji, obciążenia i load, które Twoje urządzenie rejestruje w nocy.",
+ "description": "Jak dobrze Twój organizm się zregenerował: sygnały regeneracji, obciążenia i load, które Twoje urządzenie rejestruje w nocy.",
"scoreCrossLink": "Zobacz łączny wynik regeneracji",
"emptyTitle": "Brak danych o regeneracji",
"empty": "Brak danych o regeneracji. Połącz konto WHOOP, Polar lub Oura, a te wyniki uzupełnią się po synchronizacji urządzenia.",
"block": {
- "ansChargeExplainer": "Jak naładowany jest Twój autonomiczny układ nerwowy na dany dzień — odczyt regeneracji oparty na nocnych sygnałach serca i oddechu.",
+ "ansChargeExplainer": "Jak naładowany jest Twój autonomiczny układ nerwowy na dany dzień: odczyt regeneracji oparty na nocnych sygnałach serca i oddechu.",
"dayStrainExplainer": "Dzienne obciążenie sercowo-naczyniowe na skali wysiłku Twojego urządzenia. Odczytuj je w odniesieniu do regeneracji, nie jako cel sam w sobie.",
- "workoutStrainExplainer": "Jak wymagający był pojedynczy trening na tej samej skali wysiłku — koszt sercowo-naczyniowy tej sesji.",
+ "workoutStrainExplainer": "Jak wymagający był pojedynczy trening na tej samej skali wysiłku, czyli koszt sercowo-naczyniowy tej sesji.",
"cardioLoadExplainer": "Przybliżenie obciążenia treningowego, które śledzi nagromadzone obciążenie sercowo-naczyniowe. Porównuj je z własnymi ostatnimi dniami.",
"averageHeartRateExplainer": "Twoje średnie tętno w ciągu całego dnia lub cyklu, zgodnie z odczytem Twojego urządzenia.",
"maxHeartRateExplainer": "Twoje maksymalne tętno w ciągu całego dnia lub cyklu, zgodnie z odczytem Twojego urządzenia.",
@@ -4427,8 +4585,8 @@
"subtitle": "Nocne odchylenie od Twojej linii bazowej"
},
"resilience": {
- "explainer": "Jak dobrze Twój organizm równoważy codzienne obciążenie z regeneracją — odczytane z nocnych sygnałów z ostatnich dni.",
- "learning": "Wciąż poznajemy Twoją odporność — kilka kolejnych dni ustali poziom.",
+ "explainer": "Jak dobrze Twój organizm równoważy codzienne obciążenie z regeneracją, odczytane z nocnych sygnałów z ostatnich dni.",
+ "learning": "Wciąż poznajemy Twoją odporność. Kilka kolejnych dni ustali poziom.",
"band": {
"limited": "Ograniczona",
"adequate": "Wystarczająca",
@@ -4459,6 +4617,7 @@
"shiftUp": "{vital} przesunął się w górę względem Twojej ostatniej normy.",
"shiftDown": "{vital} przesunął się w dół względem Twojej ostatniej normy.",
"rangeContext": "Teraz {value}, zwykle {low}–{high}.",
+ "rangeContextTimeOfDay": "Teraz {value}, zwykle {low}–{high} o tej porze dnia.",
"shiftContext": "Około {before} wcześniej, {after} od tego czasu."
},
"breathingScreening": {
@@ -4472,7 +4631,7 @@
"nights": "Śledzono {count} nocy",
"indexLine": "ostatni średni indeks {value}",
"eventsFlagged": "{count} zdarzeń oznaczonych przez urządzenie",
- "disclaimer": "To sygnał przesiewowy, a nie diagnoza — skonsultuj się z lekarzem."
+ "disclaimer": "To sygnał przesiewowy, a nie diagnoza. Skonsultuj się z lekarzem."
},
"labsChanges": {
"sectionTitle": "Od ostatnich badań",
@@ -4488,14 +4647,14 @@
"navWaistToHeight": "Talia/wzrost",
"gripStrength": {
"title": "Siła chwytu",
- "description": "Maksymalna siła chwytu dłoni — solidny wskaźnik ogólnej siły ciała. Poniżej progu zależnego od płci (mężczyźni 27 kg, kobiety 16 kg) sygnalizuje prawdopodobną sarkopenię; odczyt prowadzi Twój własny trend.",
+ "description": "Maksymalna siła chwytu dłoni, solidny wskaźnik ogólnej siły ciała. Poniżej progu zależnego od płci (mężczyźni 27 kg, kobiety 16 kg) sygnalizuje prawdopodobną sarkopenię; odczyt prowadzi Twój własny trend.",
"chartTitle": "Siła chwytu",
"emptyState": {
"title": "Brak pomiarów siły chwytu",
"description": "Zapisz pomiar z dynamometru ręcznego (zanotuj najlepszy z kilku ściśnięć), a trend pojawi się tutaj.",
"cta": "Zapisz siłę chwytu"
},
- "coachPrefill": "Nie zapisałem jeszcze siły chwytu — co mówi o zdrowiu moich mięśni i jak ją zmierzyć oraz poprawić?"
+ "coachPrefill": "Nie zapisałem jeszcze siły chwytu. Co mówi o zdrowiu moich mięśni i jak ją zmierzyć oraz poprawić?"
},
"pain": {
"title": "Ból (0–10)",
@@ -4506,29 +4665,29 @@
"description": "Zapisz, jak odczuwasz ból teraz, w skali 0–10, a trend pojawi się tutaj.",
"cta": "Zapisz ocenę bólu"
},
- "coachPrefill": "Nie zapisałem jeszcze ocen bólu — jak śledzić ból w skali 0–10 i co liczy się jako istotna zmiana?"
+ "coachPrefill": "Nie zapisałem jeszcze ocen bólu. Jak śledzić ból w skali 0–10 i co liczy się jako istotna zmiana?"
},
"waist": {
"title": "Obwód talii",
- "description": "Obwód talii w centymetrach — wskaźnik tłuszczu brzusznego. Próg zwiększonego ryzyka WHO zależy od płci (mężczyźni 94 cm, kobiety 80 cm); odczyt prowadzi Twój własny trend.",
+ "description": "Obwód talii w centymetrach, wskaźnik tłuszczu brzusznego. Próg zwiększonego ryzyka WHO zależy od płci (mężczyźni 94 cm, kobiety 80 cm); odczyt prowadzi Twój własny trend.",
"chartTitle": "Obwód talii",
"emptyState": {
"title": "Brak pomiarów talii",
"description": "Zmierz talię w połowie odległości między najniższym żebrem a kością biodrową, na końcu normalnego wydechu, i zapisz tutaj.",
"cta": "Zapisz obwód talii"
},
- "coachPrefill": "Nie zapisałem jeszcze pomiarów talii — jak prawidłowo zmierzyć talię i co oznacza ta liczba dla mojego zdrowia?"
+ "coachPrefill": "Nie zapisałem jeszcze pomiarów talii. Jak prawidłowo zmierzyć talię i co oznacza ta liczba dla mojego zdrowia?"
},
"waistToHeight": {
"title": "Stosunek talii do wzrostu",
- "description": "Twoja talia podzielona przez wzrost — najbardziej uniwersalna pojedyncza liczba. Utrzymanie go poniżej 0,5 (talia mniejsza niż połowa wzrostu) to prosty cel.",
+ "description": "Twoja talia podzielona przez wzrost: najbardziej uniwersalna pojedyncza liczba. Utrzymanie go poniżej 0,5 (talia mniejsza niż połowa wzrostu) to prosty cel.",
"chartTitle": "Stosunek talii do wzrostu",
"emptyState": {
"title": "Brak stosunku talii do wzrostu",
"description": "Zapisz stosunek talii do wzrostu, a trend pojawi się tutaj. Zasada: utrzymuj talię poniżej połowy wzrostu.",
"cta": "Zapisz stosunek talii do wzrostu"
},
- "coachPrefill": "Nie zapisałem jeszcze stosunku talii do wzrostu — dlaczego to przydatna liczba i jak utrzymać ją poniżej 0,5?"
+ "coachPrefill": "Nie zapisałem jeszcze stosunku talii do wzrostu. Dlaczego to przydatna liczba i jak utrzymać ją poniżej 0,5?"
},
"ecg": {
"sectionTitle": "Zapisy EKG",
@@ -4569,9 +4728,9 @@
"emptyCta": "Połącz urządzenie"
},
"intradayPulse": {
- "title": "Przebieg Twojego dnia",
- "caption": "Twoje tętno w ciągu dnia, uśrednione w 10-minutowych odcinkach.",
- "hourlyNote": "Pokazano średnią godzinową dla tego dnia — ostatnie dni pokazują pełny, 10-minutowy szczegół.",
+ "title": "Tętno w ciągu dnia",
+ "caption": "Uśrednione w krokach co 10 minut.",
+ "hourlyNote": "Pokazano średnią godzinową dla tego dnia. Ostatnie dni pokazują pełny, 10-minutowy szczegół.",
"empty": "Za mało pomiarów z tego dnia, aby pokazać jego przebieg.",
"coverageOne": "Na podstawie 1 pomiaru w ciągu {hours}h.",
"coverageMany": "Na podstawie {count} pomiarów w ciągu {hours}h.",
@@ -4580,10 +4739,10 @@
"previousDay": "Poprzedni dzień",
"nextDay": "Następny dzień",
"tension": {
- "morning": "Utrzymujący się podwyższony spoczynek rano — możliwe napięcie.",
- "afternoon": "Utrzymujący się podwyższony spoczynek po południu — możliwe napięcie.",
- "evening": "Utrzymujący się podwyższony spoczynek wieczorem — możliwe napięcie.",
- "night": "Utrzymujący się podwyższony spoczynek w nocy — możliwe napięcie."
+ "morning": "Utrzymujący się podwyższony spoczynek rano: możliwe napięcie.",
+ "afternoon": "Utrzymujący się podwyższony spoczynek po południu: możliwe napięcie.",
+ "evening": "Utrzymujący się podwyższony spoczynek wieczorem: możliwe napięcie.",
+ "night": "Utrzymujący się podwyższony spoczynek w nocy: możliwe napięcie."
}
},
"navEcg": "EKG",
@@ -4594,6 +4753,19 @@
"settingsBody": "HealthLog działa w pełni bez AI. Podłącz dostawcę poniżej, jeśli chcesz codzienne podsumowanie, krótkie notatki o pomiarach i Coacha.",
"action": "Skonfiguruj AI",
"dismiss": "Nie teraz"
+ },
+ "bmiHeight": {
+ "coachPrefill": "Nie podałem jeszcze wzrostu. Dlaczego BMI ma znaczenie i co powinienem wiedzieć, zanim go ustawię?"
+ },
+ "medicationsEmpty": {
+ "coachPrefill": "Nie dodałem jeszcze żadnych leków. Co powinienem wiedzieć, zanim zacznę tu śledzić przestrzeganie zaleceń?"
+ },
+ "scoreHistory": {
+ "healthTitle": "Przebieg wyniku zdrowia",
+ "healthDescription": "Twój wynik zdrowia dzień po dniu, taki jak był pokazywany każdego dnia.",
+ "emptyDescription": "W tym okresie nie zapisano jeszcze żadnego wyniku.",
+ "usualRange": "Zacieniowane: twój zwykły zakres, od {lo} do {hi}.",
+ "seam": "Linia przerywa się tam, gdzie zmieniło się to, co liczy się do wyniku."
}
},
"thresholds": {
@@ -4623,14 +4795,15 @@
"metricBodyFat": "Tkanka tłuszczowa",
"metricSleep": "Czas snu",
"metricSteps": "Kroki/dzień",
- "metricGlucoseFasting": "Glikemia — na czczo",
- "metricGlucosePostprandial": "Glikemia — po posiłku",
- "metricGlucoseRandom": "Glikemia — dowolna",
- "metricGlucoseBedtime": "Glikemia — przed snem",
+ "metricGlucoseFasting": "Glikemia: na czczo",
+ "metricGlucosePostprandial": "Glikemia: po posiłku",
+ "metricGlucoseRandom": "Glikemia: dowolna",
+ "metricGlucoseBedtime": "Glikemia: przed snem",
"metricBodyWater": "Woda w organizmie",
"metricBoneMass": "Masa kostna",
"metricOxygenSaturation": "Saturacja tlenem (SpO₂)",
- "unsetExplanation": "Brak własnej wartości — aktywna jest wartość domyślna."
+ "unsetExplanation": "Brak własnej wartości, aktywna jest wartość domyślna.",
+ "cardTitle": "Zakresy według metryki"
},
"onboarding": {
"errorGeneric": "Coś poszło nie tak podczas kończenia konfiguracji. Spróbuj ponownie.",
@@ -4643,11 +4816,11 @@
"steps": {
"dashboardOverview": {
"title": "Twój pulpit",
- "body": "Pulpit zbiera wszystkie moduły na jednym ekranie. Przytrzymaj kafelek, aby go przestawić lub ukryć (Ustawienia → Układ pulpitu). Kafelek jest wart tyle, ile dane za nim — kolejne moduły je wypełniają."
+ "body": "Pulpit zbiera wszystkie moduły na jednym ekranie. Przytrzymaj kafelek, aby go przestawić lub ukryć (Ustawienia → Układ pulpitu). Kafelek jest wart tyle, ile dane za nim, a kolejne moduły je wypełniają."
},
"quickAdd": {
"title": "Zapisane w kilka sekund",
- "body": "Przez „+ Dodaj” zapiszesz każdy pomiar w sekundy — wagę, ciśnienie, tętno, glukozę. Ukryta moc: datowanie wstecz zapomnianych wartości. Wszystko, co tu wpiszesz, zasila trendy, wynik zdrowia i analizy AI."
+ "body": "Przez „+ Dodaj” zapiszesz każdy pomiar w sekundy: wagę, ciśnienie, tętno, glukozę. Ukryta moc: datowanie wstecz zapomnianych wartości. Wszystko, co tu wpiszesz, zasila trendy, wynik zdrowia i analizy AI."
},
"measurements": {
"title": "Pomiary i zakresy docelowe",
@@ -4659,7 +4832,7 @@
},
"labs": {
"title": "Wyniki laboratoryjne",
- "body": "Laboratorium gromadzi biomarkery z zakresami referencyjnymi — morfologia, cholesterol, HbA1c. Atut: import i eksport FHIR. Wartości poza zakresem wracają jako dowody w analizach i w raporcie dla lekarza."
+ "body": "Laboratorium gromadzi biomarkery z zakresami referencyjnymi: morfologia, cholesterol, HbA1c. Atut: import i eksport FHIR. Wartości poza zakresem wracają jako dowody w analizach i w raporcie dla lekarza."
},
"illness": {
"title": "Dziennik chorób",
@@ -4675,15 +4848,15 @@
},
"mood": {
"title": "Nastrój",
- "body": "Nastrój zapisuje samopoczucie i tagi w dwie sekundy. Atut: własne tagi. Trendy nastroju korelują ze snem, lekami i cyklem — Coach dostrzega wzorce."
+ "body": "Nastrój zapisuje samopoczucie i tagi w dwie sekundy. Atut: własne tagi. Trendy nastroju korelują ze snem, lekami i cyklem, a Coach dostrzega wzorce."
},
"insights": {
"title": "Analizy",
- "body": "Analizy to serce aplikacji: dynamiczna analiza oparta na dowodach zamiast ogólnych porad — każde stwierdzenie wskazuje na Twoje realne dane. Ukryte: układ można dostosować. Analizy łączą wszystko, co dostarczają moduły powyżej."
+ "body": "Analizy to serce aplikacji: dynamiczna analiza oparta na dowodach zamiast ogólnych porad, a każde stwierdzenie wskazuje na Twoje realne dane. Ukryte: układ można dostosować. Analizy łączą wszystko, co dostarczają moduły powyżej."
},
"coach": {
"title": "Coach",
- "body": "Coach odpowiada na pytania o Twoje dane w dialogu, korzystając z wywiadu, leków i wyników jako kontekstu. Dostawcę AI wybierasz sam (własny klucz lub lokalnie). Niczego nie przelicza — czyta te same liczby co pulpit."
+ "body": "Coach odpowiada na pytania o Twoje dane w dialogu, korzystając z wywiadu, leków i wyników jako kontekstu. Dostawcę AI wybierasz sam (własny klucz lub lokalnie). Niczego nie przelicza, czyta te same liczby co pulpit."
},
"integrations": {
"title": "Integracje",
@@ -4691,15 +4864,15 @@
},
"export": {
"title": "Raport dla lekarza",
- "body": "Raport dla lekarza łączy pomiary, leki, wyniki laboratoryjne i badania w PDF lub FHIR na wizytę. Tu zbiegają się wszystkie moduły — pełny raport powstaje tylko z zadbanych danych."
+ "body": "Raport dla lekarza łączy pomiary, leki, wyniki laboratoryjne i badania w PDF lub FHIR na wizytę. Tu zbiegają się wszystkie moduły, więc pełny raport powstaje tylko z zadbanych danych."
},
"achievements": {
"title": "Osiągnięcia",
- "body": "Osiągnięcia nagradzają konsekwencję — passy i kamienie milowe. Mała pętla motywacji, dzięki której łatwiej wytrwać."
+ "body": "Osiągnięcia nagradzają konsekwencję: passy i kamienie milowe. Mała pętla motywacji, dzięki której łatwiej wytrwać."
},
"wrapUp": {
"title": "To koniec przewodnika",
- "body": "Ten przewodnik pokazuje się tylko raz — nie otworzy się ponownie sam. Wszystko, co pokazał, zostaje na miejscu, a funkcje możesz włączać i wyłączać w dowolnym momencie w Ustawienia → Moduły."
+ "body": "Ten przewodnik pokazuje się tylko raz i nie otworzy się ponownie sam. Wszystko, co pokazał, zostaje na miejscu, a funkcje możesz włączać i wyłączać w dowolnym momencie w Ustawienia → Moduły."
}
}
},
@@ -4730,7 +4903,7 @@
"title": "Wszystko gotowe",
"body": "Witaj na pokładzie. Pulpit czeka na Ciebie.",
"returnCta": "Otwórz pulpit",
- "learning": "Niektóre wnioski — glukoza, rytm snu, trendy ciśnienia, cykl — wyostrzają się w ciągu pierwszego tygodnia lub dwóch, w miarę gromadzenia danych. Puste kafelki oznaczają teraz tylko, że wciąż poznajemy Twój punkt odniesienia.",
+ "learning": "Niektóre wnioski (glukoza, rytm snu, trendy ciśnienia, cykl) wyostrzają się w ciągu pierwszego tygodnia lub dwóch, w miarę gromadzenia danych. Puste kafelki oznaczają teraz tylko, że wciąż poznajemy Twój punkt odniesienia.",
"connectCta": "Połącz urządzenie",
"logCta": "Zapisz pierwszy pomiar",
"importCta": "Importuj istniejącą historię",
@@ -4747,33 +4920,33 @@
"anamnesis": {
"title": "O Twoim zdrowiu (opcjonalnie)",
"subtitle": "Trochę kontekstu, aby HealthLog rzetelniej odczytywał Twoje wyniki.",
- "intro": "Podziel się tym, czym chcesz — każde pole jest opcjonalne i możesz je zmienić w dowolnej chwili w Ustawieniach.",
+ "intro": "Podziel się tym, czym chcesz. Każde pole jest opcjonalne i możesz je zmienić w dowolnej chwili w Ustawieniach.",
"conditionsLabel": "Aktualne schorzenia",
"conditionsPlaceholder": "np. nadciśnienie, cukrzyca typu 2",
"conditionsHint": "Pomaga HealthLog ujmować Twoje pomiary w kontekście, zamiast oznaczać to, czego należy się spodziewać.",
"allergiesLabel": "Alergie lub nietolerancje",
"allergiesPlaceholder": "np. penicylina, laktoza",
- "privacyNote": "Przechowywane w postaci zaszyfrowanej i używane tylko po to, by dać Ci lepszy kontekst. Nie pytamy tu o leki — dodasz je w dowolnej chwili na stronie Leki."
+ "privacyNote": "Przechowywane w postaci zaszyfrowanej i używane tylko po to, by dać Ci lepszy kontekst. Nie pytamy tu o leki, dodasz je w dowolnej chwili na stronie Leki."
},
"ai": {
"panelTitle": "Zobacz, jak wyglądają insighty",
- "panelIntro": "Coach, codzienny briefing i oceny dla poszczególnych metryk zamieniają Twoje własne liczby w prosty język. Oto jak to wygląda — zanim cokolwiek podłączysz.",
+ "panelIntro": "Coach, codzienny briefing i oceny dla poszczególnych metryk zamieniają Twoje własne liczby w prosty język. Oto jak to wygląda, zanim cokolwiek podłączysz.",
"showSample": "Zobacz przykładowy briefing",
"sampleTitle": "Codzienny briefing",
"sampleTag": "Przykład",
"sampleAriaLabel": "Przykładowy codzienny briefing",
- "sampleBody": "Waga niższa o 1,2 kg w ciągu dwóch tygodni. Tętno spoczynkowe stabilne na 58 bpm. Ciśnienie w normie przez 6 z 7 dni — jedyny wysoki odczyt nastąpił po późnej, słonej kolacji.",
- "sampleCaption": "Tylko przykład — to nie są Twoje dane. Tak czyta się briefing, gdy insighty są włączone.",
+ "sampleBody": "Waga niższa o 1,2 kg w ciągu dwóch tygodni. Tętno spoczynkowe stabilne na 58 bpm. Ciśnienie w normie przez 6 z 7 dni; jedyny wysoki odczyt nastąpił po późnej, słonej kolacji.",
+ "sampleCaption": "Tylko przykład, to nie są Twoje dane. Tak czyta się briefing, gdy insighty są włączone.",
"ladderTitle": "Trzy sposoby, by to włączyć",
- "ladderLocalTitle": "Model lokalny — zapytania do AI zostają w Twojej sieci",
- "ladderLocalBody": "Skieruj HealthLog na Ollama lub LM Studio na własnej maszynie. Za darmo i w pełni prywatnie — spokojny wybór domyślny.",
+ "ladderLocalTitle": "Model lokalny: zapytania do AI zostają w Twojej sieci",
+ "ladderLocalBody": "Skieruj HealthLog na Ollama lub LM Studio na własnej maszynie. Za darmo i w pełni prywatnie: spokojny wybór domyślny.",
"ladderByokTitle": "Użyj własnego klucza",
"ladderByokBody": "Użyj własnego klucza OpenAI lub Anthropic. Działa na Twoim koncie i rozliczeniu, a poziom API domyślnie wyklucza Twoje dane z treningu.",
"ladderOauthTitle": "Zaloguj się przez subskrypcję",
- "ladderOauthBody": "Podłącz konto ChatGPT — bez kosztu za token, ale warunki poziomu konsumenckiego mogą wykorzystywać treści do treningu, więc to świadomy wybór, nigdy domyślny dla danych o zdrowiu.",
- "keylessLine": "HealthLog jest w pełni użyteczny bez AI. Twoje kafelki, wykresy, trendy i eksporty działają — AI dokłada tylko odczyt prostym językiem.",
+ "ladderOauthBody": "Podłącz konto ChatGPT. Bez kosztu za token, ale warunki poziomu konsumenckiego mogą wykorzystywać treści do treningu, więc to świadomy wybór, nigdy domyślny dla danych o zdrowiu.",
+ "keylessLine": "HealthLog jest w pełni użyteczny bez AI. Twoje kafelki, wykresy, trendy i eksporty działają; AI dokłada tylko odczyt prostym językiem.",
"setupCta": "Skonfiguruj (około minuty)",
- "sharedKeyNote": "Ta instancja udostępnia już wspólnego dostawcę AI, więc insighty działają dla Ciebie od razu — bez konfiguracji.",
+ "sharedKeyNote": "Ta instancja udostępnia już wspólnego dostawcę AI, więc insighty działają dla Ciebie od razu, bez konfiguracji.",
"offer": {
"title": "Ta instancja potrafi już napisać twoje podsumowanie zwykłym językiem.",
"body": "Operator udostępnia dostawcę, który działał po raz ostatni w ciągu ostatnich 24 godzin. Jedno dotknięcie włącza zwykły język; twoje dane trafią wtedy do tego jednego dostawcy i do nikogo więcej.",
@@ -4990,7 +5163,7 @@
"profileDescription": "Wzrost, data urodzenia i płeć odblokowują spersonalizowane cele.",
"profileCta": "Otwórz profil",
"measurementTitle": "Zarejestruj pierwszy pomiar",
- "measurementDescription": "Jeden pomiar aktywuje kafelek — trend rysuje się od 5. pomiaru.",
+ "measurementDescription": "Jeden pomiar aktywuje kafelek, a trend rysuje się od 5. pomiaru.",
"measurementCta": "Dodaj pomiar",
"medicationTitle": "Dodaj lek",
"medicationDescription": "Śledź przestrzeganie i otrzymuj przypomnienia dokładnie wtedy, kiedy ich potrzebujesz.",
@@ -4999,7 +5172,7 @@
"dataSourceDescription": "Automatycznie synchronizuj wagę, ciśnienie, sen i więcej z Withings, WHOOP, Oura, Polar, Nightscout, Fitbit lub Apple Health.",
"dataSourceCta": "Połącz",
"notificationsTitle": "Skonfiguruj powiadomienia",
- "notificationsDescription": "Wybierz, jak HealthLog ma ci przypominać — Telegram, ntfy lub Web Push.",
+ "notificationsDescription": "Wybierz, jak HealthLog ma ci przypominać: Telegram, ntfy lub Web Push.",
"notificationsCta": "Otwórz powiadomienia",
"insightsTitle": "Włącz insighty (opcjonalnie)",
"insightsDescription": "Dodaj model lokalny lub własny klucz AI, aby odblokować codzienny briefing i Coach.",
@@ -5088,11 +5261,6 @@
"brief": "Krótka",
"default": "Domyślna",
"detailed": "Szczegółowa",
- "defaultWindow": "Domyślny okres",
- "last7days": "Ostatnie 7 dni",
- "last30days": "Ostatnie 30 dni",
- "last90days": "Ostatnie 90 dni",
- "allTime": "Cały okres",
"excludedMetrics": "Wykluczone metryki",
"dataClusters": "Grupy danych",
"customClusters": "Użyj własnego wyboru grup",
@@ -5152,12 +5320,10 @@
"sections": {
"account": {
"title": "Konto",
- "description": "Profil, hasło, klucze dostępu i przewodnik wprowadzający.",
"subtitle": "Twój profil, tożsamość i dane logowania."
},
"integrations": {
"title": "Integracje",
- "description": "Połączone usługi, kanały dostarczania i priorytet źródeł w jednym miejscu.",
"subtitle": "Połącz i zarządzaj źródłami danych oraz urządzeniami.",
"connectionsHeading": "Połączenia",
"channelsHeading": "Kanały dostarczania",
@@ -5166,16 +5332,15 @@
},
"notifications": {
"title": "Powiadomienia",
- "description": "Wszystkie przypomnienia w jednym miejscu — co i kiedy oraz gdzie są dostarczane.",
"subtitle": "Wybierz, które przypomnienia otrzymujesz i gdzie są dostarczane.",
"remindersHeading": "Przypomnienia",
"matrixLink": "Powiadomienia wg kanału",
- "channelsLink": "Kanały dostarczania"
+ "channelsLink": "Kanały dostarczania",
+ "displayHeading": "Na telefonie"
},
"layout": {
- "title": "Wygląd",
- "description": "Ustal, jak wyglądają i są rozłożone pulpit, analizy i Twoje moduły śledzenia.",
- "backToHub": "Powrót do Wygląd",
+ "title": "Układ",
+ "backToHub": "Powrót do układu",
"dashboard": {
"title": "Pulpit",
"description": "Układ i kolejność kafelków."
@@ -5184,7 +5349,7 @@
"title": "Analizy",
"description": "Kolejność przeglądu i pigułek nawigacyjnych."
},
- "subtitle": "Dostosuj wygląd pulpitu, analiz i modułów śledzenia.",
+ "subtitle": "Wybierz, co pokazują pulpit, analizy i moduły śledzenia oraz w jakiej kolejności.",
"medications": {
"title": "Leki",
"description": "Widok listy i ręczna kolejność."
@@ -5204,6 +5369,10 @@
"mood": {
"title": "Tagi nastroju",
"description": "Grupy, tagi i zarchiwizowane tagi w wyborze nastroju."
+ },
+ "documents": {
+ "title": "Dokumenty",
+ "description": "Widok i układ miesięcy."
}
},
"dashboard": {
@@ -5211,15 +5380,13 @@
},
"thresholds": {
"title": "Cele",
- "description": "Dla każdej metryki: własny zakres docelowy zdrowych wartości.",
"subtitle": "Ustaw osobiste zakresy docelowe dla każdej metryki."
},
"sources": {
"title": "Priorytet źródeł",
- "description": "Wybierz, które źródło wygrywa, gdy kilka rejestruje tę samą metrykę tego samego dnia.",
"integrationsHint": "Źródła danych podłączysz lub odłączysz na stronie integracji.",
"integrationsHintLink": "Otwórz integracje",
- "cardTitle": "Priorytet źródeł według metryki",
+ "cardTitle": "Kolejność według metryki",
"help": "Gdy kilka źródeł dostarcza tę samą wartość tego samego dnia, źródło na górze liczy się jako kanoniczne.",
"resetDefaults": "Przywróć wartości domyślne",
"resetDefaultsTitle": "Zresetować kolejność źródeł?",
@@ -5232,7 +5399,7 @@
"resetSuccess": "Przywrócono domyślny priorytet źródeł",
"perMetricToggle": "Ustawienia dla poszczególnych metryk ({count})",
"perMetricHelp": "Metryki dostosowane względem domyślnej kolejności. Użyj tej listy, aby przywrócić pojedynczą metrykę do wartości domyślnej, nie zmieniając pozostałych.",
- "perMetricEmpty": "Brak indywidualnych ustawień — każda metryka korzysta z globalnej kolejności powyżej.",
+ "perMetricEmpty": "Brak indywidualnych ustawień. Każda metryka korzysta z globalnej kolejności powyżej.",
"resetMetric": "Resetuj",
"deviceTypeToggle": "Priorytet według typu urządzenia ({count})",
"deviceTypeHelp": "W obrębie źródła decyduje, które urządzenie wygrywa. Gdy Apple Watch i iPhone rejestrują kroki, wyżej sklasyfikowany typ urządzenia liczy się jako odczyt kanoniczny.",
@@ -5257,7 +5424,8 @@
"respiratoryRate": "Częstość oddechów",
"recovery": "Regeneracja",
"stress": "Stres",
- "sleepDebt": "Dług snu"
+ "sleepDebt": "Dług snu",
+ "hrvRmssd": "HRV (RMSSD)"
},
"sourceLabels": {
"WITHINGS": "Withings",
@@ -5274,7 +5442,8 @@
"MCP": "MCP",
"GOOGLE_HEALTH": "Google Health",
"STRAVA": "Strava",
- "EXTERNAL": "Urządzenie zewnętrzne"
+ "EXTERNAL": "Urządzenie zewnętrzne",
+ "HEALTH_CONNECT": "Health Connect"
},
"deviceLabels": {
"watch": "Zegarek",
@@ -5294,28 +5463,25 @@
"daysWithData": "{count} z {days} dni",
"moduleHint": "Synchronizacją steruje moduł spożycia składników odżywczych.",
"moduleHintLink": "Zarządzaj modułami",
- "detail": "Tylko do odczytu — wartości należą do aplikacji źródłowej."
+ "detail": "Tylko do odczytu: wartości należą do aplikacji źródłowej."
},
- "helpDetail": "Pozostałe pozostają zapisane na potrzeby audytu."
+ "helpDetail": "Pozostałe pozostają zapisane na potrzeby audytu. Widać tylko źródła, które połączono lub z których są dane.",
+ "noneInUse": "Nie ma jeszcze źródeł do uszeregowania. Połącz jedno w Integracjach albo wpisz wartość."
},
"ai": {
"title": "Dostawca AI",
- "description": "Dostawca, model i klucz API do analiz, a także Twój kontekst „o mnie”.",
"subtitle": "Wybierz dostawcę AI i użyj własnego klucza."
},
"api": {
- "title": "API & Tokens",
- "description": "Tokeny Bearer dla własnych skryptów i aplikacji — zapisuj pomiary lub przyjęcia leków z dowolnego miejsca.",
+ "title": "API i tokeny",
"subtitle": "Zarządzaj punktami końcowymi API i tokenami dostępu."
},
"advanced": {
"title": "Zaawansowane",
- "description": "Wyjście awaryjne: trwale usuń wszystkie dane zdrowotne albo zamknij konto.",
"subtitle": "Reset danych i usunięcie konta."
},
"export": {
"title": "Eksport i import",
- "description": "Pobierz swoje dane jako CSV lub pełną kopię zapasową JSON albo zaimportuj z Apple Health, JSON lub CSV. Raport dla lekarza znajdziesz teraz w Dokumentacji zdrowotnej.",
"otherOptionsHeading": "Inne opcje eksportu",
"downloadFailed": "Nie udało się pobrać eksportu. Spróbuj ponownie.",
"actions": {
@@ -5342,12 +5508,12 @@
"fullBackup": {
"title": "Pełna kopia zapasowa",
"description": "Cała dokumentacja w jednym pliku JSON.",
- "scopeNote": "Metadane dokumentów oraz podsumowanie AI, jeśli istnieje, są uwzględnione jako czysty tekst; oryginalne pliki dokumentów oraz dane tras GPS/próbek treningów nie są uwzględnione — pobierz je osobno z archiwum dokumentów lub eksportu treningów.",
+ "scopeNote": "Metadane dokumentów oraz podsumowanie AI, jeśli istnieje, są uwzględnione jako czysty tekst; oryginalne pliki dokumentów oraz dane tras GPS/próbek treningów nie są uwzględnione. Pobierz je osobno z archiwum dokumentów lub eksportu treningów.",
"encryptToggle": "Zaszyfruj hasłem-frazą",
"passphrasePlaceholder": "Hasło-fraza (min. 12 znaków)",
"passphraseHelp": "Wyprowadza klucz archiwum za pomocą Argon2id i pieczętuje je AES-256-GCM. Zapisz plik z rozszerzeniem .hlx.",
- "noRecoveryWarning": "Nie ma odzyskiwania — jeśli zgubisz tę frazę, archiwum nie da się otworzyć. Przechowuj ją bezpiecznie.",
- "detail": "Ten sam kształt co cotygodniowa kopia automatyczna. Niektóre rodzaje wpisów nie są jeszcze przenoszone — manifest pliku wskazuje, co zawiera.",
+ "noRecoveryWarning": "Nie ma odzyskiwania: jeśli zgubisz tę frazę, archiwum nie da się otworzyć. Przechowuj ją bezpiecznie.",
+ "detail": "Ten sam kształt co cotygodniowa kopia automatyczna. Niektóre rodzaje wpisów nie są jeszcze przenoszone; manifest pliku wskazuje, co zawiera.",
"contents": "Twoje pomiary, leki, nastrój, cykl, wyniki badań, epizody chorobowe, alergie, wywiad rodzinny, składniki odżywcze, treningi, Twój profil zdrowia i własne wskaźniki w jednym pliku JSON."
}
},
@@ -5362,7 +5528,7 @@
"dropHint": "lub kliknij, aby wybrać plik",
"fileInputLabel": "Wybierz plik export.zip z Apple Health",
"choose": "Wybierz plik",
- "idempotencyNote": "Ponowne przesłanie tego samego archiwum scala dane — nie tworzy duplikatów. Duże archiwa są przetwarzane w tle.",
+ "idempotencyNote": "Ponowne przesłanie tego samego archiwum scala dane i nie tworzy duplikatów. Duże archiwa są przetwarzane w tle.",
"uploading": "Przesyłanie…",
"uploadFailed": "Przesyłanie nie powiodło się. Spróbuj ponownie.",
"rateLimited": "Zbyt wiele przesłań. Poczekaj chwilę i spróbuj ponownie.",
@@ -5371,8 +5537,11 @@
"failureInterrupted": "Import został przerwany przed zakończeniem, zwykle przez restart serwera lub brak pamięci. Dane są bezpieczne; prześlij archiwum ponownie, aby spróbować jeszcze raz.",
"failureInsufficientMemory": "Limit pamięci serwera jest zbyt niski dla archiwum tej wielkości. Poproś osobę administrującą serwerem o zwiększenie limitu pamięci Node.js (NODE_OPTIONS=--max-old-space-size), a następnie prześlij archiwum ponownie.",
"rowsImported": "Zaimportowano dotąd {count} wierszy",
- "doneSummary": "Gotowe — zaimportowano {imported} z {read} rekordów, pominięto {skipped} rekordów klinicznych.",
+ "doneSummary": "Gotowe: zaimportowano {imported} z {read} rekordów, pominięto {skipped} rekordów klinicznych.",
"doneNothing": "Z tego archiwum nic nie zaimportowano: dotarło 0 z {read} rekordów.",
+ "writtenByHealthLogOne": "Pominięto {count} wpis, który HealthLog sam zapisał w Apple Health, ponieważ jest już w Twoich danych.",
+ "writtenByHealthLogOther": "Pominięto {count} wpisów, które HealthLog sam zapisał w Apple Health, ponieważ są już w Twoich danych.",
+ "writtenByHealthLogFew": "Pominięto {count} wpisy, które HealthLog sam zapisał w Apple Health, ponieważ są już w Twoich danych.",
"ecgResult": "EKG: wykryto {discovered}, zaimportowano {imported}, zaktualizowano {updated}, pominięto {skipped}, nie powiodło się {failed}. Błędy EKG nie usuwają obsługiwanych rekordów zaimportowanych z pliku export.xml.",
"estimateWarning": "Łączne wartości dla {count} dni oszacowano na podstawie największego źródła w eksporcie. Natywne statystyki Apple Health pozostają nadrzędne.",
"phase": {
@@ -5381,12 +5550,12 @@
"parsing": "Odczytywanie eksportu…",
"upserting": "Importowanie rekordów…"
},
- "skipUnsupported": "{count} próbek pominięto — typy danych, których HealthLog nie importuje.",
+ "skipUnsupported": "Pominięto {count} próbek: typy danych, których HealthLog nie importuje.",
"skipRefused": "{count} próbek nie udało się zapisać (nieczytelne, poza zakresem lub zapis się nie powiódł).",
"skipBreakdown": "Pokaż zestawienie ({count} typów)",
"skipBreakdownMore": "+ {count} kolejnych typów",
"cycleSummary": "{samples} próbek cyklu połączono w {days} dzienników dziennych.",
- "cycleModuleOff": "{count} próbek cyklu pominięto — śledzenie cyklu jest wyłączone dla tego konta.",
+ "cycleModuleOff": "Pominięto {count} próbek cyklu, bo śledzenie cyklu jest wyłączone dla tego konta.",
"cycleDaysFailed": "{count} dni cyklu nie udało się zapisać: {reason}"
},
"json": {
@@ -5471,8 +5640,8 @@
"failed": "Import nie udał się. Nic nie zostało zapisane.",
"columnsHeading": "Co się dzieje z każdą kolumną",
"jsonOnly": "tylko JSON",
- "previewLine": "Odczytane wiersze: {rows} · do zaimportowania: {queued} · odrzucone: {refused}",
- "submittedLine": "Odczytane wiersze: {rows} · w kolejce do importu: {queued}",
+ "previewLine": "Odczytane wiersze: {rows}, do zaimportowania: {queued}, odrzucone: {refused}",
+ "submittedLine": "Odczytane wiersze: {rows}, w kolejce do importu: {queued}",
"unmatched": "Żaden zapisany lek nie odpowiada: {names}",
"ambiguous": "Odpowiada więcej niż jednemu lekowi: {names}",
"mirrored": "Skopiowany z innej aplikacji, więc nie można dodać dawki: {names}",
@@ -5508,18 +5677,59 @@
"tooManyRows": "Ten plik ma więcej wierszy, niż może przyjąć jeden import. Podziel go i zaimportuj części."
},
"detail": "Jeden plik obejmuje wszystkie leki, które zawiera."
+ },
+ "healthConnect": {
+ "title": "Eksport z Health Connect",
+ "description": "Zaimportuj plik ZIP, który aplikacja Health Connect na Androidzie tworzy przy eksporcie danych.",
+ "choose": "Wybierz plik ZIP",
+ "uploading": "Przesyłanie…",
+ "uploadFailed": "Przesyłanie nie powiodło się. Spróbuj ponownie.",
+ "tooLarge": "Plik jest większy, niż przyjmuje ten serwer.",
+ "doneSummary": "Zaimportowano wartości: {count}.",
+ "failed": "Import nie powiódł się: {reason}",
+ "phase": {
+ "queued": "Czeka na start",
+ "unpacking": "Rozpakowywanie pliku",
+ "parsing": "Odczytywanie wpisów",
+ "upserting": "Zapisywanie wartości",
+ "done": "Gotowe",
+ "failed": "Niepowodzenie"
+ },
+ "steps": {
+ "export": "Na telefonie z Androidem otwórz Health Connect, przejdź do Zarządzaj danymi, potem Eksport i import, i rozpocznij eksport.",
+ "cloud": "Health Connect zapisuje eksport tylko w aplikacji chmurowej, takiej jak Google Drive, Dropbox czy Nextcloud, nigdy w samym telefonie. Wybierz tam jedną z nich.",
+ "upload": "Pobierz plik ZIP z tej chmury na to urządzenie i prześlij go tutaj."
+ },
+ "dropLabel": "Upuść tutaj plik ZIP z Health Connect",
+ "dropHint": "lub kliknij, aby go wybrać. Do 1 GB.",
+ "fileInputLabel": "Plik ZIP eksportu Health Connect",
+ "privacyNote": "Plik jest usuwany z serwera zaraz po zakończeniu importu. Dane z aplikacji, które łączysz już bezpośrednio, np. Withings lub Fitbit, są pomijane, żeby nic nie trafiło podwójnie. Ponowne przesłanie tego samego pliku niczego nie zmienia.",
+ "rateLimited": "Za dużo przesyłań w krótkim czasie. Odczekaj minutę i spróbuj ponownie.",
+ "busy": "Dla Twojego konta trwa już import. Poczekaj, aż się zakończy.",
+ "rowsSoFar": "Zapisane wartości do tej pory: {count}",
+ "doneNothing": "Import się zakończył, ale nie było nic nowego do dodania.",
+ "workouts": "Treningi",
+ "leftOutConnected": "Pominięte wpisy z aplikacji, które łączysz już bezpośrednio: {count}.",
+ "nutrientsModuleOff": "Sumy wody i składników odżywczych nie zostały zaimportowane, bo moduł Składniki odżywcze jest wyłączony.",
+ "cycleModuleOff": "Dane o miesiączce nie zostały zaimportowane, bo śledzenie cyklu jest wyłączone.",
+ "failure": {
+ "unsupportedVersion": "Ten eksport Health Connect jest zbyt stary, aby go odczytać. Zaktualizuj Health Connect na telefonie i wyeksportuj ponownie.",
+ "notHealthConnect": "Ten plik nie jest eksportem Health Connect. Wybierz plik ZIP zapisany przez Health Connect.",
+ "unsafeSchema": "Ten plik nie ma struktury eksportu Health Connect i nie został odczytany.",
+ "tooLarge": "Eksport jest większy, niż ten serwer przyjmuje.",
+ "stagingMissing": "Przesłanego pliku nie było już na serwerze. Prześlij go ponownie.",
+ "interrupted": "Import został przerwany przez ponowne uruchomienie serwera. Prześlij plik ponownie."
+ }
}
},
"subtitle": "Wyeksportuj dane lub zaimportuj z kopii zapasowej."
},
"about": {
"title": "O tej aplikacji",
- "description": "Wersja, licencja, linki.",
"subtitle": "Wersja, licencja i linki do projektu."
},
"sharing": {
"title": "Udostępnianie",
- "description": "Utwórz link tylko do odczytu, aby udostępnić swoją dokumentację zdrowotną lekarzowi.",
"subtitle": "Udostępnij dostęp tylko do odczytu do swoich danych."
},
"insights": {
@@ -5546,7 +5756,7 @@
"error": "Nie udało się zapisać. Spróbuj ponownie.",
"toggleable": {
"title": "Co śledzisz",
- "description": "Wyłączone moduły znikają z nawigacji, kafelków i analiz — Twoje dane pozostają nienaruszone."
+ "description": "Wyłączone moduły znikają z nawigacji, kafelków i analiz, a Twoje dane pozostają nienaruszone."
},
"manageIn": "Zarządzaj w {section}",
"operatorDisabled": "Wyłączone na całym serwerze",
@@ -5554,12 +5764,10 @@
},
"gesundheitsakte": {
"title": "Dokumentacja zdrowotna",
- "description": "Wyeksportuj pełną dokumentację zdrowotną jako PDF, FHIR R4 lub łączony pakiet ZIP.",
"subtitle": "Eksportuj kompletną dokumentację zdrowotną gotową dla lekarza."
},
"coach": {
"title": "Coach",
- "description": "Włącz Coacha, ton i zakres, zapisane wspomnienia.",
"subtitle": "Dostosuj Coacha, jego proaktywne podpowiedzi i pamięć."
},
"security": {
@@ -5568,8 +5776,7 @@
},
"privacy": {
"title": "Dane i prywatność",
- "subtitle": "Twoje dane, retencja, szyfrowanie i bezpieczeństwo konta w jednym miejscu.",
- "description": "Eksport, usuwanie, retencja, stan szyfrowania, sesje i aktywność zabezpieczeń."
+ "subtitle": "Twoje dane, retencja, szyfrowanie i bezpieczeństwo konta w jednym miejscu."
},
"mcp": {
"title": "Łącznik MCP",
@@ -5577,8 +5784,7 @@
},
"anamnesis": {
"title": "Wywiad medyczny",
- "subtitle": "Zarządzaj swoimi alergiami i wywiadem rodzinnym.",
- "description": "Ustrukturyzowane rekordy alergii i wywiadu rodzinnego."
+ "subtitle": "Twoja historia zdrowia, styl życia, dane alarmowe i to, co może czytać AI."
},
"environment": {
"title": "Lokalizacja i pogoda",
@@ -5600,7 +5806,7 @@
"storedDays": "Zapisano dni pogody: {count}.",
"home": {
"title": "Lokalizacja domowa",
- "description": "Przybliżona lokalizacja na poziomie miasta – nigdy dokładny ślad. Pogoda i światło dzienne są pobierane dla tego miejsca w dni bez okresu lokalizacji, od dnia jego ustawienia.",
+ "description": "Przybliżona lokalizacja na poziomie miasta, nigdy dokładny ślad. Pogoda i światło dzienne są pobierane dla tego miejsca w dni bez okresu lokalizacji, od dnia jego ustawienia.",
"current": "Obecnie: {label}",
"none": "Nie ustawiono jeszcze lokalizacji domowej.",
"since": "Pogoda zapisywana od {date}. Wcześniejsze dni pozostają puste, dopóki nie dodasz okresu lokalizacji."
@@ -5612,7 +5818,8 @@
"add": "Dodaj {label}",
"remove": "Usuń okres lokalizacji",
"removeConfirmTitle": "Usunąć tę podróż?",
- "removeConfirmBody": "Pomiary z tego okresu pozostają w historii; znika tylko wpis podróży."
+ "removeConfirmBody": "Pomiary z tego okresu pozostają w historii; znika tylko wpis podróży.",
+ "range": "Od {start} do {end}"
},
"backfill": {
"title": "Uzupełnij minione dni",
@@ -5622,7 +5829,21 @@
"noResults": "Nie znaleziono pasujących miejsc.",
"fetchFailedOne": "Ostatnia aktualizacja pogody w tle nie powiodła się ({when}).",
"fetchFailedFew": "Ostatnie {count} aktualizacje pogody w tle nie powiodły się, najnowsza {when}.",
- "fetchFailedOther": "Ostatnich {count} aktualizacji pogody w tle nie powiodło się, najnowsza {when}."
+ "fetchFailedOther": "Ostatnich {count} aktualizacji pogody w tle nie powiodło się, najnowsza {when}.",
+ "airQuality": {
+ "title": "Jakość powietrza i pyłki",
+ "description": "Codzienna jakość powietrza, pyłki i UV dla twojej lokalizacji, z modelu CAMS przez Open-Meteo.",
+ "toggle": "Uwzględniaj jakość powietrza i pyłki",
+ "operatorDisabled": "Jakość powietrza jest wyłączona na tym serwerze.",
+ "coverage": "Dni z danymi o jakości powietrza: {count}",
+ "pollenEuropeOnly": "Dane o pyłkach obejmują tylko Europę.",
+ "historyProgress": "Uzupełnianie jakości powietrza: {done} z {total} dni",
+ "historyComplete": "Jakość powietrza uzupełniono dla wszystkich wcześniejszych dni z wpisami, które obejmuje źródło ({total} dni). Starsze dni pozostają puste.",
+ "attribution": "Źródła danych i licencje",
+ "saveError": "Nie udało się zapisać ustawienia.",
+ "latest": "Najnowszy dzień ({date})",
+ "modelNote": "Modelowane wartości zewnętrzne dla twojej okolicy, nie twoja osobista ekspozycja."
+ }
},
"score": {
"title": "Wynik zdrowia",
@@ -5655,7 +5876,7 @@
"omitted": "{count} filarów nie ma na liście, bo ich śledzenie jest wyłączone. Włącz moduł z powrotem, a jego filar wróci.",
"omittedLink": "Otwórz moduły",
"refusal": {
- "empty": "Wynik zdrowia potrzebuje co najmniej jednego obszaru zdrowia do oceny. Zostaw wybrany przynajmniej jeden — wynik sam powie, na których obszarach się opiera."
+ "empty": "Wynik zdrowia potrzebuje co najmniej jednego obszaru zdrowia do oceny. Zostaw wybrany przynajmniej jeden, a wynik sam powie, na których obszarach się opiera."
},
"notice": {
"upgrade": {
@@ -5749,7 +5970,6 @@
},
"changePassword": "Zmień hasło",
"changePasswordDescription": "Zastąp obecne hasło nowym.",
- "passwordReset": "Resetowanie hasła",
"currentPassword": "Obecne hasło",
"newPassword": "Nowe hasło",
"confirmNewPassword": "Potwierdź nowe hasło",
@@ -5794,9 +6014,9 @@
"botToken": "Token bota",
"chatId": "ID czatu",
"enableNotifications": "Włącz powiadomienia",
- "telegramStep1": "1. Utwórz bota przez @BotFather w Telegramie i skopiuj token.",
- "telegramStep2": "2. Wyślij /start do swojego bota, aby aktywować czat.",
- "telegramStep3": "3. Znajdź swoje ID czatu przez @userinfobot lub API bota.",
+ "telegramStep1": "Utwórz bota przez @BotFather w Telegramie i skopiuj token.",
+ "telegramStep2": "Wyślij /start do swojego bota, aby aktywować czat.",
+ "telegramStep3": "Znajdź swoje ID czatu przez @userinfobot lub API bota.",
"ntfy": "ntfy",
"ntfyDescription": "Powiadomienia przez ntfy (własny serwer lub ntfy.sh).",
"ntfyEnable": "Włącz ntfy",
@@ -5805,7 +6025,7 @@
"ntfyAuthToken": "Token uwierzytelniania (opcjonalny)",
"ntfyAuthTokenHint": "Wymagany tylko dla prywatnych tematów z kontrolą dostępu.",
"webhook": "Webhook",
- "webhookDescription": "Wysyłaj powiadomienia metodą POST na dowolny URL — Gotify, Discord, Slack, Matrix lub Home Assistant.",
+ "webhookDescription": "Powiadomienia metodą POST na dowolny URL: Discord, Slack, Gotify, Home Assistant, n8n lub przekaźnik przyjmujący JSON.",
"webhookEnable": "Włącz webhook",
"webhookUrl": "Adres URL webhooka",
"webhookUrlHint": "Pełny adres URL dla żądania POST. Adres w sieci prywatnej musi zatwierdzić administrator serwera.",
@@ -5813,6 +6033,7 @@
"webhookHeaderValue": "Wartość nagłówka (opcjonalnie)",
"webhookFormat": "Format wiadomości",
"webhookFormatGeneric": "Ogólny JSON",
+ "webhookFormatGenericHint": "Wklej adres webhooka Discorda lub Slacka bez zmian. Home Assistant i n8n odczytują pola title, message, eventType i priority.",
"webhookFormatGotify": "Gotify",
"webhookFormatGotifyHint": "Użyj adresu URL kończącego się na /message, a token aplikacji wpisz w nagłówku X-Gotify-Key.",
"email": "E-mail",
@@ -5829,7 +6050,6 @@
"webPushSubscribed": "Powiadomienia push włączone!",
"webPushUnsubscribed": "Powiadomienia push wyłączone.",
"webPushSubscribeFailed": "Aktywacja nie powiodła się",
- "kiInsightsDescription": "Opcjonalnie: zapisz swój klucz OpenAI, aby uzyskać codzienne, automatyczne analizy w Insights.",
"codexConnected": "ChatGPT połączony pomyślnie! Insights jest teraz aktywny.",
"codexDisconnected": "Połączenie z ChatGPT usunięte.",
"codexConnectionFailed": "Połączenie z ChatGPT nie powiodło się. Spróbuj ponownie.",
@@ -5839,7 +6059,7 @@
"rawDataWarning": "Szczegółowa historia jest włączona: każda analiza wysyła dodatkowo średnie dzienne z ostatnich 90 dni, średnie tygodniowe z roku wcześniej i średnie miesięczne do 5 lat wstecz.",
"regenerateInsights": "Wygeneruj raporty ponownie",
"regenerateSuccess": "Raporty wygenerowane ponownie",
- "regenerateRateLimit": "Poczekaj — osiągnięto godzinny limit analiz.",
+ "regenerateRateLimit": "Poczekaj, osiągnięto godzinny limit analiz.",
"lastGeneratedAt": "Ostatnio wygenerowano",
"ai": {
"chatgptConnectedBadge": "ChatGPT połączony",
@@ -5849,11 +6069,11 @@
"deviceCodeHeading": "Dokończ łączenie na chatgpt.com",
"deviceCodeStep1": "Otwórz ten link na dowolnym urządzeniu:",
"deviceCodeStep2": "Wprowadź ten jednorazowy kod:",
- "deviceCodeStep3": "Zatwierdź połączenie — ta strona zaktualizuje się automatycznie.",
+ "deviceCodeStep3": "Zatwierdź połączenie. Ta strona zaktualizuje się automatycznie.",
"deviceCodeCopy": "Kopiuj",
"deviceCodeWaiting": "Oczekiwanie na zatwierdzenie…",
"deviceCodeCancel": "Anuluj",
- "oauthNotConfigured": "OAuth ChatGPT nie jest skonfigurowany w tej instancji — użyj zamiast tego własnego klucza API poniżej.",
+ "oauthNotConfigured": "OAuth ChatGPT nie jest skonfigurowany w tej instancji. Użyj zamiast tego własnego klucza API poniżej.",
"modelLabel": "Model",
"modelOptionDefault": "— Domyślny —",
"modelOptionCustom": "Niestandardowy…",
@@ -5861,7 +6081,7 @@
"anthropicKeyLabel": "Klucz API Anthropic",
"baseUrlLabel": "Base URL",
"localKeyLabel": "Klucz API (opcjonalny)",
- "localDescription": "Działa tu każdy punkt końcowy zgodny z OpenAI — model lokalny (Ollama, LM Studio) albo brama taka jak LiteLLM, OpenRouter czy vLLM. Adres bazowy kończy się na /v1; klucz API jest opcjonalny.",
+ "localDescription": "Działa tu każdy punkt końcowy zgodny z OpenAI: model lokalny (Ollama, LM Studio) albo brama taka jak LiteLLM, OpenRouter czy vLLM. Adres bazowy kończy się na /v1; klucz API jest opcjonalny.",
"savedPreview": "(zapisano {preview})",
"savedShort": "(zapisano)",
"saveCta": "Zapisz",
@@ -5877,7 +6097,7 @@
"admin-openai": "OpenAI administratora"
},
"title": "Łańcuch zapasowy",
- "description": "Jeśli główny dostawca zawiedzie, HealthLog przechodzi przez łańcuch po kolei. Przeciągaj wiersze, aby zmienić kolejność; użyj przełącznika, aby wyłączyć dostawcę bez usuwania go.",
+ "description": "Gdy aktywny dostawca zawiedzie, HealthLog próbuje kolejnego włączonego w tej kolejności.",
"moveUp": "Przesuń w górę",
"moveDown": "Przesuń w dół",
"removeFromChain": "Usuń z łańcucha",
@@ -5890,11 +6110,8 @@
"resetConfirmTitle": "Przywrócić domyślny łańcuch?",
"resetConfirmBody": "Łańcuch wraca do Codex → OpenAI → Anthropic → Lokalny → OpenAI administratora. Zapisane dane logowania pozostają nienaruszone."
},
- "providerCardTitle": "Dostawca AI",
"activeProviderHeading": "Aktywny dostawca",
- "activeProviderBody": "Wybierz dostawcę, którego chcesz użyć jako pierwszego. Formularz poniżej konfiguruje tylko wybranego dostawcę; łańcuch zapasowy na dole decyduje, co się stanie w razie awarii.",
"activeProviderLabel": "Główny dostawca",
- "providerConfigTitle": "Konfiguracja dostawcy",
"providerSelect": {
"codex": "Konto ChatGPT (Codex)",
"openai": "OpenAI (Twój klucz API)",
@@ -5904,7 +6121,7 @@
"admin-openai": "OpenAI udostępniony przez administratora"
},
"compat": {
- "description": "Pełnoprawny dostawca dla każdego punktu końcowego mówiącego API /v1/chat/completions OpenAI: LiteLLM, OpenRouter, vLLM lub inna brama. Adres bazowy kończy się na /v1, klucz API jest opcjonalny, a model to ten, który kieruje twoja brama. Twój klucz OpenAI nigdy nie trafia do tego punktu końcowego — ten dostawca ma własny.",
+ "description": "Pełnoprawny dostawca dla każdego punktu końcowego mówiącego API /v1/chat/completions OpenAI: LiteLLM, OpenRouter, vLLM lub inna brama. Adres bazowy kończy się na /v1, klucz API jest opcjonalny, a model to ten, który kieruje twoja brama. Twój klucz OpenAI nigdy nie trafia do tego punktu końcowego, bo ten dostawca ma własny.",
"keyLabel": "Klucz API (opcjonalny)",
"modelHint": "Nazwa modelu, której oczekuje twoja brama. Pozostawione puste, użyty zostanie model zapisany dla pozostałych dostawców; gdy nie ustawiono żadnego, ten dostawca jest pomijany."
},
@@ -5940,25 +6157,24 @@
"notConfigured": "Operator nie skonfigurował współdzielonego klucza OpenAI w tej instancji."
},
"testProvider": "Przetestuj aktywnego dostawcę",
- "testSuccess": "OK — {provider} ({model})",
+ "testSuccess": "OK: {provider} ({model})",
"testFailedShort": "Test nie powiódł się: {message}",
- "testUnexpectedResponse": "Połączenie z dostawcą AI nie powiodło się — nieoczekiwana odpowiedź serwera.",
- "testReasonCredentials": "Dostawca odrzucił dane logowania — uwierzytelnij się ponownie w ustawieniach AI.",
- "testReasonRateLimited": "Dostawca ograniczył żądanie — spróbuj ponownie za chwilę.",
+ "testUnexpectedResponse": "Połączenie z dostawcą AI nie powiodło się: nieoczekiwana odpowiedź serwera.",
+ "testReasonCredentials": "Dostawca odrzucił dane logowania. Uwierzytelnij się ponownie w ustawieniach AI.",
+ "testReasonRateLimited": "Dostawca ograniczył żądanie. Spróbuj ponownie za chwilę.",
"testReasonServerError": "Dostawca AI zwrócił błąd serwera.",
- "testReasonBadRequest": "Dostawca odrzucił żądanie (HTTP {status}) — punkt końcowy odpowiedział, więc problem dotyczy formatu żądania lub nazwy modelu, a nie łączności.",
+ "testReasonBadRequest": "Dostawca odrzucił żądanie (HTTP {status}). Punkt końcowy odpowiedział, więc problem dotyczy formatu żądania lub nazwy modelu, a nie łączności.",
"testReasonUnreachable": "Nie udało się połączyć z dostawcą AI.",
"coachMemory": {
"title": "Co Coach pamięta",
"description": "Trwałe informacje, które przekazałeś Coachowi.",
- "empty": "Coach nie zapisał jeszcze żadnych informacji. Podczas rozmowy zapamiętuje trwałe szczegóły, którymi się dzielisz — Twoje cele, dolegliwości, preferencje — i możesz je tutaj w każdej chwili usunąć.",
+ "empty": "Coach nie zapisał jeszcze żadnych informacji. Podczas rozmowy zapamiętuje trwałe szczegóły, którymi się dzielisz (Twoje cele, dolegliwości, preferencje), i możesz je tutaj w każdej chwili usunąć.",
"learnedPrefix": "zapamiętano {when}",
"categoryPreference": "Preferencje",
"categoryCondition": "Dolegliwości",
"categoryGoal": "Cele",
"categoryConstraint": "Ograniczenia",
"categoryContext": "Kontekst",
- "forget": "Zapomnij",
"forgetAria": "Zapomnij tę informację",
"forgetTitle": "Zapomnieć tę informację?",
"forgetBody": "Coach przestanie z niej korzystać od następnej wiadomości. Zapomnianej informacji nie da się odzyskać.",
@@ -5968,13 +6184,14 @@
"forgetAllConfirmBody": "Spowoduje to usunięcie wszystkich zapisanych informacji. Coach zacznie od nowa przy następnej rozmowie. Tej operacji nie można cofnąć.",
"forgetAllConfirmAction": "Zapomnij wszystko",
"cancel": "Anuluj",
- "summaryNote": "Coach może też przechowywać krótkie, bieżące podsumowanie każdej rozmowy. Nie ma dla niego osobnej kontrolki — usunięcie rozmowy usuwa jej podsumowanie.",
+ "summaryNote": "Coach może też przechowywać krótkie, bieżące podsumowanie każdej rozmowy. Nie ma dla niego osobnej kontrolki: usunięcie rozmowy usuwa jej podsumowanie.",
"loadError": "Nie udało się wczytać tego, co Coach pamięta.",
"forgotToast": "Informacja zapomniana.",
"forgotError": "Nie udało się zapomnieć tej informacji. Spróbuj ponownie.",
"clearedToast": "Zapomniano {count} zapisanych informacji.",
"clearError": "Nie udało się usunąć zapisanych informacji. Spróbuj ponownie.",
- "detail": "Wykorzystuje je, aby jego odpowiedzi pozostawały trafne. Usuń wszystko, o czym wolisz, by zapomniał."
+ "detail": "Wykorzystuje je, aby jego odpowiedzi pozostawały trafne. Usuń wszystko, o czym wolisz, by zapomniał.",
+ "categoryMedication": "Leki"
},
"coachReminders": {
"title": "Twoje przypomnienia",
@@ -5988,7 +6205,7 @@
"markDone": "Gotowe",
"dismiss": "Odrzuć",
"deleteAria": "Usuń to przypomnienie",
- "note": "Przypomnienia są przechowywane w postaci zaszyfrowanej. Usuń je w dowolnej chwili — znika z tego, co widzi Coach.",
+ "note": "Przypomnienia są przechowywane w postaci zaszyfrowanej. Usuń je w dowolnej chwili, a znikną z tego, co widzi Coach.",
"status": {
"proposed": "Sugerowane",
"active": "Zapisane",
@@ -6034,20 +6251,20 @@
"autoRead": {
"title": "Automatyczne czytanie dokumentów przez AI",
"subLabel": "Dokumenty są czytane i indeksowane przez skonfigurowanego dostawcę AI bez potwierdzania każdego dokumentu.",
- "honesty": "Po włączeniu każdy przesłany dokument jest wysyłany do skonfigurowanego dostawcy AI, aby go odczytać, opisać i zaindeksować do wyszukiwania — bez potwierdzania każdego dokumentu. Włączenie działa też wstecz: dokumenty już przesłane, które nie mają jeszcze streszczenia, również zostaną wysłane — do 200 przy każdym włączeniu. Dzieje się to na serwerze, a treść dokumentu opuszcza to urządzenie i trafia do tego dostawcy. Jeśli skonfigurowany dostawca to połączenie subskrypcyjne (zalogowane konto AI zamiast klucza API), obowiązują jego własne ustawienia danych — dostawcy subskrypcyjni mogą wykorzystywać treść do ulepszania swoich modeli i nie obejmuje tego żadna umowa powierzenia przetwarzania danych. Dokumenty odczytywane przez samodzielnie hostowany model lokalny nigdy nie opuszczają Twojego serwera. Domyślnie wyłączone; włącz je tylko wtedy, gdy akceptujesz ten kompromis dla swoich dokumentów.",
- "confirm": "Rozumiem — włącz",
+ "honesty": "Po włączeniu każdy przesłany dokument jest wysyłany do skonfigurowanego dostawcy AI, aby go odczytać, opisać i zaindeksować do wyszukiwania, bez potwierdzania każdego dokumentu. Włączenie działa też wstecz: dokumenty już przesłane, które nie mają jeszcze streszczenia, również zostaną wysłane (do 200 przy każdym włączeniu). Dzieje się to na serwerze, a treść dokumentu opuszcza to urządzenie i trafia do tego dostawcy. Jeśli skonfigurowany dostawca to połączenie subskrypcyjne (zalogowane konto AI zamiast klucza API), obowiązują jego własne ustawienia danych: dostawcy subskrypcyjni mogą wykorzystywać treść do ulepszania swoich modeli i nie obejmuje tego żadna umowa powierzenia przetwarzania danych. Dokumenty odczytywane przez samodzielnie hostowany model lokalny nigdy nie opuszczają Twojego serwera. Domyślnie wyłączone; włącz je tylko wtedy, gdy akceptujesz ten kompromis dla swoich dokumentów.",
+ "confirm": "Rozumiem, włącz",
"cancel": "Anuluj"
},
"centralCodex": {
"title": "Użyj wspólnego dostępu AI serwera",
- "subLabel": "Kieruj zapytania AI przez połączenie skonfigurowane przez operatora dla tego serwera, obok Twoich własnych dostawców jako rozwiązanie awaryjne.",
- "honesty": "Wspólny dostęp to jedno zalogowane konto AI, które operator podłączył dla wszystkich na tym serwerze — nie Twój własny klucz. Zapytania, które przez nie wysyłasz, są domyślnie wykorzystywane do trenowania modeli dostawcy i nie obejmuje ich żadna umowa powierzenia przetwarzania danych, godzinowe i tygodniowe limity konta dzielisz z wszystkimi innymi, którzy się zgodzili, a Twoi właśni dostawcy są próbowani jako pierwsi. Włącz to tylko, jeśli akceptujesz ten kompromis.",
- "confirm": "Rozumiem — użyj połączenia",
+ "subLabel": "Zapytania AI idą przez wspólne połączenie serwera, a Twoi dostawcy służą jako zapas.",
+ "honesty": "Wspólny dostęp to jedno zalogowane konto AI, które operator podłączył dla wszystkich na tym serwerze, a nie Twój własny klucz. Zapytania, które przez nie wysyłasz, są domyślnie wykorzystywane do trenowania modeli dostawcy i nie obejmuje ich żadna umowa powierzenia przetwarzania danych, godzinowe i tygodniowe limity konta dzielisz z wszystkimi innymi, którzy się zgodzili, a Twoi właśni dostawcy są próbowani jako pierwsi. Włącz to tylko, jeśli akceptujesz ten kompromis.",
+ "confirm": "Rozumiem, użyj połączenia",
"cancel": "Anuluj"
},
"consent": {
"title": "Zgoda na AI",
- "activeSince": "Udzielona {date}. Twoje dane zdrowotne mogą być wysyłane do skonfigurowanego dostawcy na potrzeby funkcji AI.",
+ "activeSince": "Udzielona {date}: Twoje dane zdrowotne mogą trafiać do skonfigurowanego dostawcy na potrzeby funkcji AI.",
"withdrawn": "Wycofana. Jeśli żadna inna zgoda tego nie obejmuje, Coach i Insights nie korzystają już z dostępu AI zapewnianego przez ten serwer, a żaden model online nie czyta Twoich dokumentów. Na własnym kluczu lub lokalnym modelu Coach i Insights działają dalej.",
"withdraw": "Wycofaj zgodę",
"confirmBody": "Jeśli żadna inna Twoja zgoda ich nie obejmuje, od razu przestają działać: Coach i Insights tam, gdzie korzystają z dostępu AI zapewnianego przez ten serwer, oraz czytanie dokumentów przez model online. Na własnym kluczu lub lokalnym modelu Coach i Insights działają dalej. Jeśli żadna inna zgoda nie obejmuje już Insights, notatki napisane dla Ciebie przez model zostają usunięte: notatki o stanie, codzienne podsumowanie, podsumowania okresu napisane przez model, linie po nowym pomiarze i notatki o treningach. Twoje rozmowy z Coachem, to, co zapamiętał Coach, Twoje plany i podsumowania dokumentów zostają, a Ty możesz je sam przeczytać lub usunąć.",
@@ -6058,7 +6275,7 @@
"error": "Nie udało się zapisać wycofania. Spróbuj ponownie.",
"documentReading": {
"title": "Zgoda na odczyt dokumentów",
- "activeSince": "Udzielona {date}. Dokumenty, zeskanowane wyniki badań i opisy leków mogą być wysyłane do Twojej usługi AI do odczytu.",
+ "activeSince": "Udzielona {date}: dokumenty, skany badań i opisy leków mogą trafiać do Twojej usługi AI do odczytu.",
"confirmBody": "Dokumenty, zeskanowane wyniki badań i opisy leków nie są już wysyłane do odczytu. Nic, co już masz, nie zostaje usunięte."
}
},
@@ -6080,7 +6297,21 @@
"title": "Plany z Coachem",
"description": "Plany uzgodnione z Coachem, do przeczytania lub usunięcia.",
"open": "Otwórz plany"
- }
+ },
+ "reasoning": {
+ "label": "Rozumowanie",
+ "hint": "Modele rozumujące, takie jak Gemma czy Qwen, myślą przed odpowiedzią. Po wyłączeniu cały budżet odpowiedzi idzie na samą odpowiedź.",
+ "options": {
+ "default": "Domyślnie",
+ "none": "Wyłączone",
+ "low": "Niskie",
+ "medium": "Średnie",
+ "high": "Wysokie"
+ },
+ "coachNote": "W Coachu obowiązuje głębokość myślenia z ustawień Coacha."
+ },
+ "providerCardDescription": "Dostawca, którego HealthLog pyta najpierw, wraz z kluczem i modelem.",
+ "runtimeTitle": "Sprawdź i odśwież"
},
"dashboard": {
"units": {
@@ -6104,8 +6335,8 @@
"withingsClientId": "ID klienta",
"withingsClientSecret": "Sekret klienta",
"withingsCredentialsSaved": "Dane logowania zapisane",
- "withingsCredentialsSavedPlaceholder": "Zapisano — wprowadź nowy, aby zastąpić",
- "withingsCredentialsSavedPlaceholderSecret": "Zapisano — wprowadź nowy, aby zastąpić",
+ "withingsCredentialsSavedPlaceholder": "Zapisano. Wprowadź nowy, aby zastąpić",
+ "withingsCredentialsSavedPlaceholderSecret": "Zapisano. Wprowadź nowy, aby zastąpić",
"withingsSaveCredentials": "Zapisz",
"configured": "Skonfigurowano",
"withingsSync": "Synchronizuj teraz",
@@ -6120,7 +6351,7 @@
},
"withingsFullSyncResult": "{count} pomiarów w pełni zsynchronizowano",
"withingsSyncFailed": "Synchronizacja nie powiodła się",
- "withingsOauthConnected": "Withings połączony — pierwsza synchronizacja rozpocznie się w tle.",
+ "withingsOauthConnected": "Withings połączony. Pierwsza synchronizacja rozpocznie się w tle.",
"withingsOauthFailed": "Połączenie z Withings nie powiodło się",
"withingsOauthError": {
"csrf1": "Kontrola bezpieczeństwa nie powiodła się, ponieważ brakowało pliku cookie przeglądarki. Rozpocznij połączenie ponownie w tej samej przeglądarce.",
@@ -6145,8 +6376,8 @@
"whoopClientId": "Client ID",
"whoopClientSecret": "Client Secret",
"whoopCredentialsSaved": "Dane zapisane",
- "whoopCredentialsSavedPlaceholder": "Zapisano — wpisz nowe, aby zastąpić",
- "whoopCredentialsSavedPlaceholderSecret": "Zapisano — wpisz nowe, aby zastąpić",
+ "whoopCredentialsSavedPlaceholder": "Zapisano. Wpisz nowe, aby zastąpić",
+ "whoopCredentialsSavedPlaceholderSecret": "Zapisano. Wpisz nowe, aby zastąpić",
"whoopSaveCredentials": "Zapisz",
"whoopSync": "Synchronizuj teraz",
"whoopFullSync": "Synchronizuj wszystkie dane",
@@ -6161,7 +6392,7 @@
"whoopConnect": "Połącz z WHOOP",
"whoopNoCredentials": "Wprowadź powyżej swoje dane API, aby połączyć WHOOP.",
"whoopBackfillInProgress": "Importowanie historii WHOOP w tle…",
- "whoopOauthConnected": "WHOOP połączony — pierwsza synchronizacja startuje w tle.",
+ "whoopOauthConnected": "WHOOP połączony. Pierwsza synchronizacja startuje w tle.",
"whoopOauthFailed": "Połączenie z WHOOP nie powiodło się",
"whoopOauthError": {
"csrf1": "Kontrola bezpieczeństwa nie powiodła się, ponieważ brakowało pliku cookie przeglądarki. Rozpocznij połączenie ponownie w tej samej przeglądarce.",
@@ -6175,14 +6406,14 @@
},
"fitbit": "Fitbit",
"fitbitTag": "Fitbit i Pixel",
- "fitbitDescription": "Połącz Fitbit, aby synchronizować dane z Fitbit i Pixel Watch — wagę, tętno i więcej.",
+ "fitbitDescription": "Połącz Fitbit, aby synchronizować dane z Fitbit i Pixel Watch: wagę, tętno i więcej.",
"fitbitExperimentalBadge": "Eksperymentalne",
"fitbitCredentials": "Dane uwierzytelniające API",
"fitbitClientId": "Client ID",
"fitbitClientSecret": "Client Secret",
"fitbitCredentialsSaved": "Dane uwierzytelniające zapisane",
- "fitbitCredentialsSavedPlaceholder": "Zapisano — wpisz nowe, aby zastąpić",
- "fitbitCredentialsSavedPlaceholderSecret": "Zapisano — wpisz nowe, aby zastąpić",
+ "fitbitCredentialsSavedPlaceholder": "Zapisano. Wpisz nowe, aby zastąpić",
+ "fitbitCredentialsSavedPlaceholderSecret": "Zapisano. Wpisz nowe, aby zastąpić",
"fitbitSaveCredentials": "Zapisz",
"fitbitSync": "Synchronizuj teraz",
"fitbitFullSync": "Synchronizuj wszystkie dane",
@@ -6197,18 +6428,18 @@
"fitbitConnect": "Połącz z Fitbit",
"fitbitNoCredentials": "Wprowadź powyżej swoje dane uwierzytelniające API, aby połączyć Fitbit.",
"fitbitBackfillInProgress": "Importowanie historii Fitbit w tle…",
- "fitbitOauthConnected": "Fitbit połączony — pierwsza synchronizacja startuje w tle.",
+ "fitbitOauthConnected": "Fitbit połączony. Pierwsza synchronizacja startuje w tle.",
"fitbitOauthFailed": "Połączenie z Fitbit nie powiodło się",
"googleHealth": "Google Health",
"googleHealthTag": "Fitbit i Pixel",
"googleHealthBetaBadge": "Beta",
- "googleHealthDescription": "Połącz Fitbit, Pixel Watch i Fitbit Air przez konto Google — waga, tętno, sen i aktywność. Zastępuje to klasyczne połączenie Fitbit, które Google wyłącza we wrześniu 2026.",
+ "googleHealthDescription": "Połącz Fitbit, Pixel Watch i Fitbit Air przez konto Google: waga, tętno, sen i aktywność. Zastępuje to klasyczne połączenie Fitbit, które Google wyłącza we wrześniu 2026.",
"googleHealthCredentials": "Dane API",
"googleHealthClientId": "Identyfikator klienta",
"googleHealthClientSecret": "Sekret klienta",
"googleHealthCredentialsSaved": "Dane logowania zapisane",
- "googleHealthCredentialsSavedPlaceholder": "Zapisane — wpisz nowe, aby zastąpić",
- "googleHealthCredentialsSavedPlaceholderSecret": "Zapisane — wpisz nowe, aby zastąpić",
+ "googleHealthCredentialsSavedPlaceholder": "Zapisane. Wpisz nowe, aby zastąpić",
+ "googleHealthCredentialsSavedPlaceholderSecret": "Zapisane. Wpisz nowe, aby zastąpić",
"googleHealthSaveCredentials": "Zapisz",
"googleHealthSync": "Synchronizuj teraz",
"googleHealthSyncCheckStatus": "Odśwież stan",
@@ -6242,9 +6473,9 @@
"googleHealthConnect": "Połącz z Google",
"googleHealthNoCredentials": "Wprowadź powyżej swoje dane API, aby połączyć Google Health.",
"googleHealthBackfillInProgress": "Importowanie historii Google Health w tle…",
- "googleHealthReauthBanner": "Google unieważnił połączenie — dzieje się to okresowo, dopóki Twoja aplikacja OAuth pozostaje w trybie testowym. Połącz ponownie, aby wznowić synchronizację.",
+ "googleHealthReauthBanner": "Google unieważnił połączenie. Dzieje się to okresowo, dopóki Twoja aplikacja OAuth pozostaje w trybie testowym. Połącz ponownie, aby wznowić synchronizację.",
"googleHealthReconnect": "Połącz ponownie",
- "googleHealthOauthConnected": "Google Health połączony — pierwsza synchronizacja startuje w tle.",
+ "googleHealthOauthConnected": "Google Health połączony. Pierwsza synchronizacja startuje w tle.",
"googleHealthOauthFailed": "Połączenie z Google Health nie powiodło się",
"googleHealthOauthError": {
"csrf1": "Kontrola bezpieczeństwa nie powiodła się, ponieważ brakowało pliku cookie przeglądarki. Rozpocznij połączenie ponownie w tej samej przeglądarce.",
@@ -6279,10 +6510,10 @@
},
"integrationPill": {
"connected": "Połączono",
- "errorReconnect": "Błąd — połącz ponownie",
+ "errorReconnect": "Błąd: połącz ponownie",
"warningServerError": "Synchronizacja nie działa",
"failureCount": "{count}/{threshold}",
- "parkedReconnect": "Wstrzymane — połącz ponownie",
+ "parkedReconnect": "Wstrzymane: połącz ponownie",
"notConnected": "Nie połączono",
"justNow": "przed chwilą",
"minutesAgo": "{count} min temu",
@@ -6297,7 +6528,7 @@
},
"integrationFreshness": {
"title": "Odebrane dane",
- "summary": "{count} typów danych · najnowsze {relative}",
+ "summary": "{count} typów danych, najnowsze {relative}",
"quiet": "{count} bez danych",
"workouts": "Treningi"
},
@@ -6311,31 +6542,33 @@
"fitbitViewData": "Zobacz swój sen",
"googleHealthViewData": "Zobacz swoje kroki",
"apiTokens": "Tokeny API",
- "apiTokensDescription": "Przeglądaj i unieważniaj tokeny API do zewnętrznego rejestrowania przyjęć leków.",
+ "apiTokensDescription": "Przeglądaj i unieważniaj tokeny do pomiarów, treningów, dokumentów i przyjęć leków.",
"measurementsToken": {
"title": "Tokeny pomiarów",
"description": "Utwórz token, aby waga, zegarek lub automatyka domowa mogły dodawać pomiary.",
"detail": "Token pokazujemy tylko raz.",
- "scopeNote": "Może wyłącznie dodawać nowe pomiary do Twojej dokumentacji — nie odczyta danych, niczego nie zmieni ani nie usunie i nie utworzy kolejnego tokenu. Pomiary są oznaczane jako pochodzące z urządzenia zewnętrznego, dzięki czemu później je rozpoznasz, i nadal możesz je poprawić.",
- "createFailed": "Nie udało się utworzyć tokenu. Spróbuj ponownie."
+ "scopeNote": "Może wyłącznie dodawać nowe pomiary do Twojej dokumentacji: nie odczyta danych, niczego nie zmieni ani nie usunie i nie utworzy kolejnego tokenu. Pomiary są oznaczane jako pochodzące z urządzenia zewnętrznego, dzięki czemu później je rozpoznasz, i nadal możesz je poprawić.",
+ "createFailed": "Nie udało się utworzyć tokenu. Spróbuj ponownie.",
+ "namePlaceholder": "Nazwa, np. Home Assistant"
},
"workoutsToken": {
"title": "Tokeny treningów",
"description": "Utwórz token, aby mostek zegarka lub skrypt synchronizacji mogły dodawać Twoje treningi.",
"detail": "Token pokazujemy tylko raz.",
"scopeNote": "Może wyłącznie dodawać nowe treningi do Twojej dokumentacji. Nie odczyta danych, niczego nie zmieni ani nie usunie i nie utworzy kolejnego tokenu. Dodane przez niego treningi są oznaczane jako pochodzące z urządzenia zewnętrznego, a jeśli telefon zapisał tę samą sesję, widzisz zapis z telefonu.",
- "createFailed": "Nie udało się utworzyć tokenu. Spróbuj ponownie."
+ "createFailed": "Nie udało się utworzyć tokenu. Spróbuj ponownie.",
+ "namePlaceholder": "Nazwa, np. Garmin"
},
"documentsToken": {
"title": "Tokeny dokumentów",
"description": "Utwórz token, aby Paperless-ngx, Papra lub skrypt importu mogły dodawać dokumenty do Twojego archiwum.",
"detail": "Token pokazujemy tylko raz.",
"scopeNote": "Może wyłącznie dodawać nowe dokumenty do Twojego archiwum. Niczego nie wyświetli, nie otworzy, nie pobierze, nie zmieni ani nie usunie i nie utworzy kolejnego tokenu. Dowiaduje się tylko, czy plik został zapisany, a nie to, jak został opisany.",
- "createFailed": "Nie udało się utworzyć tokenu. Spróbuj ponownie."
+ "createFailed": "Nie udało się utworzyć tokenu. Spróbuj ponownie.",
+ "namePlaceholder": "Nazwa, np. Paperless-ngx"
},
- "tokenMintMovedDescription": "Tokeny wydajemy tam, gdzie działają: powyżej dla pomiarów i dokumentów albo w punkcie końcowym API danego leku dla tego jednego leku.",
- "tokenNamePlaceholder": "Nazwa tokena (np. Skrót iPhone)",
- "tokenCreated": "Token utworzony — skopiuj go teraz! Nie zostanie pokazany ponownie.",
+ "tokenMintMovedDescription": "Tokeny wydajemy tam, gdzie działają: powyżej dla pomiarów, treningów i dokumentów albo w punkcie końcowym API danego leku dla tego jednego leku.",
+ "tokenCreated": "Token utworzony. Skopiuj go teraz! Nie zostanie pokazany ponownie.",
"tokenCopy": "Kopiuj token",
"tokenCopied": "Token skopiowany do schowka",
"tokenCopyFailed": "Nie udało się skopiować tokenu",
@@ -6374,7 +6607,7 @@
"deleteAccountCta": "Usuń konto",
"deleteAccountConfirmTitle": "Trwale usunąć konto?",
"deleteAccountConfirmDescription": "Twoje konto, wszystkie dane zdrowotne, klucze dostępu, sesje i wpisy audytu zostaną trwale usunięte. Zostaniesz wylogowany natychmiast po tym.",
- "deleteAccountSuccess": "Konto usunięte — trwa wylogowywanie.",
+ "deleteAccountSuccess": "Konto usunięte, trwa wylogowywanie.",
"deleteAccountFailed": "Nie udało się usunąć konta",
"deleteAccountFinal": "Usuń trwale",
"saved": "Zapisano",
@@ -6397,7 +6630,7 @@
"testing": "Testowanie…",
"ok": "Połączono (opóźnienie {latency} ms)",
"errors": {
- "credentials_rejected": "Dane logowania odrzucone — sprawdź token",
+ "credentials_rejected": "Dane logowania odrzucone, sprawdź token",
"rate_limited": "Ograniczone przez usługę nadrzędną",
"timeout": "Przekroczono limit czasu żądania",
"upstream_error": "Usługa nadrzędna zwróciła błąd",
@@ -6408,7 +6641,7 @@
"private_origin_not_approved": "Cel w sieci prywatnej. Operator musi dodać to źródło do NOTIFICATION_PRIVATE_ORIGINS",
"private_origin_not_grantable": "Ten adres to link-local, adres metadanych lub nieokreślony; żadna zgoda operatora nie może go otworzyć. Użyj adresu, na którym nasłuchuje przekaźnik",
"url_invalid": "Adres URL jest nieprawidłowy",
- "redirected": "Punkt końcowy przekierowuje — sprawdź adres URL",
+ "redirected": "Punkt końcowy przekierowuje, sprawdź adres URL",
"endpoint_not_found": "Nie znaleziono punktu końcowego pod adresem URL",
"credentials_unreadable": "Nie można odszyfrować danych logowania",
"upstream_invalid_json": "Usługa nadrzędna wysłała nieprawidłowy JSON",
@@ -6419,7 +6652,7 @@
},
"notificationStatus": {
"title": "Niezawodność kanałów",
- "description": "Stan każdego kanału — „Automatycznie wyłączone” oznacza zatrzymanie po trwałych błędach.",
+ "description": "Stan każdego kanału; „Automatycznie wyłączone” oznacza zatrzymanie po trwałych błędach.",
"emptyDescription": "Dodaj kanał w Ustawienia › Integracje › Kanały dostarczania.",
"stateActive": "Aktywne",
"stateAutoDisabled": "Automatycznie wyłączone",
@@ -6445,7 +6678,7 @@
},
"healthRecord": {
"title": "Eksportuj dokumentację zdrowotną",
- "description": "Pełna dokumentacja dla lekarza — jako PDF, jako FHIR lub oba.",
+ "description": "Pełna dokumentacja dla lekarza: jako PDF, jako FHIR lub oba.",
"format": "Format",
"format_pdf": "PDF",
"format_fhir": "Ustrukturyzowany (FHIR)",
@@ -6477,7 +6710,7 @@
"createTitle": "Nowy link udostępniania",
"createDescription": "Link jest tylko do odczytu i ograniczony czasowo.",
"label": "Etykieta",
- "labelPlaceholder": "np. dr Kowalski — kontrola roczna",
+ "labelPlaceholder": "np. dr Kowalski, kontrola roczna",
"range": "Dni historii",
"rangeHint": "Jak daleko wstecz sięga udostępnione okno.",
"expiry": "Wygasa za (dni)",
@@ -6497,7 +6730,7 @@
"statusRevoked": "Unieważniony",
"statusExpired": "Wygasły",
"inactiveTitle": "Unieważnione i wygasłe ({count})",
- "shownOnce": "Wyświetlane tylko raz. Zapisz teraz kod QR i hasło — nie można ich później odzyskać.",
+ "shownOnce": "Wyświetlane tylko raz. Zapisz teraz kod QR i hasło, bo nie można ich później odzyskać.",
"qrAlt": "Kod QR zawierający link do udostępniania i hasło",
"linkLabel": "Link",
"passphraseLabel": "Hasło",
@@ -6508,12 +6741,12 @@
"attachTitle": "Dołącz dokumenty",
"attachButton": "Wybierz dokumenty",
"attachCount": "Wybrano {count} z {max}",
- "attachFrozen": "Zestaw dokumentów jest ustalany przy tworzeniu linku — później nie można dodawać ani usuwać dokumentów.",
+ "attachFrozen": "Zestaw dokumentów jest ustalany przy tworzeniu linku. Później nie można dodawać ani usuwać dokumentów.",
"attachRemove": "Usuń {title}",
"exifNote": "Metadane aparatu (EXIF/GPS) są usuwane z obrazów JPEG, PNG i WebP podczas udostępniania. Pliki PDF, TIFF, HEIC i Office są przekazywane bez zmian.",
"documentCount": "Dołączone dokumenty: {count}",
"qrScanTitle": "Zeskanuj, aby otworzyć na innym urządzeniu",
- "qrScanHint": "Skieruj aparat telefonu na kod, aby otworzyć udostępnioną dokumentację — hasło jest już dołączone.",
+ "qrScanHint": "Skieruj aparat telefonu na kod, aby otworzyć udostępnioną dokumentację. Hasło jest już dołączone.",
"pickerTitle": "Dołącz dokumenty",
"pickerDescription": "Przeszukaj swój sejf i wybierz dokumenty do udostępnienia. Maksymalnie {max}.",
"pickerSearch": "Szukaj dokumentów",
@@ -6526,7 +6759,8 @@
"statusNeedsReselect": "Wybierz zawartość ponownie",
"reselectReason": "Ten link udostępniał zakres, którego nikt nie wybrał, więc został zamknięty. Utwórz nowy i wybierz jego zawartość.",
"reselectAction": "Utwórz zamiennik",
- "createDetail": "Token jest pokazywany tylko raz przy tworzeniu — zapisz go wtedy; nie można go odzyskać."
+ "createDetail": "Token jest pokazywany tylko raz przy tworzeniu. Zapisz go wtedy; nie można go odzyskać.",
+ "activeDescription": "Udostępnione linki, termin ich wygaśnięcia i liczba otwarć."
},
"genderOther": "Inna",
"nightscout": "Nightscout",
@@ -6534,10 +6768,10 @@
"nightscoutDescription": "Połącz własną stronę Nightscout, aby synchronizować ciągłe odczyty glukozy w panelu glukozy.",
"nightscoutViewData": "Zobacz swoją glukozę",
"nightscoutUrl": "Adres URL Nightscout",
- "nightscoutUrlSavedPlaceholder": "Zapisano — wpisz nowy adres URL, aby go zastąpić",
+ "nightscoutUrlSavedPlaceholder": "Zapisano. Wpisz nowy adres URL, aby go zastąpić",
"nightscoutToken": "Token API",
"nightscoutTokenOptional": "Opcjonalny dla witryny publicznej",
- "nightscoutTokenSavedPlaceholder": "Zapisano — wpisz nowy token, aby go zastąpić",
+ "nightscoutTokenSavedPlaceholder": "Zapisano. Wpisz nowy token, aby go zastąpić",
"nightscoutTokenHelp": "Wklej token dostępu z narzędzi administracyjnych Nightscout. Pozostaw puste, jeśli witryna jest publicznie czytelna.",
"nightscoutPrivateOperatorTitle": "Dostęp do prywatnego serwera Nightscout wymaga zgody administratora serwera.",
"nightscoutPrivateOperatorDescription": "Poproś administratora serwera o zatwierdzenie dokładnie tego pochodzenia Nightscout.",
@@ -6558,7 +6792,7 @@
"polarClientId": "Client ID",
"polarClientSecret": "Client Secret",
"polarCredentialsSaved": "Dane zapisane",
- "polarCredentialsSavedPlaceholder": "Zapisano — wpisz nowe, aby zastąpić",
+ "polarCredentialsSavedPlaceholder": "Zapisano. Wpisz nowe, aby zastąpić",
"polarSaveCredentials": "Zapisz",
"polarUnavailable": "Polar nie jest skonfigurowany na tym serwerze. Dodaj powyżej własne dane OAuth Polar lub poproś administratora o skonfigurowanie współdzielonej aplikacji.",
"polarConnect": "Połącz Polar",
@@ -6568,7 +6802,7 @@
"polarDisconnect": "Rozłącz",
"polarDisconnectTitle": "Rozłączyć Polar?",
"polarDisconnectDescription": "Przyszłe synchronizacje zostaną zatrzymane. Już zaimportowane odczyty pozostaną w historii.",
- "polarOauthConnected": "Polar połączony — pierwsza synchronizacja startuje w tle.",
+ "polarOauthConnected": "Polar połączony. Pierwsza synchronizacja startuje w tle.",
"polarOauthFailed": "Połączenie z Polar nie powiodło się",
"polarOauthError": {
"csrf1": "Kontrola bezpieczeństwa nie powiodła się, ponieważ brakowało pliku cookie przeglądarki. Rozpocznij połączenie ponownie w tej samej przeglądarce.",
@@ -6587,7 +6821,7 @@
"ouraClientId": "Client ID",
"ouraClientSecret": "Client Secret",
"ouraCredentialsSaved": "Dane zapisane",
- "ouraCredentialsSavedPlaceholder": "Zapisano — wpisz nowe, aby zastąpić",
+ "ouraCredentialsSavedPlaceholder": "Zapisano. Wpisz nowe, aby zastąpić",
"ouraSaveCredentials": "Zapisz",
"ouraUnavailable": "Oura nie jest skonfigurowana na tym serwerze. Dodaj powyżej własne dane OAuth Oura lub poproś administratora o skonfigurowanie współdzielonej aplikacji.",
"ouraConnect": "Połącz Oura",
@@ -6597,7 +6831,7 @@
"ouraDisconnect": "Rozłącz",
"ouraDisconnectTitle": "Rozłączyć Oura?",
"ouraDisconnectDescription": "Przyszłe synchronizacje zostaną zatrzymane. Już zaimportowane odczyty pozostaną w historii.",
- "ouraOauthConnected": "Oura połączona — pierwsza synchronizacja startuje w tle.",
+ "ouraOauthConnected": "Oura połączona. Pierwsza synchronizacja startuje w tle.",
"ouraOauthFailed": "Połączenie z Oura nie powiodło się",
"ouraOauthError": {
"csrf1": "Kontrola bezpieczeństwa nie powiodła się, ponieważ brakowało pliku cookie przeglądarki. Rozpocznij połączenie ponownie w tej samej przeglądarce.",
@@ -6607,6 +6841,7 @@
"nocreds": "Oura nie jest skonfigurowana na tym serwerze. Poproś administratora o dodanie danych OAuth Oura.",
"token": "Wymiana tokena z Oura nie powiodła się. Rozpocznij połączenie ponownie.",
"rate_limited": "Zbyt wiele prób połączenia. Poczekaj chwilę i spróbuj ponownie.",
+ "issuer": "Odpowiedź logowania nie pochodziła z serwera autoryzacji Oura, więc połączenie zostało przerwane dla Twojego bezpieczeństwa. Rozpocznij połączenie ponownie. Jeśli to się powtarza, powiadom administratora.",
"generic": "Nieznany błąd podczas łączenia. Rozpocznij połączenie ponownie."
},
"integrationSetupGuide": "Przewodnik konfiguracji",
@@ -6618,10 +6853,17 @@
"savedOn": "Coach włączony.",
"savedOff": "Coach ukryty.",
"saveError": "Nie udało się zapisać zmiany. Spróbuj ponownie."
+ },
+ "memory": {
+ "edit": "Edytuj",
+ "source": {
+ "user": "od ciebie",
+ "coach": "od Coacha"
+ }
}
},
"integrationRedirectGuide": {
- "intro": "To połączenie działa przez Twoją własną aplikację deweloperską {provider} — około 5 minut konfiguracji:",
+ "intro": "To połączenie działa przez Twoją własną aplikację deweloperską {provider}, około 5 minut konfiguracji:",
"step1": "Utwórz aplikację w portalu deweloperskim {provider}.",
"step2": "Ustaw jej URL przekierowania/callback na:",
"step3": "Wklej powyżej otrzymany identyfikator klienta i sekret klienta."
@@ -6681,7 +6923,7 @@
},
"recovery": {
"saveNow": "Zapisz teraz te kody odzyskiwania",
- "saveNowHint": "Każdy kod działa raz. Są wyświetlane tylko teraz — przechowuj je w bezpiecznym miejscu. Jeśli stracisz aplikację uwierzytelniającą, kod odzyskiwania będzie jedyną drogą powrotu.",
+ "saveNowHint": "Każdy kod działa raz. Są wyświetlane tylko teraz, więc przechowuj je w bezpiecznym miejscu. Jeśli stracisz aplikację uwierzytelniającą, kod odzyskiwania będzie jedyną drogą powrotu.",
"download": "Pobierz",
"remainingOne": "Pozostał {count} kod odzyskiwania.",
"remainingOther": "Pozostało {count} kodów odzyskiwania.",
@@ -6709,7 +6951,7 @@
},
"passkeyNudge": {
"title": "Dodaj klucz dostępu, aby logować się szybciej",
- "body": "Klucze dostępu pozwalają logować się odciskiem palca, twarzą lub kodem PIN urządzenia — bez hasła do zapamiętania."
+ "body": "Klucze dostępu pozwalają logować się odciskiem palca, twarzą lub kodem PIN urządzenia, bez hasła do zapamiętania."
},
"trustedDevices": {
"title": "Zaufane urządzenia",
@@ -6745,12 +6987,11 @@
},
"stored": {
"title": "Co jest przechowywane",
- "measurements": "Pomiary (waga, ciśnienie, tętno, glukoza, sen, skład ciała)",
- "medications": "Leki i historia przyjmowania",
- "moodLabs": "Wpisy nastroju i wyniki badań laboratoryjnych",
- "coach": "Rozmowy z asystentem",
+ "measurements": "Pomiary: waga, ciśnienie, tętno i każda inna wartość zapisana przez ciebie lub urządzenie",
"integrations": "Tokeny integracji (szyfrowane)",
- "security": "Aktywność konta i zabezpieczeń (sesje, dziennik logowań)"
+ "security": "Aktywność konta i zabezpieczeń (sesje, dziennik logowań)",
+ "profile": "Twój profil, wywiad medyczny i dane dokumentacji zdrowotnej",
+ "modules": "Wpisy poszczególnych modułów: {list}"
},
"retention": {
"title": "Retencja",
@@ -6781,13 +7022,11 @@
"mcp": {
"enableTitle": "Zdalny punkt końcowy MCP",
"enableDescription": "Zezwól asystentowi obsługującemu MCP (Claude.ai, ChatGPT) na odczyt danych zdrowotnych za pomocą ograniczonego tokenu.",
- "enabled": "Włączony",
- "disabled": "Wyłączony",
"enableToggleLabel": "Włącz punkt końcowy MCP",
"endpointLabel": "Adres URL łącznika",
"connectDescription": "Wklej ten adres URL w konfiguracji własnego łącznika asystenta, a następnie autoryzuj za pomocą poniższego tokenu lub zaloguj się po wyświetleniu monitu.",
"tokensTitle": "Tokeny łącznika",
- "tokensDescription": "Wygeneruj token dla asystenta — tylko odczyt, chyba że pozwolisz poniżej na zapis.",
+ "tokensDescription": "Wygeneruj token dla asystenta: tylko odczyt, chyba że pozwolisz poniżej na zapis.",
"scopeNote": "Nowe tokeny mogą tylko odczytywać Twoje dane. Dostęp do zapisu (health:write) przyznajesz dla każdego tokena przełącznikiem poniżej. Token nigdy nie może uzyskać dostępu do funkcji administracyjnych.",
"writeScopeToggleLabel": "Zezwól na zapis (health:write)",
"writeScopeNote": "Dodaje zakres health:write, aby asystent mógł zapisywać nowe pomiary i wpisy nastroju na Twoim koncie. Każdy zapis jest najpierw potwierdzany. Token nadal nie może usuwać ani edytować istniejących wpisów, zmieniać leków ani uzyskiwać dostępu do funkcji REST lub administracyjnych. Pozostaw wyłączone, aby uzyskać dostęp tylko do odczytu.",
@@ -6801,7 +7040,8 @@
"connectionRevokeDescription": "To natychmiast kończy dostęp asystenta i uniemożliwia odnawianie. Ponowne połączenie będzie wymagać nowej autoryzacji.",
"connectionRevokeFailed": "Nie udało się rozłączyć asystenta.",
"tokensDetail": "Token jest wyświetlany tylko raz.",
- "connectionsDetail": "Cofnięcie połączenia natychmiast kończy dostęp, w tym możliwość odnawiania."
+ "connectionsDetail": "Cofnięcie połączenia natychmiast kończy dostęp, w tym możliwość odnawiania.",
+ "tokenNamePlaceholder": "Nazwa, np. Claude Desktop"
},
"strava": "Strava",
"stravaDescription": "Połącz konto Strava, aby synchronizować aktywności z historią treningów.",
@@ -6810,7 +7050,7 @@
"stravaClientId": "Identyfikator klienta",
"stravaClientSecret": "Sekret klienta",
"stravaCredentialsSaved": "Dane zapisane",
- "stravaCredentialsSavedPlaceholder": "Zapisane — wpisz nowe, aby zastąpić",
+ "stravaCredentialsSavedPlaceholder": "Zapisane. Wpisz nowe, aby zastąpić",
"stravaSaveCredentials": "Zapisz",
"stravaUnavailable": "Strava nie jest skonfigurowana na tym serwerze. Dodaj powyżej własne dane API Strava lub poproś administratora o skonfigurowanie wspólnej aplikacji.",
"stravaConnect": "Połącz Strava",
@@ -6820,7 +7060,7 @@
"stravaDisconnect": "Rozłącz",
"stravaDisconnectTitle": "Rozłączyć Strava?",
"stravaDisconnectDescription": "Przyszłe synchronizacje zostaną zatrzymane. Już zaimportowane aktywności pozostaną w historii.",
- "stravaOauthConnected": "Strava połączona — pierwsza synchronizacja rusza w tle.",
+ "stravaOauthConnected": "Strava połączona. Pierwsza synchronizacja rusza w tle.",
"stravaOauthFailed": "Połączenie ze Strava nie powiodło się",
"stravaOauthError": {
"csrf1": "Kontrola bezpieczeństwa nie powiodła się, ponieważ brakowało pliku cookie przeglądarki. Rozpocznij połączenie ponownie w tej samej przeglądarce.",
@@ -6848,33 +7088,40 @@
"title": "Postęp synchronizacji",
"receivedLabel": "Odebrane rekordy",
"oldestLabel": "Najstarszy osiągnięty pomiar",
- "flowing": "W ciągu ostatniej godziny dotarły dane — aplikacja na iPhonie nadal je przesyła.",
- "waiting": "Oczekiwanie na kolejne dane z aplikacji na iPhonie. Przesyłanie trwa dalej, gdy aplikacja jest otwarta lub gdy iOS przydzieli jej czas w tle — cisza podczas pierwszego dużego uzupełniania historii jest normalna, to nie błąd."
+ "flowing": "W ciągu ostatniej godziny dotarły dane. Aplikacja na iPhonie nadal je przesyła.",
+ "waiting": "Oczekiwanie na kolejne dane z aplikacji na iPhonie. Przesyłanie trwa dalej, gdy aplikacja jest otwarta lub gdy iOS przydzieli jej czas w tle. Cisza podczas pierwszego dużego uzupełniania historii jest normalna, to nie błąd."
},
"delivery": {
"title": "Dostarczanie",
"lastTriggerLabel": "Ostatnia synchronizacja dotarła",
"lastBackgroundLabel": "Ostatnia synchronizacja w tle",
- "backgroundNever": "Nigdy — jak dotąd każda synchronizacja dotarła przy otwartej aplikacji",
+ "backgroundNever": "Nigdy: jak dotąd każda synchronizacja dotarła przy otwartej aplikacji",
"noSyncYet": "Żadna synchronizacja jeszcze nie dotarła, więc nie ma tu nic do zgłoszenia.",
"trigger": {
"foreground": "przy otwartej aplikacji",
"background": "w tle",
"push": "z wybudzenia push",
- "unknown": "aplikacja nie zgłosiła wyzwalacza"
+ "unknown": "aplikacja nie zgłosiła wyzwalacza",
+ "manual": "przez „Synchronizuj wszystko”"
}
+ },
+ "freshness": {
+ "title": "Według typu danych",
+ "lastReceived": "Ostatnio odebrano {relative}",
+ "lastNewValue": "Ostatnia nowa wartość {relative}",
+ "never": "Jeszcze nie odebrano"
}
},
"garminInfo": {
"title": "Garmin",
- "body": "Garmin nie ma tutaj bezpośredniego połączenia. Połącz przez Apple Health (iOS) lub Google Health Connect (Android). Zastrzeżone wskaźniki Garmina — Body Battery, Training Load, status HRV, VO₂ max — nie są udostępniane przez Garmina żadnej z tych platform.",
+ "body": "Garmin nie ma tutaj bezpośredniego połączenia. Połącz przez Apple Health (iOS) lub Google Health Connect (Android). Zastrzeżone wskaźniki Garmina (Body Battery, Training Load, status HRV, VO₂ max) nie są udostępniane przez Garmina żadnej z tych platform.",
"docsLink": "Przeczytaj przewodnik"
},
"glucoseReference": {
"title": "Zakres referencyjny glukozy",
"description": "Do jakiego zakresu porównywane są twoje wyniki glukozy we krwi.",
"enable": "Użyj zakresu docelowego dla cukrzycy",
- "explainer": "Wyłączone — wyniki są oceniane względem ogólnego zakresu referencyjnego dla osób bez cukrzycy. Włączone — są oceniane względem węższego zakresu docelowego glikemii z ADA Standards of Care, a Coach opiera na nim swoje uwagi o glukozie. Twoje własne wartości docelowe poniżej nadal mają pierwszeństwo.",
+ "explainer": "Wyłączone: wyniki są oceniane względem ogólnego zakresu referencyjnego dla osób bez cukrzycy. Włączone: są oceniane względem węższego zakresu docelowego glikemii z ADA Standards of Care, a Coach opiera na nim swoje uwagi o glukozie. Twoje własne wartości docelowe poniżej nadal mają pierwszeństwo.",
"disclaimer": "To wybiera zakres referencyjny. Nie jest to diagnoza, nigdy nie jest wywnioskowane z pomiaru, a aplikacja nie podejmuje na tej podstawie działań klinicznych. O to, jakie cele dotyczą ciebie, zapytaj swojego lekarza.",
"error": "Nie udało się zapisać ustawienia."
},
@@ -6923,6 +7170,12 @@
"description": "Ta czynność udostępnia lub eksportuje Twoje dane zdrowotne. Potwierdź, że to Ty, aby kontynuować.",
"signInAgain": "Zaloguj się ponownie, a potem spróbuj jeszcze raz. Twoje konto nie ma hasła ani klucza dostępu do potwierdzenia tutaj.",
"failed": "To się nie udało. Sprawdź wprowadzone dane i spróbuj ponownie."
+ },
+ "passwordTitle": "Hasło",
+ "tokenNameLabel": "Nazwa tokena",
+ "displayCard": {
+ "title": "Wyświetlanie",
+ "description": "Język, jednostki i formaty zapisywane od razu po zmianie."
}
},
"admin": {
@@ -6942,27 +7195,27 @@
"loadingStatus": "Ładowanie stanu...",
"systemStatusLoadError": "Nie udało się załadować stanu systemu.",
"systemStatusRetry": "Spróbuj ponownie",
- "adminSettingsLoadError": "Nie udało się załadować ustawień administracyjnych. Przełączniki poniżej mogą nie odzwierciedlać bieżącego stanu — odśwież stronę.",
+ "adminSettingsLoadError": "Nie udało się załadować ustawień administracyjnych. Przełączniki poniżej mogą nie odzwierciedlać bieżącego stanu, odśwież stronę.",
"hostMetrics": {
"title": "Obciążenie hosta",
"load1": "Obciążenie (1m)",
"memUsedPercent": "Pamięć",
"diskBusyPercent": "Disk I/O",
"last2hours": "Ostatnie 2 godziny",
- "empty": "Rozpoczęto próbkowanie — dane pojawią się w ciągu 1–2 minut.",
+ "empty": "Rozpoczęto próbkowanie. Dane pojawią się w ciągu 1–2 minut.",
"loadError": "Nie udało się załadować metryk hosta. Spróbuj odświeżyć stronę."
},
"aiServerKey": {
"title": "Serwerowy klucz AI",
"description": "Klucz operatora dla kont, które nie mają własnego.",
- "byokHint": "Własny klucz użytkownika (BYOK, logowanie ChatGPT lub dostawca lokalny) zawsze ma pierwszeństwo — klucz serwera jest ostatnim ogniwem łańcucha dostawców.",
+ "byokHint": "Własny klucz użytkownika (BYOK, logowanie ChatGPT lub dostawca lokalny) zawsze ma pierwszeństwo; klucz serwera jest ostatnim ogniwem łańcucha dostawców.",
"consentHint": "Żądania przez klucz serwera wymagają aktywnej zgody użytkownika na AI; bez potwierdzenia zgody nic nie jest wysyłane.",
"baseUrlHint": "Adres bazowy decyduje o API: adres Anthropic taki jak https://api.anthropic.com/v1 jest rozpoznawany, każdy inny host jest wywoływany przez API zgodne z OpenAI.",
"statusConfigured": "Skonfigurowany",
"statusNotConfigured": "Nieskonfigurowany",
"keyLabel": "Klucz API",
"keyPlaceholder": "sk-...",
- "keyPlaceholderSet": "Zapisany ({preview}) — wpisz nowy klucz, aby go zastąpić",
+ "keyPlaceholderSet": "Zapisany ({preview}). Wpisz nowy klucz, aby go zastąpić",
"modelLabel": "Model",
"baseUrlLabel": "Adres bazowy URL",
"save": "Zapisz",
@@ -6996,13 +7249,16 @@
"typeCodex": "Logowanie ChatGPT (osobiste)",
"typeLocal": "Model lokalny (osobisty)",
"typeGateway": "Brama zgodna z OpenAI (osobista)",
- "spendToday": "Dzisiaj ({date}): {total} tokenów, w tym {operator} na kluczu operatora."
+ "spendToday": "Dzisiaj, według dnia kalendarzowego każdego konta: {total} tokenów, w tym {operator} na kluczu operatora (konta ze zużyciem: {accounts}).",
+ "statusPaused": "Wstrzymany",
+ "pausedHint": "Wstrzymany po powtarzających się błędach dla {count} użytkowników do {at}. Potem zostanie automatycznie ponowiony.",
+ "modelNotServed": "Punkt końcowy nie oferuje już modelu {model}. Wybierz w ustawieniach AI model, który oferuje."
},
"aiQuality": {
"title": "Jakość analiz",
"loading": "Ładowanie podsumowania opinii...",
"loadError": "Nie udało się załadować podsumowania jakości analiz. Spróbuj odświeżyć stronę.",
- "empty": "Brak opinii. Agregator uruchamia się codziennie o 04:00 — gdy użytkownicy zaczną oceniać rekomendacje, tutaj pojawią się wskaźniki przydatności.",
+ "empty": "Brak opinii. Agregator uruchamia się codziennie o 04:00. Gdy użytkownicy zaczną oceniać rekomendacje, tutaj pojawią się wskaźniki przydatności.",
"windowLabel": "Ostatnie {days} dni",
"generatedAtLabel": "Wygenerowano",
"colSeverity": "Ważność",
@@ -7017,7 +7273,7 @@
"title": "Opinie o Coachu",
"loading": "Ładowanie podsumowania opinii o Coachu...",
"loadError": "Nie udało się załadować podsumowania opinii o Coachu. Spróbuj odświeżyć stronę.",
- "empty": "Brak opinii o Coachu. Agregator uruchamia się codziennie o 04:00 — gdy użytkownicy ocenią odpowiedzi Coacha, tutaj pojawią się wskaźniki przydatności.",
+ "empty": "Brak opinii o Coachu. Agregator uruchamia się codziennie o 04:00. Gdy użytkownicy ocenią odpowiedzi Coacha, tutaj pojawią się wskaźniki przydatności.",
"windowLabel": "Ostatnie {days} dni",
"generatedAtLabel": "Wygenerowano",
"colPromptVersion": "Wersja promptu",
@@ -7036,13 +7292,13 @@
"appSettingsDescription": "Globalne ustawienia domyślne rejestracji, języka i strefy czasowej nowych kont.",
"registrationEnabled": "Zezwól na rejestrację",
"registrationEnabledDescription": "Gdy wyłączone, nowi użytkownicy nie mogą się zarejestrować.",
- "registrationOpenHint": "Rejestracja jest otwarta — każdy, kto ma dostęp do tej instancji, może założyć konto. Zamknij ją tutaj, gdy wszyscy będą mieć konto.",
+ "registrationOpenHint": "Rejestracja jest otwarta: każdy, kto ma dostęp do tej instancji, może założyć konto. Zamknij ją tutaj, gdy wszyscy będą mieć konto.",
"defaultLanguage": "Domyślny język",
"defaultLanguageDescription": "Używany dla nowych użytkowników i jako rozwiązanie zapasowe.",
"defaultUserTimezone": "Domyślna strefa czasowa dla nowych kont",
- "defaultUserTimezoneDescription": "Używana dla nowych kont bez strefy wykrytej przez przeglądarkę. Nie wpływa na istniejących użytkowników.",
+ "defaultUserTimezoneDescription": "Używana dla nowych kont bez strefy wykrytej przez przeglądarkę; istniejące konta zachowują swoją.",
"defaultUserTimezoneFallback": "Europe/Berlin (domyślna)",
- "servicesGlobal": "Usługi (globalne)",
+ "servicesGlobal": "Kanały i API",
"servicesGlobalDescription": "Wyłączone usługi nie są dostępne dla żadnego użytkownika.",
"telegramGlobal": "Zezwól na powiadomienia Telegram.",
"ntfyGlobal": "Zezwól na powiadomienia ntfy.",
@@ -7050,19 +7306,21 @@
"integrationWebPush": "Web Push",
"offlineGeoLabel": "Dane geograficzne",
"offlineGeoEnabled": "Offline (GeoLite2)",
- "offlineGeoFallback": "Online ({host}) — podłącz bazy GeoLite2 i ustaw GEOLITE2_DIR",
+ "offlineGeoFallback": "Online ({host}): podłącz bazy GeoLite2 i ustaw GEOLITE2_DIR",
+ "offlineGeoOff": "Wyłączone",
+ "offlineGeoChosen": "Online ({host})",
"apiGlobal": "Zezwól na punkty końcowe API i tokeny API.",
"umamiTitle": "Umami",
- "umamiDescription": "Analityka internetowa z Umami",
+ "umamiDescription": "Analityka internetowa z Umami.",
"glitchtipTitle": "GlitchTip",
- "glitchtipDescription": "Śledzenie błędów z GlitchTip",
+ "glitchtipDescription": "Śledzenie błędów z GlitchTip.",
"webPushVapidTitle": "Web Push VAPID",
- "umamiEnabled": "Włącz śledzenie Umami.",
+ "umamiEnabled": "Włącz śledzenie Umami",
"umamiScriptUrl": "Adres URL skryptu Umami",
"umamiScriptUrlPlaceholder": "https://analytics.example.com/script.js",
"umamiWebsiteId": "Identyfikator witryny Umami",
"umamiWebsiteIdPlaceholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "glitchtipEnabled": "Włącz śledzenie błędów GlitchTip.",
+ "glitchtipEnabled": "Włącz śledzenie błędów GlitchTip",
"glitchtipDsn": "GlitchTip DSN",
"glitchtipDsnPlaceholder": "https://@glitchtip.example.com/",
"glitchtipEnvironment": "Środowisko GlitchTip",
@@ -7085,7 +7343,7 @@
"webPushVapidGenerating": "Generowanie…",
"webPushVapidGenerated": "Klucze VAPID wygenerowane. Zapisz temat, aby zakończyć.",
"webPushVapidGenerateFailed": "Nie udało się wygenerować kluczy VAPID.",
- "webPushVapidGenerateConfirm": "Klucze VAPID są już skonfigurowane. Wygenerowanie nowej pary unieważnia wszystkie subskrypcje push w przeglądarce — każde urządzenie musi zasubskrybować ponownie. Kontynuować?",
+ "webPushVapidGenerateConfirm": "Klucze VAPID są już skonfigurowane. Wygenerowanie nowej pary unieważnia wszystkie subskrypcje push w przeglądarce, więc każde urządzenie musi zasubskrybować ponownie. Kontynuować?",
"webPushVapidGenerateConfirmTitle": "Wygenerować nowe klucze VAPID?",
"webPushVapidRegenerate": "Wygeneruj ponownie",
"configured": "Skonfigurowano",
@@ -7113,18 +7371,23 @@
"documentAi": {
"title": "Czytanie dokumentów",
"description": "Czytanie zapisanych dokumentów przez AI, skanowanie wyników badań laboratoryjnych i odczytywanie leków z tekstu. Największa część danych opuszczających serwer."
+ },
+ "reasoning": {
+ "enabled": "Zezwalaj na głębszy namysł",
+ "max": "Największa głębokość namysłu",
+ "description": "Modele myślą przed odpowiedzią: Coach, codzienne podsumowanie i podsumowania okresów. Dokładniej, ale więcej tokenów."
}
},
"modules": {
"title": "Dostępność modułów",
"description": "Wyłącz moduł dla całego serwera.",
"saveError": "Nie udało się zaktualizować dostępności modułów.",
- "detail": "Moduł wyłączony tutaj jest nieaktywny dla każdego konta, niezależnie od preferencji użytkownika. Domeny podstawowe (waga, ciśnienie, tętno, leki) są zawsze włączone i nie można ich wyłączyć.",
- "coachFollowsSwitch": "Podlega przełącznikowi Coach w sekcji Asystent."
+ "detail": "Waga, ciśnienie i tętno to podstawowe pomiary i zawsze pozostają włączone.",
+ "coachFollowsSwitch": "Podlega przełącznikowi Coach w sekcji Coach."
},
"invites": {
- "title": "Zaproszenia",
- "description": "Linki z zaproszeniem do rejestracji — działają nawet przy zamkniętej rejestracji.",
+ "title": "Linki z zaproszeniem",
+ "description": "Każdy link pozwala na rejestrację nawet przy zamkniętej rejestracji.",
"create": "Utwórz zaproszenie",
"createTitle": "Utwórz zaproszenie",
"createDescription": "Link umożliwia rejestrację nawet przy zamkniętej rejestracji.",
@@ -7136,8 +7399,8 @@
"createdToast": "Zaproszenie utworzone",
"createError": "Nie udało się utworzyć zaproszenia.",
"tokenTitle": "Link z zaproszeniem",
- "tokenOnceHint": "Do skopiowania tylko teraz — skopiuj link lub udostępnij kod QR do zeskanowania.",
- "hashExplainer": "Zapisywany jest tylko hash linku — nie można go później ponownie wyświetlić. W razie utraty unieważnij zaproszenie i utwórz nowe.",
+ "tokenOnceHint": "Do skopiowania tylko teraz: skopiuj link lub udostępnij kod QR do zeskanowania.",
+ "hashExplainer": "Zapisywany jest tylko hash linku, więc nie można go później ponownie wyświetlić. W razie utraty unieważnij zaproszenie i utwórz nowe.",
"copyLink": "Kopiuj link",
"copiedToast": "Link skopiowany",
"copyError": "Kopiowanie nie powiodło się.",
@@ -7170,8 +7433,8 @@
"revokedToast": "Zaproszenie unieważnione.",
"revokeError": "Nie udało się unieważnić zaproszenia."
},
- "userManagement": "Zarządzanie użytkownikami",
- "userManagementDescription": "Zarządzaj kontami: role, resety haseł i wymuszone wylogowania.",
+ "userManagement": "Konta",
+ "userManagementDescription": "Role, resety haseł i wymuszone wylogowania dla każdego konta.",
"userName": "Nazwa",
"userEmail": "E-mail",
"userRole": "Rola",
@@ -7198,14 +7461,13 @@
"tokenName": "Nazwa",
"tokenPermissions": "Uprawnienia",
"tokenPermissionAll": "Wszystko",
- "tokenPermissionAllTooltip": "Uprawnienie wieloznaczne — ten token może wywołać każdą trasę API.",
+ "tokenPermissionAllTooltip": "Uprawnienie wieloznaczne: ten token może wywołać każdą trasę API.",
"tokenStatus": "Stan",
"tokenLastUsed": "Ostatnio użyto",
"tokenCreated": "Utworzono",
"tokenNeverUsed": "Nigdy",
"tokensEmptyTitle": "Brak tokenów API",
"tokensEmptyDescription": "Tokeny są wydawane, gdy klient natywny się loguje, lub przez Ustawienia → Konto.",
- "loginOverview": "Przegląd logowań",
"loginOverviewDescription": "Zdarzenia logowania i uwierzytelniania wszystkich kont, z filtrowaniem i eksportem CSV.",
"allAuthEvents": "Wszystkie zdarzenia uwierzytelniania",
"failedOnly": "Tylko nieudane",
@@ -7238,6 +7500,7 @@
"finalDelete": "Usuń trwale",
"deletedResult": "Usunięto {rows} wierszy w {tables} tabelach. Konta użytkowników i dane logowania zostały zachowane.",
"authRegister": "Rejestracja",
+ "authClaim": "Profil przejęty",
"authLogin": "Zaloguj",
"authLoginPasskey": "Logowanie kluczem passkey",
"authLoginPassword": "Logowanie hasłem",
@@ -7312,7 +7575,7 @@
},
"services": {
"title": "Usługi",
- "subtitle": "Włącz lub wyłącz kanały powiadomień i integracje w całym systemie."
+ "subtitle": "Włącz lub wyłącz kanały powiadomień i API dla całego serwera."
},
"integrations": {
"title": "Integracje",
@@ -7376,7 +7639,7 @@
"app-logs": {
"title": "Logi aplikacji",
"subtitle": "Wyświetl najnowsze ustrukturyzowane wide-events emitowane przez ten proces.",
- "processNote": "Wyświetlane są tylko logi procesu aplikacji — logi procesu workera trafiają do Loki, gdy ta integracja jest skonfigurowana.",
+ "processNote": "Wyświetlane są tylko logi procesu aplikacji; logi procesu workera trafiają do Loki, gdy ta integracja jest skonfigurowana.",
"filterTraceId": "Identyfikator śladu",
"filterAction": "Akcja zawiera…",
"filterLevel": "Poziom",
@@ -7398,7 +7661,8 @@
"refresh": "Odśwież",
"eventDetails": "Szczegóły zdarzenia",
"closeDetails": "Zamknij szczegóły",
- "processDetail": "Bufor jest ulotny i ograniczony do 500 wpisów."
+ "processDetail": "Bufor jest ulotny i ograniczony do 500 wpisów.",
+ "cardTitle": "Ostatnie zdarzenia"
},
"backups": {
"title": "Kopie zapasowe",
@@ -7410,7 +7674,7 @@
"loading": "Ładowanie kopii zapasowych...",
"loadError": "Nie udało się załadować kopii zapasowych.",
"emptyTitle": "Brak kopii zapasowych",
- "emptyDescription": "Kopie zapasowe wykonują się automatycznie w każdą niedzielę o 03:00 — lub uruchom jedną teraz, aby utworzyć pierwszą migawkę.",
+ "emptyDescription": "Kopie zapasowe wykonują się automatycznie w każdą niedzielę o 03:00. Możesz też uruchomić jedną teraz, aby utworzyć pierwszą migawkę.",
"colUser": "Użytkownik",
"colType": "Typ",
"typeWeeklyAuto": "Cotygodniowo (auto)",
@@ -7425,6 +7689,12 @@
"scheduleFailedDescription": "{when}: {reason}",
"scheduleFailedNoReason": "nie zapisano przyczyny",
"scheduleRemedy": "Uruchom kopię teraz i sprawdź, czy pojawi się nowy wiersz. Jeśli znowu się nie powiedzie, log workera wskaże konto, na którym się zatrzymał.",
+ "activity": {
+ "running": "Trwa od {when}.",
+ "interruptedTitle": "Przebieg przerwał się w połowie i nie wrócił",
+ "interruptedAccount": "{username}: rozpoczęto {when}, nigdy nie zakończono",
+ "interruptedRemedy": "Najpewniej aplikacja uruchomiła się ponownie albo zabrakło jej pamięci podczas zapisu tej kopii. Następny przebieg weźmie te konta na końcu, aby wszystkie pozostałe dostały kopię wcześniej. Jeśli to się powtórzy, przydziel kontenerowi więcej pamięci."
+ },
"download": "Pobierz",
"downloadAria": "Pobierz kopię zapasową dla {username}",
"downloadFailed": "Pobieranie nie powiodło się",
@@ -7440,11 +7710,11 @@
"restoreTitle": "Przywróć z kopii zapasowej",
"restoreDescription": "Spowoduje to zastąpienie WSZYSTKICH bieżących danych {username} migawką z {when}. Istniejące pomiary, leki, zdarzenia przyjęcia, wpisy nastroju, kanały powiadomień, subskrypcje push i zakolejkowane usunięcia Telegram zostaną usunięte.",
"restoreInstanceSettingsLabel": "Przywróć też ustawienia instancji",
- "restoreInstanceSettingsHint": "Rejestracja, wymóg MFA, domyślny język, dostępność modułów, konfiguracja powiadomień i AI — brane z migawki i stosowane do wszystkich w tej instalacji. Zostaw niezaznaczone, aby przywrócić tylko to konto.",
+ "restoreInstanceSettingsHint": "Rejestracja, wymóg MFA, domyślny język, dostępność modułów, konfiguracja powiadomień i AI: brane z migawki i stosowane do wszystkich w tej instalacji. Zostaw niezaznaczone, aby przywrócić tylko to konto.",
"restorePromptLabel": "Wpisz RESTORE, aby kontynuować",
"previewTitle": "Zawartość kopii",
"previewLoading": "Odczytywanie zawartości…",
- "previewUnavailable": "Nie udało się odczytać zawartości — przywracanie jest nadal możliwe.",
+ "previewUnavailable": "Nie udało się odczytać zawartości, ale przywracanie jest nadal możliwe.",
"previewStillReading": "Ta kopia powstała we wcześniejszej wersji, więc jej zawartość jest odczytywana w całości, co przy dużych danych chwilę trwa. Otwórz to okno ponownie za kilka minut, aby ją zobaczyć. Przywracanie od tego nie zależy.",
"previewEmpty": "Plik nie zawiera rekordów.",
"previewOther": "Inne rekordy",
@@ -7487,7 +7757,7 @@
"restoreSkippedMedicationTarget": "Analit docelowy leku",
"restoreSkippedScheduleRevisionLink": "Następstwo okresów schematu",
"restoreSkippedAccountSetting": "Ustawienie konta",
- "restoreMissingTitle": "Przywracanie odrzucone — plik jest niekompletny",
+ "restoreMissingTitle": "Przywracanie odrzucone: plik jest niekompletny",
"restoreMissingDescription": "Manifest pliku mówi, że zawiera on poniższą sekcję, a jej nie zawiera. Nic nie zostało zmienione: przywracanie zastępuje całą klasę rekordów, więc brakująca sekcja by ją opróżniła.",
"restoreMissingRemedy": "Użyj kopii do odtwarzania po awarii albo wyeksportuj konto ponownie i wgraj nowy plik. Sekcja, którą manifest jawnie deklaruje jako pominiętą, to co innego i przywraca się normalnie.",
"restoreMissingDocuments": "Dokumenty",
@@ -7555,7 +7825,12 @@
"restoreFailureKeyMissing": "Kopia wymaga klucza szyfrowania {keys}, którym ten serwer nie może jej otworzyć. Dodaj klucz z powrotem do ENCRYPTION_KEYS, uruchom ponownie i rozpocznij przywracanie jeszcze raz.",
"restoreFailureForeignReference": "Plik wskazuje na wpisy należące do innego konta. Nic nie zostało zapisane.",
"previewKeyMissing": "Tej kopii nie da się przywrócić na tym serwerze: wymaga klucza szyfrowania {keys}. Najpierw dodaj klucz z powrotem do ENCRYPTION_KEYS i uruchom ponownie.",
- "restoreSkippedSymptomEpisodeReference": "Wpis objawu powiązany z chorobą, której nie ma w pliku (zachowany bez powiązania)"
+ "restoreSkippedSymptomEpisodeReference": "Wpis objawu powiązany z chorobą, której nie ma w pliku (zachowany bez powiązania)",
+ "restoreSkippedCycleCiphertext": "Notatka lub etykieta cyklu zaszyfrowana kluczem, którego ten serwer nie ma (dzień lub objaw zachowany bez niej)",
+ "restoreSkippedMoodLabelCiphertext": "Nazwa tagu lub grupy nastroju zaszyfrowana kluczem, którego ten serwer nie ma (zachowana bez nazwy)",
+ "restoreSkippedVisitCiphertext": "Tekst wizyty lub lekarza zaszyfrowany kluczem, którego ten serwer nie ma (zachowany bez niego)",
+ "restoreSkippedVaccinationCiphertext": "Notatka szczepienia zaszyfrowana kluczem, którego ten serwer nie ma (dawka zachowana bez niej)",
+ "restoreSkippedEnvironmentLocationCiphertext": "Lokalizacja okresu lub dnia środowiskowego zaszyfrowana kluczem, którego ten serwer nie ma (zachowana w postaci zaszyfrowanej, te dni nie są odświeżane)"
},
"danger-zone": {
"title": "Strefa zagrożenia",
@@ -7572,7 +7847,6 @@
"encryption": {
"title": "Szyfrowanie",
"subtitle": "Pokrycie szyfrowaniem i status rotacji kluczy dla danych w spoczynku.",
- "loading": "Ładowanie statusu szyfrowania...",
"loadError": "Nie udało się wczytać statusu szyfrowania.",
"coverageTitle": "Pokrycie",
"coverageDescription": "Ile zaszyfrowanych wierszy przypada na każdy identyfikator klucza.",
@@ -7607,7 +7881,7 @@
"completed": "Zakończono",
"failed": "Niepowodzenie"
},
- "coverageDetail": "Pokazywane są tylko identyfikatory kluczy i liczby — nigdy materiał klucza.",
+ "coverageDetail": "Pokazywane są tylko identyfikatory kluczy i liczby, nigdy materiał klucza.",
"backupsTitle": "Klucze, których kopie nadal potrzebują",
"backupsDescription": "Wycofany klucz jest potrzebny, dopóki którakolwiek kopia poniżej go wymienia.",
"backupsStoredLine": "Klucz {key}: {count} zapisanych kopii, najstarsza z {when}",
@@ -7617,12 +7891,45 @@
"backupsNone": "Żadna kopia nie zapisała jeszcze kluczy, których potrzebuje.",
"backupsStillNeed": "Wszystkie wiersze są zrotowane, ale kopie nadal potrzebują {keys}",
"backupsUnrecordedBadge": "Wszystkie wiersze są zrotowane, ale starsze kopie mogą nadal potrzebować wycofanego klucza",
- "backupsDetail": "Rotacja ponownie szyfruje każdy wiersz, ale kopia zachowuje klucz, którym zapisano jej zawartość, a jej przywrócenie wymaga tego klucza. Trzymaj wycofany klucz w ENCRYPTION_KEYS, dopóki widnieje poniżej."
+ "backupsDetail": "Rotacja ponownie szyfruje każdy wiersz, ale kopia zachowuje klucz, którym zapisano jej zawartość, a jej przywrócenie wymaga tego klucza. Trzymaj wycofany klucz w ENCRYPTION_KEYS, dopóki widnieje poniżej.",
+ "columnsShow": "Pokaż tabelę (z wierszami: {count})",
+ "columnsHide": "Ukryj tabelę",
+ "columnsShowAll": "Pokaż wszystkie kolumny ({count})",
+ "columnsShowWithRows": "Tylko kolumny z wierszami",
+ "columnsNoneWithRows": "Żadna zaszyfrowana kolumna nie zawiera jeszcze wierszy."
+ },
+ "maintenance": {
+ "title": "Konserwacja tabeli pomiarów",
+ "description": "Zwalnia miejsce zajmowane przez stare pozostałości synchronizacji, gdy czyszczenie już się zakończy.",
+ "body": "Wykonuje vacuum tabeli pomiarów, a potem odbudowuje jej indeksy, jeden po drugim. Odczyt i zapis działają w tym czasie dalej. Wybierz spokojną godzinę i zapewnij wolne miejsce na dysku na jeszcze jedną kopię największego indeksu.",
+ "tableSize": "Tabela z indeksami",
+ "indexSize": "Indeksy",
+ "loading": "Sprawdzanie stanu konserwacji…",
+ "loadError": "Nie udało się odczytać stanu konserwacji.",
+ "unavailable": "Nie jest podłączony żaden proces w tle, więc konserwacja nie może tu działać.",
+ "purgePending": "Czyszczenie starych pozostałości synchronizacji wciąż trwa. Konserwacja może się rozpocząć, gdy się zakończy.",
+ "state": {
+ "idle": "Jeszcze nie uruchomiona.",
+ "queued": "Czeka na proces w tle.",
+ "running": "Trwa od {since}. Na dużej instancji może to chwilę potrwać.",
+ "completed": "Zakończona {at}.",
+ "refusedPurge": "Nie rozpoczęła się, bo czyszczenie wciąż trwało. Uruchom ją ponownie, gdy czyszczenie się zakończy.",
+ "alreadyRunning": "Nie rozpoczęła się, bo inny przebieg już działał.",
+ "failed": "Zatrzymana {at}. Dziennik aplikacji podaje krok, a runbook wyjaśnia, co sprawdzić."
+ },
+ "start": "Uruchom konserwację",
+ "confirmTitle": "Uruchomić konserwację teraz?",
+ "confirmBody": "Działa w tle, krok po kroku, i może chwilę potrwać. Jeśli dziś nie zrobiono jeszcze kopii zapasowej, najpierw ją zrób.",
+ "confirm": "Uruchom",
+ "cancel": "Anuluj",
+ "started": "Konserwacja uruchomiona.",
+ "alreadyQueued": "Konserwacja jest już w kolejce lub trwa.",
+ "startFailed": "Nie udało się uruchomić konserwacji."
}
},
"carrier": "Operator",
"mfaRequired": "Wymagaj drugiego składnika",
- "mfaRequiredDescription": "Każde konto bez aktywnego drugiego składnika jest po zalogowaniu kierowane do konfiguracji. To zachęca użytkowników sieci do włączenia go, ale nie blokuje bezpośredniego dostępu przez API ani natywnego (Bearer) dla kont jeszcze nieskonfigurowanych.",
+ "mfaRequiredDescription": "Konta bez drugiego składnika są po logowaniu w przeglądarce kierowane do jego konfiguracji; dostęp przez API i natywny (Bearer) pozostaje otwarty.",
"mfaEnforcedOn": "MFA wł.",
"mfaEnforcedOff": "MFA wył.",
"mfaEnforcedOnHint": "Ten użytkownik musi używać drugiego składnika. Kliknij, aby przestać go wymagać.",
@@ -7640,7 +7947,7 @@
"centralCodex": {
"title": "Wspólne połączenie AI",
"description": "Podłącz jedno zalogowane konto ChatGPT, które każdy użytkownik tego serwera może włączyć dla funkcji AI.",
- "honesty": "To jedno wspólne, zalogowane konto AI — nie klucz API. Każdy, kto je włączy, wysyła przez nie zapytania, więc godzinowe i tygodniowe limity konta są dzielone między nimi, a te same limity zasilają już Coacha. Dostawcy subskrypcyjni mogą wykorzystywać treść do ulepszania swoich modeli i nie obejmuje tego żadna umowa powierzenia przetwarzania danych, dlatego nigdy nie jest to opcja domyślna i pozostaje poniżej dostawców bez trenowania przy odczycie dokumentów. Podłącz tylko konto, które chcesz udostępniać w ten sposób.",
+ "honesty": "To jedno wspólne, zalogowane konto AI, a nie klucz API. Każdy, kto je włączy, wysyła przez nie zapytania, więc godzinowe i tygodniowe limity konta są dzielone między nimi, a te same limity zasilają już Coacha. Dostawcy subskrypcyjni mogą wykorzystywać treść do ulepszania swoich modeli i nie obejmuje tego żadna umowa powierzenia przetwarzania danych, dlatego nigdy nie jest to opcja domyślna i pozostaje poniżej dostawców bez trenowania przy odczycie dokumentów. Podłącz tylko konto, które chcesz udostępniać w ten sposób.",
"statusConnected": "Połączono",
"statusDisconnected": "Nie połączono",
"statusExpired": "Wygasło",
@@ -7652,13 +7959,13 @@
"connectedSince": "Połączono od {when}",
"connected": "Wspólne połączenie AI podłączone",
"disconnected": "Wspólne połączenie AI usunięte",
- "error": "Coś poszło nie tak — spróbuj ponownie"
+ "error": "Coś poszło nie tak. Spróbuj ponownie"
},
"failingJobs": {
"title": "Zadania w tle",
"description": "Kolejki, których zadania poddały się po wyczerpaniu ponowień.",
"none": "W ciągu ostatnich {hours} godzin żadne zadanie nie zawiodło.",
- "unavailable": "Brak kolejki zadań do odczytu — to wdrożenie działa bez workera w tle.",
+ "unavailable": "Brak kolejki zadań do odczytu: to wdrożenie działa bez workera w tle.",
"countOne": "{count} niepowodzenie",
"countFew": "{count} niepowodzenia",
"countOther": "{count} niepowodzeń",
@@ -7720,7 +8027,9 @@
"restartNote": "Po zmianie uruchom HealthLog ponownie.",
"lastResort": "Ostateczność, tylko jeśli masz pewność, że klucz jest właściwy, a żadna z trzech dróg nie pasuje: ustaw {setting} i uruchom ponownie. HealthLog będzie wtedy działać mimo to i tylko zapisze wynik w logu. To wyłącza kontrolę bezpieczeństwa: przy złym kluczu każda zaszyfrowana wartość zawiedzie, a nowe dane zostaną zapisane tym kluczem.",
"codes": "Dla administratora: żądania API odpowiadają 503 z kodem błędu {code}, a /api/health podaje powód {reason}."
- }
+ },
+ "loginOverviewCardTitle": "Zdarzenia logowania",
+ "userColumn": "Użytkownik"
},
"achievements": {
"title": "Osiągnięcia",
@@ -7736,7 +8045,9 @@
"noneUnlockedYet": "Nie odblokowano jeszcze żadnego osiągnięcia.",
"pointsValue": "{points} punktów",
"metricCount": "{count}",
- "metricDays": "{count} dni",
+ "metricDaysOne": "{count} dzień",
+ "metricDaysFew": "{count} dni",
+ "metricDaysOther": "{count} dni",
"metricPercent": "{count}%",
"badges": {
"intakeTotal1": {
@@ -7759,14 +8070,6 @@
"title": "Legenda przyjęć",
"description": "Zarejestruj 300 przyjęć leków."
},
- "overIntake1": {
- "title": "Podwójna dawka",
- "description": "Przyjmij lek co najmniej raz więcej niż zaplanowano."
- },
- "skippedIntake1": {
- "title": "Krok wstecz",
- "description": "Pomiń co najmniej jedno zaplanowane przyjęcie."
- },
"passkeyCreated1": {
"title": "Klucz dostępu ustawiony",
"description": "Utwórz swój pierwszy klucz dostępu."
@@ -7792,19 +8095,19 @@
"description": "Przyjmij wszystkie leki na czas przez 1 dzień."
},
"compliance801": {
- "title": "Przestrzeganie 80% · 1d",
+ "title": "Przestrzeganie 80% przez 1 dzień",
"description": "Osiągnij co najmniej 80% przestrzegania w 30 dni przez 1 dzień."
},
"bmiGreen1": {
- "title": "BMI na zielono · 1d",
+ "title": "BMI na zielono przez 1 dzień",
"description": "Utrzymaj BMI w normie przez 1 dzień."
},
"bpGreen1": {
- "title": "Ciśnienie na zielono · 1d",
+ "title": "Ciśnienie na zielono przez 1 dzień",
"description": "Utrzymaj ciśnienie w normie przez 1 dzień."
},
"pulseGreen1": {
- "title": "Tętno na zielono · 1d",
+ "title": "Tętno na zielono przez 1 dzień",
"description": "Utrzymaj tętno spoczynkowe w normie przez 1 dzień."
},
"onTimePerfect7": {
@@ -7812,19 +8115,19 @@
"description": "Przyjmij wszystkie leki na czas przez 7 kolejnych dni."
},
"compliance807": {
- "title": "Przestrzeganie 80% · 7d",
+ "title": "Przestrzeganie 80% przez 7 dni",
"description": "Osiągnij co najmniej 80% przestrzegania w 30 dni przez 7 kolejnych dni."
},
"bmiGreen7": {
- "title": "BMI na zielono · 7d",
+ "title": "BMI na zielono przez 7 dni",
"description": "Utrzymaj BMI w normie przez 7 kolejnych dni."
},
"bpGreen7": {
- "title": "Ciśnienie na zielono · 7d",
+ "title": "Ciśnienie na zielono przez 7 dni",
"description": "Utrzymaj ciśnienie w normie przez 7 kolejnych dni."
},
"pulseGreen7": {
- "title": "Tętno na zielono · 7d",
+ "title": "Tętno na zielono przez 7 dni",
"description": "Utrzymaj tętno spoczynkowe w normie przez 7 kolejnych dni."
},
"onTimePerfect30": {
@@ -7832,19 +8135,19 @@
"description": "Przyjmij wszystkie leki na czas przez 30 kolejnych dni."
},
"compliance8030": {
- "title": "Przestrzeganie 80% · 30d",
+ "title": "Przestrzeganie 80% przez 30 dni",
"description": "Osiągnij co najmniej 80% przestrzegania w 30 dni przez 30 kolejnych dni."
},
"bmiGreen30": {
- "title": "BMI na zielono · 30d",
+ "title": "BMI na zielono przez 30 dni",
"description": "Utrzymaj BMI w normie przez 30 kolejnych dni."
},
"bpGreen30": {
- "title": "Ciśnienie na zielono · 30d",
+ "title": "Ciśnienie na zielono przez 30 dni",
"description": "Utrzymaj ciśnienie w normie przez 30 kolejnych dni."
},
"pulseGreen30": {
- "title": "Tętno na zielono · 30d",
+ "title": "Tętno na zielono przez 30 dni",
"description": "Utrzymaj tętno spoczynkowe w normie przez 30 kolejnych dni."
},
"onTimePerfect180": {
@@ -7852,19 +8155,19 @@
"description": "Przyjmij wszystkie leki na czas przez 180 kolejnych dni."
},
"compliance80180": {
- "title": "Przestrzeganie 80% · 180d",
+ "title": "Przestrzeganie 80% przez 180 dni",
"description": "Osiągnij co najmniej 80% przestrzegania w 30 dni przez 180 kolejnych dni."
},
"bmiGreen180": {
- "title": "BMI na zielono · 180d",
+ "title": "BMI na zielono przez 180 dni",
"description": "Utrzymaj BMI w normie przez 180 kolejnych dni."
},
"bpGreen180": {
- "title": "Ciśnienie na zielono · 180d",
+ "title": "Ciśnienie na zielono przez 180 dni",
"description": "Utrzymaj ciśnienie w normie przez 180 kolejnych dni."
},
"pulseGreen180": {
- "title": "Tętno na zielono · 180d",
+ "title": "Tętno na zielono przez 180 dni",
"description": "Utrzymaj tętno spoczynkowe w normie przez 180 kolejnych dni."
},
"onTimePerfect360": {
@@ -7872,19 +8175,19 @@
"description": "Przyjmij wszystkie leki na czas przez 360 kolejnych dni."
},
"compliance80360": {
- "title": "Przestrzeganie 80% · 360d",
+ "title": "Przestrzeganie 80% przez 360 dni",
"description": "Osiągnij co najmniej 80% przestrzegania w 30 dni przez 360 kolejnych dni."
},
"bmiGreen360": {
- "title": "BMI na zielono · 360d",
+ "title": "BMI na zielono przez 360 dni",
"description": "Utrzymaj BMI w normie przez 360 kolejnych dni."
},
"bpGreen360": {
- "title": "Ciśnienie na zielono · 360d",
+ "title": "Ciśnienie na zielono przez 360 dni",
"description": "Utrzymaj ciśnienie w normie przez 360 kolejnych dni."
},
"pulseGreen360": {
- "title": "Tętno na zielono · 360d",
+ "title": "Tętno na zielono przez 360 dni",
"description": "Utrzymaj tętno spoczynkowe w normie przez 360 kolejnych dni."
},
"moodFirst": {
@@ -7968,15 +8271,15 @@
"description": "Zmieniłeś język aplikacji co najmniej raz."
},
"missFree7": {
- "title": "Bez pominiętej dawki · 7 dni",
+ "title": "Bez pominiętej dawki przez 7 dni",
"description": "7 kolejnych dni bez pominiętej dawki."
},
"missFree30": {
- "title": "Bez pominiętej dawki · 30 dni",
+ "title": "Bez pominiętej dawki przez 30 dni",
"description": "30 kolejnych dni bez pominiętej dawki."
},
"missFree90": {
- "title": "Bez pominiętej dawki · 90 dni",
+ "title": "Bez pominiętej dawki przez 90 dni",
"description": "90 kolejnych dni bez pominiętej dawki."
},
"measurementWeeks4": {
@@ -7985,10 +8288,10 @@
},
"selfContextComplete": {
"title": "Wywiad uzupełniony",
- "description": "Wszystkie cztery pola wywiadu wypełnione — Coach zna Twój kontekst."
+ "description": "Wszystkie cztery pola wywiadu wypełnione. Coach zna Twój kontekst."
},
"sleepLog7": {
- "title": "Dziennik snu · 7 dni",
+ "title": "Dziennik snu przez 7 dni",
"description": "Dane o śnie zapisane przez 7 kolejnych dni."
}
},
@@ -8007,7 +8310,7 @@
},
"hiddenCard": {
"title": "Ukryte osiągnięcie",
- "description": "Śledź dalej — może na nie natrafisz.",
+ "description": "Śledź dalej, może na nie natrafisz.",
"ariaLabel": "Ukryte zablokowane osiągnięcie"
},
"hiddenUnlockToast": {
@@ -8016,7 +8319,7 @@
"dashboardCard": {
"title": "Ostatnio odblokowane",
"viewAll": "Zobacz wszystkie",
- "empty": "Brak osiągnięć — zapisuj dalej, aby odblokować pierwsze."
+ "empty": "Brak osiągnięć. Zapisuj dalej, aby odblokować pierwsze."
}
},
"notifications": {
@@ -8078,9 +8381,6 @@
"enabledToast": "Proaktywne podpowiedzi Coacha włączone.",
"disabledToast": "Proaktywne podpowiedzi Coacha wyłączone.",
"saveError": "Nie udało się zapisać. Spróbuj ponownie później.",
- "suggestionsLabel": "Przykładowe sugestie",
- "suggestionsDesc": "Czasami pokazuje gotowe przykładowe pytanie na ekranie Coacha.",
- "suggestionsAria": "Przełącz przykładowe sugestie Coacha",
"aiComposedLabel": "Pozwól Coachowi sformułować przypomnienie",
"aiComposedDesc": "Gdy dostawca jest dostępny, proaktywne przypomnienie tworzy model zamiast stałego szablonu. Domyślnie wyłączone.",
"aiComposedAria": "Przełącz proaktywne przypomnienie tworzone przez SI"
@@ -8151,7 +8451,46 @@
"loginRequired": "Zaloguj się, aby zarządzać powiadomieniami.",
"someChannelsGloballyDisabled": "Co najmniej jeden kanał jest wyłączony w ustawieniach i nie będzie otrzymywać powiadomień.",
"reminderSettingsLink": "Ustawienia przypomnień",
- "channelsLink": "Kanały"
+ "channelsLink": "Kanały",
+ "handover": {
+ "claimedTitle": "{name} ma teraz własne konto",
+ "claimedEnd": "{name} przejmuje swoją kartę zdrowia. Twój dostęp się zakończył.",
+ "claimedRead": "{name} przejmuje swoją kartę zdrowia. Nadal możesz ją oglądać, ale już nie zmieniać.",
+ "claimedManage": "{name} przejmuje swoją kartę zdrowia. Nadal możesz nią zarządzać, dopóki nie zapadnie inna decyzja.",
+ "changedTitle": "{name} zmienia Twój dostęp",
+ "changedEnd": "Twój dostęp do karty zdrowia osoby {name} się zakończył.",
+ "changedRead": "Możesz teraz oglądać kartę zdrowia osoby {name}, ale nie zmieniać jej.",
+ "changedManage": "Możesz zarządzać kartą zdrowia osoby {name}.",
+ "unnamed": {
+ "claimedTitle": "Osoba pod Twoją opieką ma teraz własne konto",
+ "claimedEnd": "Osoba pod Twoją opieką przejęła swoją dokumentację zdrowotną. Twój dostęp wygasł.",
+ "claimedRead": "Osoba pod Twoją opieką przejęła swoją dokumentację zdrowotną. Nadal możesz ją przeglądać, ale nie możesz już jej zmieniać.",
+ "claimedManage": "Osoba pod Twoją opieką przejęła swoją dokumentację zdrowotną. Nadal możesz nią zarządzać, dopóki nie zdecyduje inaczej.",
+ "changedTitle": "Twój dostęp do dokumentacji zdrowotnej się zmienił",
+ "changedEnd": "Twój dostęp do dokumentacji zdrowotnej osoby pod Twoją opieką wygasł.",
+ "changedRead": "Możesz teraz przeglądać dokumentację zdrowotną osoby pod Twoją opieką, ale nie możesz jej zmieniać.",
+ "changedManage": "Możesz zarządzać dokumentacją zdrowotną osoby pod Twoją opieką."
+ }
+ },
+ "links": {
+ "open": "Otwórz w HealthLog"
+ },
+ "quickActions": {
+ "moodGood": "Dobrze",
+ "moodOkay": "W porządku",
+ "moodBad": "Źle"
+ },
+ "content": {
+ "title": "Wartości zdrowotne w powiadomieniach",
+ "description": "Czy powiadomienie może pokazywać wartości, takie jak poranny wynik.",
+ "detail": "Wyłączone: powiadomienie mówi tylko, czego dotyczy, więc ekran blokady nie pokazuje wartości zdrowotnych. Włączone: dodaje wartości, na przykład poranny wynik w podsumowaniu. Dotknięcie powiadomienia w obu przypadkach otwiera właściwe miejsce w aplikacji.",
+ "toggleAria": "Pokazuj wartości zdrowotne w powiadomieniach",
+ "statusDetailed": "Z wartościami",
+ "statusGeneric": "Bez wartości",
+ "detailedToast": "Powiadomienia zawierają teraz wartości zdrowotne.",
+ "genericToast": "Powiadomienia nie zawierają już wartości zdrowotnych.",
+ "saveError": "Nie udało się zapisać. Spróbuj ponownie później."
+ }
},
"i18n": {
"maintainershipBanner": {
@@ -8182,7 +8521,7 @@
}
},
"edit": {
- "title": "Edytuj zakres docelowy — {metric}",
+ "title": "Edytuj zakres docelowy: {metric}",
"description": "Dostosuj zakres docelowy używany w wykresach, insights i raporcie medycznym. Wartości w {unit}.",
"derivedHint": "Ta karta jest wyprowadzona z innych metryk. Edytuj cele bazowe (np. ciśnienie lub waga).",
"minLabel": "Min",
@@ -8206,15 +8545,19 @@
"currentValue": "Aktualnie: {value} {unit}",
"targetRangeValue": "Zakres docelowy: {min}–{max} {unit}",
"sourceLabel": "Źródło: {source}",
+ "sourceNames": {
+ "moodEntries": "Wpisy nastroju",
+ "sevenDays": "7 dni"
+ },
"inTarget": "W zakresie docelowym",
"aboveTarget": "{delta} {unit} powyżej zakresu",
"belowTarget": "{delta} {unit} poniżej zakresu",
"diastolic": "Rozkurczowe",
- "glucoseFasting": "Glikemia — na czczo",
- "glucosePostprandial": "Glikemia — po posiłku",
- "glucoseRandom": "Glikemia — dowolna",
- "glucoseBedtime": "Glikemia — przed snem",
- "glucoseUnspecified": "Glikemia — bez oznaczenia",
+ "glucoseFasting": "Glikemia: na czczo",
+ "glucosePostprandial": "Glikemia: po posiłku",
+ "glucoseRandom": "Glikemia: dowolna",
+ "glucoseBedtime": "Glikemia: przed snem",
+ "glucoseUnspecified": "Glikemia: bez oznaczenia",
"status": {
"underweight": "Niedowaga",
"normal": "Normal",
@@ -8300,7 +8643,7 @@
"askMedicationName": "Podaj lek, np. przyjęty Ramipril.",
"medicationNotFoundExact": "Nie znaleziono leku. Wyślij nazwę dokładnie tak, jak w HealthLog.",
"helpHeader": "Dostępne polecenia:",
- "helpBody": "/help — pokaż tę pomoc\n/add — zapisz przyjęcie\n/start — uruchom bota\n\nPolecenia tekstowe:\ntaken <nazwa> — potwierdź przyjęcie\n\nPrzyciski w przypomnieniach:\n• Przyjęto — potwierdź przyjęcie\n• 🕐 1h / 🕐 3h — odłóż przypomnienie\n• ⏭ Pomiń — pomiń przyjęcie\n• ✓ Potwierdź — potwierdź pominięte przyjęcie",
+ "helpBody": "/help: pokaż tę pomoc\n/add: zapisz przyjęcie\n/start: uruchom bota\n\nPolecenia tekstowe:\ntaken <nazwa>: potwierdź przyjęcie\n\nPrzyciski w przypomnieniach:\n• Przyjęto: potwierdź przyjęcie\n• 🕐 1h / 🕐 3h: odłóż przypomnienie\n• ⏭ Pomiń: pomiń przyjęcie\n• ✓ Potwierdź: potwierdź pominięte przyjęcie",
"measureValuePrompt": "Odpowiedz wartością, aby ją zapisać (opcjonalnie).",
"buttonMeasureValue": "Wartość…",
"measureValueSaved": "Pomiar zapisany.",
@@ -8325,9 +8668,9 @@
},
"lowStockReminders": {
"title": "Niski zapas: {medName}",
- "bodyToday": "{medName}: pozostało {units} jednostek — mniej niż jeden dzień zapasu.",
- "bodyReorder": "{medName}: pozostało {units} jednostek. Zapas skończy się około {runsOutOn} — zamów ponownie do {reorderBy}.",
- "bodyLastDose": "{medName}: pozostało {units} jednostek — to mniej więcej Twoja ostatnia dawka. Zapas skończy się około {runsOutOn}.",
+ "bodyToday": "{medName}: pozostało {units} jednostek, czyli mniej niż jeden dzień zapasu.",
+ "bodyReorder": "{medName}: pozostało {units} jednostek. Zapas skończy się około {runsOutOn}. Zamów ponownie do {reorderBy}.",
+ "bodyLastDose": "{medName}: pozostało {units} jednostek, to mniej więcej Twoja ostatnia dawka. Zapas skończy się około {runsOutOn}.",
"bodyExpired": "{medName}: brak zapasu do użycia. {units} jednostek oznaczono jako przeterminowane.",
"bodyAsNeeded": "{medName}: zostało tylko {units} jednostek. Zamów ponownie, zanim będziesz potrzebować kolejnej dawki."
},
@@ -8354,11 +8697,11 @@
"questionConditions": "Czy masz choroby przewlekłe, o których Coach powinien wiedzieć?",
"questionFocus": "Czy jest coś konkretnego, na co Coach powinien zwracać uwagę?",
"questionAllergies": "Czy masz alergie lub nietolerancje?",
- "questionAboutMe": "Chcesz opowiedzieć Coachowi coś o sobie — codzienność, cele, kontekst?"
+ "questionAboutMe": "Chcesz opowiedzieć Coachowi coś o sobie: codzienność, cele, kontekst?"
},
"clinicianView": {
"title": "Udostępniona dokumentacja zdrowotna",
- "period": "Okres raportu: {start} – {end}",
+ "period": "Okres raportu: {start}–{end}",
"expires": "Ten link wygasa dnia {date}.",
"provenance": "To jest tylko do odczytu podsumowanie udostępnione przez osobę, do której należy, wyeksportowane z jej samodzielnie rejestrowanych danych HealthLog. Wartości są zgłaszane samodzielnie i mogą obejmować odczyty zsynchronizowane z urządzeń; należy je traktować jako zgłoszone przez pacjenta, a nie jako badanie kliniczne.",
"labs": "Wyniki laboratoryjne",
@@ -8382,7 +8725,7 @@
"strain": "Wynik obciążenia",
"score": "Wynik dobrostanu"
},
- "footer": "Udostępnione z HealthLog — samodzielnie hostowanego trackera zdrowia.",
+ "footer": "Udostępnione z HealthLog, samodzielnie hostowanego trackera zdrowia.",
"unlock": {
"title": "Wprowadź hasło",
"description": "Ta udostępniona dokumentacja zdrowotna jest chroniona. Wprowadź otrzymane hasło, aby ją wyświetlić.",
@@ -8417,7 +8760,11 @@
},
"course": "Kuracja {n}",
"courseRange": "od {start} do {end}",
- "courseSince": "od {start}"
+ "courseSince": "od {start}",
+ "unavailable": {
+ "title": "Link wygasł lub jest nieprawidłowy",
+ "description": "Ten link udostępniania wygasł, został cofnięty lub jest niekompletny. Poproś osobę, która go udostępniła, o nowy link."
+ }
},
"cycle": {
"symptomCategory": {
@@ -8466,11 +8813,11 @@
"generic": "Twoje {metric} jest o około {delta} {unit} {dir} w fazie lutealnej niż w fazie folikularnej.",
"dirHigher": "wyższe",
"dirLower": "niższe",
- "empty": "Za mało cykli i pomiarów, aby pokazać wzorzec fazy. Rejestruj dalej — uzupełni się to wraz z historią."
+ "empty": "Za mało cykli i pomiarów, aby pokazać wzorzec fazy. Rejestruj dalej, a uzupełni się to wraz z historią."
},
"crosstab": {
"description": "Jak każdy pomiar wypada między fazą lutealną a folikularną, na Twoich danych.",
- "pairLabel": "{metric} · lutealna vs folikularna",
+ "pairLabel": "{metric}: faza lutealna vs folikularna",
"detail": "Lutealna {lutealAvg} {unit} ({lutealDays} dni) vs folikularna {follicularAvg} {unit} ({follicularDays} dni).",
"metricRestingHeartRate": "Tętno spoczynkowe",
"metricHrv": "Zmienność rytmu serca",
@@ -8506,7 +8853,7 @@
},
"context": {
"MENSTRUAL": "Poziom hormonów jest teraz najniższy, dlatego te dni mogą sprawiać wrażenie spokojnego resetu. Niektóre osoby zauważają skurcze, lżejszy sen albo chęć zwolnienia tempa; inne przechodzą przez nie niemal bez zmian. To wszystko mieści się w normie, a odczucia potrafią zmieniać się z cyklu na cykl.",
- "FOLLICULAR": "Wraz z rosnącym estrogenem wiele osób opisuje te dni jako jasne i pełne otwartości – sen często przychodzi łatwiej, a umysł wydaje się bardziej klarowny. Podstawowa temperatura ciała zwykle utrzymuje się w niższym zakresie aż do chwili tuż przed owulacją. Długość tej fazy różni się najbardziej – od osoby do osoby i od cyklu do cyklu.",
+ "FOLLICULAR": "Wraz z rosnącym estrogenem wiele osób opisuje te dni jako jasne i pełne otwartości: sen często przychodzi łatwiej, a umysł wydaje się bardziej klarowny. Podstawowa temperatura ciała zwykle utrzymuje się w niższym zakresie aż do chwili tuż przed owulacją. Długość tej fazy różni się najbardziej, od osoby do osoby i od cyklu do cyklu.",
"OVULATORY": "Estrogen osiąga szczyt tuż przed uwolnieniem komórki jajowej, często z krótkim wzrostem nastroju i niewielkim podniesieniem podstawowej temperatury ciała w ciągu kolejnego dnia lub dwóch. Śluz szyjkowy w tym oknie zwykle staje się bardziej przejrzysty i ciągliwy. Krótkie ukłucie po jednej stronie niektóre osoby zauważają; wiele nie czuje nic, a oba warianty są zupełnie normalne.",
"LUTEAL": "Progesteron prowadzi przez drugą połowę cyklu, a podstawowa temperatura ciała zwykle ustala się nieco wyżej niż wcześniej. Niektóre osoby zauważają z dnia na dzień zmiany w śnie, apetycie, nastroju czy energii, podczas gdy inne czują się stabilnie przez cały czas. Ten wzorzec jest indywidualny i często wygląda inaczej z miesiąca na miesiąc."
},
@@ -8593,8 +8940,8 @@
"ariaUnknown": "Brak aktywnego cyklu",
"caption": "twojego cyklu",
"firstPeriodCta": "Zapisz pierwszą miesiączkę",
- "periodOverdue": "Okres jest późniejszy niż zwykle — licznik dni jest wstrzymany, dopóki nie zapiszesz następnego okresu.",
- "periodOverdueDays": "Okres jest {days} dni późniejszy niż zwykle — licznik dni jest wstrzymany, dopóki nie zapiszesz następnego okresu.",
+ "periodOverdue": "Okres jest późniejszy niż zwykle. Licznik dni jest wstrzymany, dopóki nie zapiszesz następnego okresu.",
+ "periodOverdueDays": "Okres jest {days} dni późniejszy niż zwykle. Licznik dni jest wstrzymany, dopóki nie zapiszesz następnego okresu.",
"logPeriodCta": "Zapisz okres"
},
"calendar": {
@@ -8606,7 +8953,7 @@
"prevMonth": "Poprzedni miesiąc",
"nextMonth": "Następny miesiąc",
"loading": "Ładowanie kalendarza…",
- "learning": "Uczymy się Twojego cyklu — zapisz co najmniej trzy pełne cykle, a okno płodne, owulacja i fazy się uzupełnią. Do tego czasu kalendarz pokazuje tylko to, co zapisałaś.",
+ "learning": "Uczymy się Twojego cyklu. Zapisz co najmniej trzy pełne cykle, a okno płodne, owulacja i fazy się uzupełnią. Do tego czasu kalendarz pokazuje tylko to, co zapisałaś.",
"legendSymptom": "Objawy",
"legendOvulationConfirmed": "Potwierdzona owulacja",
"flowSpotting": "plamienie",
@@ -8619,7 +8966,7 @@
"predictions": {
"title": "Co dalej",
"nextPeriod": "Następna miesiączka",
- "nextPeriodWindow": "{low} – {high}",
+ "nextPeriodWindow": "{low}–{high}",
"fertileWindow": "Okno płodne",
"ovulation": "Szacowana owulacja",
"confidenceHigh": "Wysoka",
@@ -8628,7 +8975,7 @@
"stillLearning": "Wciąż się uczę",
"stillLearningBody": "Zapisz co najmniej trzy pełne cykle, a prognozy się wyostrzą. Do tego czasu to zgrubne oszacowanie.",
"rawMode": "Prognoza wyłączona",
- "rawModeBody": "Wyłączyłaś prognozy — pokazywane są tylko zapisane dane, bez przewidywań.",
+ "rawModeBody": "Wyłączyłaś prognozy: pokazywane są tylko zapisane dane, bez przewidywań.",
"none": "Brak prognozy. Zapisz miesiączkę, aby zacząć.",
"ovulationConfirmed": "Owulacja (potwierdzona)"
},
@@ -8661,7 +9008,7 @@
"symptoms": "Objawy",
"temperature": "Podstawowa temperatura ciała",
"temperatureDisturbed": "Oznacz pomiar jako zakłócony",
- "temperatureDisturbedHint": "Gorączka, choroba, alkohol lub spóźniony pomiar — wykluczony z oceny temperatury.",
+ "temperatureDisturbedHint": "Gorączka, choroba, alkohol lub spóźniony pomiar: wykluczony z oceny temperatury.",
"ovulationTest": "Test owulacyjny",
"mucus": "Śluz szyjkowy",
"cervix": "Szyjka macicy",
@@ -8690,7 +9037,7 @@
"temperatureSection": "Temperatura i owulacja",
"intimacySection": "Intymność i antykoncepcja",
"testsSection": "Testy",
- "phaseDayHeader": "Dzień {day} · {phase}",
+ "phaseDayHeader": "Dzień {day}, {phase}",
"bbtHint": "Zmierz ją rano zaraz po przebudzeniu, przed wstaniem.",
"titleForDate": "Wpis z {date}",
"descriptionToday": "Dzisiaj. Krwawienie, objawy i notatki.",
@@ -8724,7 +9071,7 @@
"remindersDescription": "Włącz przypomnienia o okresie dla każdego kanału w ustawieniach powiadomień.",
"remindersLink": "Ustawienia powiadomień",
"fertileReminder": "Przypomnienie o oknie płodności",
- "fertileReminderDescription": "Otrzymaj powiadomienie kilka dni przed szacowanym oknem płodności. Domyślnie wyłączone – włącz je dla każdego kanału w ustawieniach powiadomień."
+ "fertileReminderDescription": "Otrzymaj powiadomienie kilka dni przed szacowanym oknem płodności. Domyślnie wyłączone; włącz je dla każdego kanału w ustawieniach powiadomień."
},
"testResult": {
"POSITIVE": "Pozytywny",
@@ -8757,15 +9104,15 @@
"fieldInfo": {
"bbt": "Twoja temperatura spoczynkowa, mierzona rano zaraz po przebudzeniu, przed wstaniem. Po owulacji nieznacznie rośnie (o około 0,2–0,3 °C).",
"bbtLabel": "Czym jest temperatura podstawowa?",
- "mucus": "Wydzielina z pochwy zmienia się w cyklu — od suchej lub lepkiej do kremowej, a następnie wodnistej i przypominającej białko jaja w najbardziej płodnych dniach.",
+ "mucus": "Wydzielina z pochwy zmienia się w cyklu: od suchej lub lepkiej do kremowej, a następnie wodnistej i przypominającej białko jaja w najbardziej płodnych dniach.",
"mucusLabel": "Czym jest śluz szyjkowy?",
- "ovulationTest": "Domowy test moczu wykrywający skok LH — wyrzut hormonu około 24–36 h przed owulacją. Wynik dodatni wskazuje najbardziej płodne okno.",
+ "ovulationTest": "Domowy test moczu wykrywający skok LH, czyli wyrzut hormonu około 24–36 h przed owulacją. Wynik dodatni wskazuje najbardziej płodne okno.",
"ovulationTestLabel": "Czym jest test owulacyjny?",
"spotting": "Lekkie krwawienie poza okresem.",
"spottingLabel": "Czym jest plamienie między okresami?",
"pregnancyTest": "Wynik domowego testu ciążowego (hCG).",
"pregnancyTestLabel": "Czym jest test ciążowy?",
- "progesteroneTest": "Test potwierdzający, że doszło do owulacji — progesteron rośnie po niej.",
+ "progesteroneTest": "Test potwierdzający, że doszło do owulacji: progesteron rośnie po niej.",
"progesteroneTestLabel": "Czym jest test progesteronu?",
"intercourse": "Czy tego dnia doszło do stosunku.",
"intercourseLabel": "Co śledzi stosunek?",
@@ -8818,14 +9165,14 @@
"postpone": {
"menuItem": "Przełóż…",
"title": "Przełóż termin",
- "description": "Przesuwa tylko bieżący termin — regularny rytm pozostaje bez zmian.",
+ "description": "Przesuwa tylko bieżący termin, a regularny rytm pozostaje bez zmian.",
"chip": "+{days} dni",
"dateLabel": "Nowa data",
"confirm": "Przełóż"
},
"skip": {
"heading": "Pomiń ten termin",
- "explainer": "Zapisane uczciwie jako pominięte — nie jako wykonane. Odstęp zaczyna się od nowa od dzisiaj.",
+ "explainer": "Zapisane uczciwie jako pominięte, nie jako wykonane. Odstęp zaczyna się od nowa od dzisiaj.",
"action": "Pomiń",
"confirmTitle": "Pominąć ten termin?"
},
@@ -8867,10 +9214,10 @@
"VISCERAL_FAT": "Tłuszcz trzewny",
"BODY_MASS_INDEX": "Wskaźnik masy ciała (BMI)",
"WAIST_CIRCUMFERENCE": "Obwód talii",
- "PHQ9_SCORE": "PHQ-9 · nastrój",
- "GAD7_SCORE": "GAD-7 · lęk",
- "WHO5_SCORE": "WHO-5 · dobrostan",
- "SCI_SCORE": "SCI · sen"
+ "PHQ9_SCORE": "PHQ-9 (nastrój)",
+ "GAD7_SCORE": "GAD-7 (lęk)",
+ "WHO5_SCORE": "WHO-5 (dobrostan)",
+ "SCI_SCORE": "SCI (sen)"
},
"typeGroups": {
"vitals": "Parametry życiowe",
@@ -8966,7 +9313,7 @@
"localOcr": {
"heading": "Odczytuj skany na tym urządzeniu",
"description": "Odczytuje zdjęcie na Twoim urządzeniu i wysyła do dostawcy tylko rozpoznany tekst.",
- "detail": "Przydatne, gdy dostawca AI nie odczytuje obrazów. Mniej dokładne — sprawdź każdą wartość przed zapisem."
+ "detail": "Przydatne, gdy dostawca AI nie odczytuje obrazów. Mniej dokładne, więc sprawdź każdą wartość przed zapisem."
},
"sort": {
"heading": "Sortowanie",
@@ -9010,7 +9357,7 @@
"sourceRangePlaceholder": "np. 3,5 - 5,0 lub < 5",
"sourceRangeAdd": "+ Dodaj zakres z wyniku",
"sourceRangeHint": "Opcjonalne. Dotyczy tylko tego pomiaru; zapisany zakres pozostaje bez zmian.",
- "sourceRangeUnparsed": "Zapisane bez zmian — nie odczytano z tego zakresu liczbowego."
+ "sourceRangeUnparsed": "Zapisane bez zmian, nie odczytano z tego zakresu liczbowego."
},
"backToLabs": "Wróć do wyników",
"hasNote": "Notatka",
@@ -9028,7 +9375,7 @@
"emptyDescription": "Dodaj wartość, aby śledzić ten biomarker w czasie.",
"title": "Biomarker",
"genericDescription": "Biomarker laboratoryjny, który śledzisz. Dodaj pomiary, aby obserwować jego trend względem zakresu referencyjnego.",
- "valuesTitle": "{marker} — wszystkie odczyty",
+ "valuesTitle": "{marker}: wszystkie odczyty",
"valuesDescription": "Każdy odczyt tego biomarkera. Edytuj lub usuń wpis bezpośrednio.",
"backToMarker": "Powrót do {marker}",
"latestResult": "Ostatni wynik"
@@ -9048,7 +9395,7 @@
},
"biomarker": {
"manageTitle": "Biomarkery",
- "managerDescription": "Zdefiniuj marker raz — jednostka i zakres są używane dla każdego pomiaru.",
+ "managerDescription": "Zdefiniuj marker raz, a jednostka i zakres będą używane dla każdego pomiaru.",
"define": "Zdefiniuj",
"defineFirst": "Zdefiniuj pierwszy biomarker",
"defineTitle": "Zdefiniuj biomarker",
@@ -9067,7 +9414,7 @@
"form": {
"seedLabel": "Zacznij od popularnego markera",
"seedPlaceholder": "Wybierz marker (opcjonalnie)",
- "seedHint": "Wybór wypełnia pola poniżej — dopasuj zakres do swojego laboratorium.",
+ "seedHint": "Wybór wypełnia pola poniżej. Dopasuj zakres do swojego laboratorium.",
"name": "Nazwa",
"namePlaceholder": "np. Cholesterol LDL",
"unit": "Jednostka",
@@ -9081,7 +9428,7 @@
"upperPlaceholder": "Górna granica",
"context": "Kontekst",
"contextPlaceholder": "Opcjonalna notatka o tym markerze",
- "rangeHint": "Obie granice są opcjonalne — zostaw jedną pustą dla zakresu otwartego (np. LDL < 116).",
+ "rangeHint": "Obie granice są opcjonalne. Zostaw jedną pustą dla zakresu otwartego (np. LDL < 116).",
"requiredError": "Nazwa i jednostka są wymagane.",
"rangeOrderError": "Dolna granica nie może przekraczać górnej.",
"save": "Zapisz biomarker",
@@ -9137,36 +9484,36 @@
"bloodCount": "Morfologia krwi"
},
"desc": {
- "total-cholesterol": "Cholesterol całkowity to suma cholesterolu przenoszonego przez wszystkie cząstki lipoprotein we krwi, w tym frakcje LDL, HDL i pozostałości. Sam cholesterol jest niezbędnym budulcem błon komórkowych, hormonów i kwasów żółciowych. Jako pojedyncza liczba pozostaje miarą zgrubną, ponieważ zarówno wysoki udział HDL, jak i wysoki udział LDL podnoszą sumę — dlatego zwykle odczytuje się go wraz z poszczególnymi frakcjami. Oznacza się go przede wszystkim w ramach długoterminowej oceny ryzyka sercowo-naczyniowego.",
+ "total-cholesterol": "Cholesterol całkowity to suma cholesterolu przenoszonego przez wszystkie cząstki lipoprotein we krwi, w tym frakcje LDL, HDL i pozostałości. Sam cholesterol jest niezbędnym budulcem błon komórkowych, hormonów i kwasów żółciowych. Jako pojedyncza liczba pozostaje miarą zgrubną, ponieważ zarówno wysoki udział HDL, jak i wysoki udział LDL podnoszą sumę, dlatego zwykle odczytuje się go wraz z poszczególnymi frakcjami. Oznacza się go przede wszystkim w ramach długoterminowej oceny ryzyka sercowo-naczyniowego.",
"ldl": "Cholesterol LDL to cholesterol przenoszony przez cząstki lipoprotein o małej gęstości, czyli frakcja, która dostarcza cholesterol do tkanek, ale może też odkładać go w ścianach tętnic. Przez lata odkładanie to przyczynia się do blaszki miażdżycowej, dlatego LDL jest jednym z najbaczniej obserwowanych markerów lipidowych. Mniejsza ekspozycja w długim okresie zwykle wiąże się z niższym ryzykiem sercowo-naczyniowym. Interpretuje się go według kategorii ryzyka, a nie wobec jednej stałej wartości prawidłowej.",
"hdl": "Cholesterol HDL to cholesterol przenoszony przez cząstki lipoprotein o dużej gęstości, które zbierają nadmiar cholesterolu z tkanek i odprowadzają go do wątroby w celu ponownego wykorzystania lub usunięcia. Z powodu tej funkcji transportowej często nazywa się go frakcją ochronną. Bardzo niskie wartości zwykle towarzyszą wyższemu ryzyku sercowo-naczyniowemu, jednak sztuczne jego podnoszenie nie obniżało ryzyka w sposób wiarygodny, dlatego odczytuje się go jako marker, a nie cel. Zwykle interpretuje się go razem z LDL i trójglicerydami.",
"triglycerides": "Trójglicerydy to główna postać tłuszczu krążącego we krwi, dostarczana zarówno z dietą, jak i wytwarzana przez wątrobę, i stanowią kluczowy zapas energii. Wartości rosną po posiłkach, dlatego próbkę pobiera się zwykle na czczo, aby była porównywalna. Utrzymujące się wysokie trójglicerydy wiążą się z ryzykiem metabolicznym i sercowo-naczyniowym i często towarzyszą insulinooporności oraz niskiemu HDL. Odczytuje się je jako jeden ze składników całościowego obrazu lipidowego.",
"fasting-glucose": "Glukoza na czczo to stężenie cukru we krwi mierzone po nocnym poście, odzwierciedlające, jak organizm utrzymuje swój podstawowy poziom cukru bez niedawnego posiłku. Glukoza jest głównym natychmiastowym paliwem organizmu, a insulina utrzymuje ją zwykle w wąskim zakresie. Pomiar na czczo eliminuje wpływ niedawnych posiłków, co czyni go standardowym badaniem przesiewowym działania regulacji glukozy. To jeden z markerów, którymi ocenia się zdrowie metaboliczne w czasie.",
"hba1c": "HbA1c to odsetek hemoglobiny w czerwonych krwinkach, który związał się z glukozą i gromadzi się proporcjonalnie do średniej ekspozycji na cukier we krwi. Ponieważ czerwone krwinki żyją około trzech miesięcy, odzwierciedla średnie stężenie glukozy z poprzednich dwóch–trzech miesięcy, a nie pojedynczej chwili. Czyni to z niego stabilny, niezależny od posiłków obraz kontroli glikemii. Jego wiarygodność mogą zaburzać stany zmieniające czas życia czerwonych krwinek.",
"tsh": "TSH (hormon tyreotropowy) jest uwalniany przez przysadkę, aby sygnalizować tarczycy wytwarzanie jej hormonów, i dostosowuje się w górę lub w dół, by utrzymać je w równowadze. Dzięki tej pętli sprzężenia zwrotnego TSH jest najczulszym badaniem przesiewowym pierwszego rzutu funkcji tarczycy: zwykle zmienia się, zanim same hormony tarczycy odbiegną od normy. Podwyższona lub obniżona wartość wskazuje na niedoczynną lub nadczynną tarczycę i zwykle prowadzi do bezpośredniego oznaczenia hormonów tarczycy. Ciąża i wiek przesuwają oczekiwany zakres.",
- "ft3": "Wolne T3 (trójjodotyronina) to niezwiązana, biologicznie czynna postać silniejszego hormonu tarczycy — frakcja dostępna do działania na tkanki. Większość krążącego T3 jest związana z białkami nośnikowymi i nieaktywna, dlatego wolna część odzwierciedla rzeczywistą dostępność hormonu. Oznacza się je, aby scharakteryzować funkcję tarczycy, zwłaszcza wraz z TSH i wolnym T4, gdy wyniki są niejasne. Wartości silnie zależą od metody oznaczenia i odczytuje się je w kontekście klinicznym.",
+ "ft3": "Wolne T3 (trójjodotyronina) to niezwiązana, biologicznie czynna postać silniejszego hormonu tarczycy, czyli frakcja dostępna do działania na tkanki. Większość krążącego T3 jest związana z białkami nośnikowymi i nieaktywna, dlatego wolna część odzwierciedla rzeczywistą dostępność hormonu. Oznacza się je, aby scharakteryzować funkcję tarczycy, zwłaszcza wraz z TSH i wolnym T4, gdy wyniki są niejasne. Wartości silnie zależą od metody oznaczenia i odczytuje się je w kontekście klinicznym.",
"ft4": "Wolne T4 (tyroksyna) to niezwiązana postać głównego hormonu, który tarczyca uwalnia do krwi; służy jako rezerwa, którą tkanki w razie potrzeby przekształcają w bardziej aktywne T3. Wolna frakcja odzwierciedla to, co jest faktycznie dostępne dla organizmu, niezależnie od poziomu białek nośnikowych. Zwykle oznacza się je wraz z TSH, aby odróżnić problemy samej tarczycy od zaburzeń sygnalizacji przysadkowej. Ciąża i niektóre leki mogą je przesuwać.",
- "ferritin": "Ferrytyna to główne białko magazynujące żelazo w organizmie, a jej poziom we krwi z grubsza odzwierciedla całkowite zapasy żelaza. Niska wartość wskazuje na wyczerpane zapasy, a wysoka może odzwierciedlać przeładowanie żelazem — lub po prostu stan zapalny, ponieważ ferrytyna rośnie także jako białko ostrej fazy. Z tego powodu często interpretuje się ją wraz z innymi markerami żelaza i oznakami zapalenia. To jedna z podstawowych miar oceny gospodarki żelazem.",
- "transferrin-saturation": "Wysycenie transferyny to odsetek transferyny — białka przenoszącego żelazo we krwi — który jest aktualnie obładowany żelazem. Łączy ono ilość żelaza w transporcie ze zdolnością organizmu do jego przenoszenia, dając migawkę dostępności żelaza, która uzupełnia obraz zapasów dawany przez ferrytynę. Niskie wysycenie sugeruje niedobór żelaza, a wysoka wartość może wskazywać na przeładowanie. Odczytuje się je wraz z ferrytyną i pełnym profilem żelaza.",
+ "ferritin": "Ferrytyna to główne białko magazynujące żelazo w organizmie, a jej poziom we krwi z grubsza odzwierciedla całkowite zapasy żelaza. Niska wartość wskazuje na wyczerpane zapasy, a wysoka może odzwierciedlać przeładowanie żelazem lub po prostu stan zapalny, ponieważ ferrytyna rośnie także jako białko ostrej fazy. Z tego powodu często interpretuje się ją wraz z innymi markerami żelaza i oznakami zapalenia. To jedna z podstawowych miar oceny gospodarki żelazem.",
+ "transferrin-saturation": "Wysycenie transferyny to odsetek transferyny (białka przenoszącego żelazo we krwi), który jest aktualnie obładowany żelazem. Łączy ono ilość żelaza w transporcie ze zdolnością organizmu do jego przenoszenia, dając migawkę dostępności żelaza, która uzupełnia obraz zapasów dawany przez ferrytynę. Niskie wysycenie sugeruje niedobór żelaza, a wysoka wartość może wskazywać na przeładowanie. Odczytuje się je wraz z ferrytyną i pełnym profilem żelaza.",
"vitamin-d": "Witaminę D oznacza się jako 25-hydroksywitaminę D, krążącą postać magazynową i standardowy marker ogólnego stanu witaminy D. Organizm wytwarza ją w skórze wystawionej na światło słoneczne i część przyswaja z pożywienia; wspiera ona gospodarkę wapniową, zdrowie kości i funkcję odpornościową. Wartości zmieniają się wraz z porą roku, szerokością geograficzną, odcieniem skóry i suplementacją. Oznacza się ją, aby z czasem ocenić, czy zapasy są wystarczające.",
- "vitamin-b12": "Witamina B12 (kobalamina) to niezbędna witamina pozyskiwana głównie z produktów pochodzenia zwierzęcego i magazynowana w dużej mierze w wątrobie. Jest potrzebna do tworzenia czerwonych krwinek, syntezy DNA i zdrowia układu nerwowego. Długotrwały niedobór — z powodu małego spożycia lub upośledzonego wchłaniania — może prowadzić do niedokrwistości i objawów neurologicznych. Oznacza się ją, aby ocenić stan odżywienia, zwłaszcza tam, gdzie spożycie lub wchłanianie mogą być ograniczone.",
+ "vitamin-b12": "Witamina B12 (kobalamina) to niezbędna witamina pozyskiwana głównie z produktów pochodzenia zwierzęcego i magazynowana w dużej mierze w wątrobie. Jest potrzebna do tworzenia czerwonych krwinek, syntezy DNA i zdrowia układu nerwowego. Długotrwały niedobór (z powodu małego spożycia lub upośledzonego wchłaniania) może prowadzić do niedokrwistości i objawów neurologicznych. Oznacza się ją, aby ocenić stan odżywienia, zwłaszcza tam, gdzie spożycie lub wchłanianie mogą być ograniczone.",
"folate": "Foliany (witamina B9) to witamina z grupy B obecna w zielonych warzywach liściastych, roślinach strączkowych i żywności wzbogacanej, kluczowa dla tworzenia czerwonych krwinek i syntezy DNA. Działa w ścisłym powiązaniu z witaminą B12, a niedobór któregokolwiek z nich może wywołać podobny rodzaj niedokrwistości. Odpowiednia ilość folianów jest szczególnie ważna na początku ciąży dla zdrowego rozwoju płodu. Oznacza się je, aby ocenić stan odżywienia i przyczynę niektórych niedokrwistości.",
"hs-crp": "Wysokoczułe CRP mierzy białko C-reaktywne, marker uwalniany przez wątrobę w odpowiedzi na stan zapalny, metodą dostrojoną do wykrywania niskich stężeń istotnych dla ryzyka sercowo-naczyniowego. W odróżnieniu od standardowego CRP stosowanego przy ostrych infekcjach wersja wysokoczuła wychwytuje subtelny, przewlekły stan zapalny powiązany z chorobą naczyń. Ponieważ każda niedawna infekcja lub uraz może je podnieść, interpretuje się je z dala od ostrej choroby i często potwierdza powtórną próbką. Odczytuje się je jako jeden z czynników całościowego ryzyka sercowo-naczyniowego.",
"creatinine": "Kreatynina to produkt przemiany materii prawidłowego metabolizmu mięśni, który nerki filtrują z krwi w dość stałym tempie. Ponieważ jej wytwarzanie jest stosunkowo stałe, jej poziom we krwi służy jako praktyczna miara tego, jak dobrze nerki usuwają produkty przemiany materii. Na wartości wpływają też masa mięśniowa, nawodnienie i dieta, dlatego zwykle interpretuje się je wraz z szacowanym współczynnikiem filtracji. To podstawowy marker funkcji nerek.",
"egfr": "eGFR (szacowany współczynnik filtracji kłębuszkowej) szacuje, ile krwi nerki filtrują w ciągu minuty, na podstawie kreatyniny oraz wieku i płci. Przekłada surową wartość kreatyniny na bardziej bezpośrednio zrozumiałą miarę funkcji nerek. Spadek eGFR w czasie to główny sposób klasyfikowania i śledzenia przewlekłych zmian w nerkach. Jako wartość szacunkowa odczytuje się go jako trend, a nie pojedynczą dokładną liczbę.",
"alt": "ALT (aminotransferaza alaninowa) to enzym skupiony w komórkach wątroby, uwalniany do krwi, gdy komórki te są obciążone lub uszkodzone. Ponieważ jest stosunkowo swoisty dla wątroby, należy do głównych markerów wykrywania uszkodzenia lub zapalenia wątroby. Wartości mogą rosnąć przy stłuszczeniu wątroby, wirusowym zapaleniu wątroby, alkoholu lub niektórych lekach. Zwykle odczytuje się go wraz z AST i pozostałymi markerami profilu wątrobowego.",
"ast": "AST (aminotransferaza asparaginianowa) to enzym obecny w wątrobie, ale także w mięśniach, sercu i czerwonych krwinkach, uwalniany przy uszkodzeniu którejkolwiek z tych tkanek. Ponieważ jest mniej swoisty dla wątroby niż ALT, oba interpretuje się razem, a ich stosunek może wskazywać na źródło i charakter problemu. Intensywny wysiłek lub uraz mięśni mogą go podnieść niezależnie od wątroby. To jeden ze standardowych enzymów profilu wątrobowego.",
- "ggt": "GGT (gamma-glutamylotransferaza) to enzym wątroby i dróg żółciowych, który rośnie zwłaszcza przy utrudnionym odpływie żółci lub w wyniku ekspozycji na alkohol. Często służy do wyjaśnienia, dlaczego inny marker wątrobowy jest podwyższony — na przykład, aby odróżnić źródło wątrobowe od żółciowego. Jest czuły, ale mało swoisty, dlatego odczytuje się go w kontekście reszty profilu wątrobowego. To jeden z kilku markerów używanych do oceny zdrowia wątroby i dróg żółciowych.",
+ "ggt": "GGT (gamma-glutamylotransferaza) to enzym wątroby i dróg żółciowych, który rośnie zwłaszcza przy utrudnionym odpływie żółci lub w wyniku ekspozycji na alkohol. Często służy do wyjaśnienia, dlaczego inny marker wątrobowy jest podwyższony, na przykład, aby odróżnić źródło wątrobowe od żółciowego. Jest czuły, ale mało swoisty, dlatego odczytuje się go w kontekście reszty profilu wątrobowego. To jeden z kilku markerów używanych do oceny zdrowia wątroby i dróg żółciowych.",
"sodium": "Sód to główny elektrolit płynu poza komórkami organizmu i podstawowy regulator gospodarki płynami oraz objętości krwi. Organizm reguluje go ściśle poprzez pragnienie, hormony i nerki, dlatego jego poziom we krwi odzwierciedla raczej równowagę między wodą a solą niż samo spożycie. Nieprawidłowe wartości zwykle sygnalizują zaburzenie gospodarki wodnej, a nie samego sodu. To podstawowy element panelu metabolicznego i elektrolitowego.",
"potassium": "Potas to główny elektrolit wewnątrz komórek organizmu i jest niezbędny do przewodzenia nerwowego oraz równego rytmu mięśnia sercowego. Jego poziom we krwi utrzymywany jest w wąskim zakresie przez nerki i sygnały hormonalne, ponieważ zarówno wysokie, jak i niskie wartości mogą zaburzać aktywność elektryczną serca. Na wartości mogą wpływać funkcja nerek, nawodnienie i kilka powszechnych leków. To podstawowa miara panelu elektrolitowego.",
"hemoglobin": "Hemoglobina to zawierające żelazo białko czerwonych krwinek, które wiąże tlen w płucach i przenosi go do tkanek. Jej stężenie jest główną miarą zdolności krwi do przenoszenia tlenu i centralnym markerem wykrywania niedokrwistości. Niskie wartości mogą odzwierciedlać utratę krwi, niedobór żelaza lub witamin albo zmniejszone wytwarzanie, a wysokie mogą następować po odwodnieniu lub w warunkach niedoboru tlenu. To filar pełnej morfologii krwi.",
"hematocrit": "Hematokryt to udział czerwonych krwinek w objętości krwi, wyrażony w procentach. Zmienia się ściśle wraz z hemoglobiną i daje uzupełniający obraz masy czerwonych krwinek oraz zdolności krwi do przenoszenia tlenu. Wpływa na niego nawodnienie, ponieważ zmiany objętości osocza przesuwają ten udział nawet wtedy, gdy liczba czerwonych krwinek pozostaje stała. Odczytuje się go wraz z hemoglobiną i resztą morfologii.",
"wbc": "Liczba białych krwinek to całkowita liczba komórek odpornościowych krążących we krwi, ruchomej obrony organizmu przed infekcjami. Rośnie w odpowiedzi na infekcję, stan zapalny lub stres i może spadać, gdy szpik kostny jest osłabiony lub w niektórych chorobach odpornościowych. Ponieważ sumuje kilka odrębnych typów komórek, wyraźną zmianę często uzupełnia się rozmazem, który rozbija ją na składowe. To podstawowy element pełnej morfologii krwi.",
"platelets": "Płytki krwi to drobne fragmenty komórek we krwi, które zlepiają się i tworzą początkowy czop zatrzymujący krwawienie z uszkodzonego naczynia. Ich liczba odzwierciedla równowagę między wytwarzaniem w szpiku kostnym a zużyciem lub rozpadem. Niska liczba może zwiększać skłonność do krwawień, a wysoka może następować po stanie zapalnym lub innych bodźcach. Stanowią część pełnej morfologii krwi i są podstawową miarą zdolności krzepnięcia.",
- "apob": "Apolipoproteina B to białko strukturalne obecne na każdej z aterogennych cząstek lipoprotein, więc jej pomiar zlicza te cząstki bezpośrednio, a nie cholesterol, który przenoszą. Ponieważ na każdej cząstce LDL, VLDL i pokrewnej znajduje się jedna cząsteczka ApoB, może ona wychwycić ryzyko sercowo-naczyniowe, które umyka wartości cholesterolu LDL — zwłaszcza gdy liczba cząstek i ich zawartość cholesterolu się rozchodzą. Z tego powodu coraz częściej używa się jej do uściślenia oceny ryzyka opartej na lipidach. Interpretuje się ją wraz ze zwykłym profilem lipidowym.",
+ "apob": "Apolipoproteina B to białko strukturalne obecne na każdej z aterogennych cząstek lipoprotein, więc jej pomiar zlicza te cząstki bezpośrednio, a nie cholesterol, który przenoszą. Ponieważ na każdej cząstce LDL, VLDL i pokrewnej znajduje się jedna cząsteczka ApoB, może ona wychwycić ryzyko sercowo-naczyniowe, które umyka wartości cholesterolu LDL, zwłaszcza gdy liczba cząstek i ich zawartość cholesterolu się rozchodzą. Z tego powodu coraz częściej używa się jej do uściślenia oceny ryzyka opartej na lipidach. Interpretuje się ją wraz ze zwykłym profilem lipidowym.",
"lp-a": "Lipoproteina(a) to cząstka podobna do LDL z dodatkowo przyłączonym białkiem, której poziom jest w dużej mierze uwarunkowany genetycznie i pozostaje dość stabilny przez całe życie. Podwyższona Lp(a) jest niezależnym, dziedzicznym czynnikiem ryzyka chorób sercowo-naczyniowych i zwężenia zastawki aortalnej. Ponieważ niewiele się zmienia, zwykle oznacza się ją raz, aby ustalić ryzyko danej osoby na całe życie, a nie śledzić wielokrotnie. Podaje się ją w jednostkach molowych i odczytuje w ramach całościowego ryzyka sercowo-naczyniowego.",
- "fasting-insulin": "Insulina na czczo to poziom insuliny — hormonu, który przenosi glukozę z krwi do komórek — mierzony po nocnym poście. W połączeniu z glukozą na czczo pomaga oszacować insulinooporność, czyli stan, w którym organizm potrzebuje więcej insuliny, aby utrzymać glukozę w zakresie. Wysoka insulina na czczo przy prawidłowej glukozie może być wczesnym znakiem, że ta kompensacja jest w toku. Odczytuje się ją w ramach obrazu zdrowia metabolicznego, a nie w oderwaniu.",
- "omega-3-index": "Indeks omega-3 to udział EPA i DHA — dwóch długołańcuchowych morskich kwasów tłuszczowych omega-3 — wśród kwasów tłuszczowych błon czerwonych krwinek. Ponieważ czerwone krwinki odnawiają się powoli, odzwierciedla on długoterminowe spożycie omega-3, a nie niedawny posiłek. Wyższy indeks wiązano w badaniach obserwacyjnych z niższym ryzykiem sercowo-naczyniowym. Śledzi się go jako stabilny marker stanu omega-3, który reaguje na dietę w ciągu miesięcy."
+ "fasting-insulin": "Insulina na czczo to poziom insuliny (hormonu, który przenosi glukozę z krwi do komórek) mierzony po nocnym poście. W połączeniu z glukozą na czczo pomaga oszacować insulinooporność, czyli stan, w którym organizm potrzebuje więcej insuliny, aby utrzymać glukozę w zakresie. Wysoka insulina na czczo przy prawidłowej glukozie może być wczesnym znakiem, że ta kompensacja jest w toku. Odczytuje się ją w ramach obrazu zdrowia metabolicznego, a nie w oderwaniu.",
+ "omega-3-index": "Indeks omega-3 to udział EPA i DHA (dwóch długołańcuchowych morskich kwasów tłuszczowych omega-3) wśród kwasów tłuszczowych błon czerwonych krwinek. Ponieważ czerwone krwinki odnawiają się powoli, odzwierciedla on długoterminowe spożycie omega-3, a nie niedawny posiłek. Wyższy indeks wiązano w badaniach obserwacyjnych z niższym ryzykiem sercowo-naczyniowym. Śledzi się go jako stabilny marker stanu omega-3, który reaguje na dietę w ciągu miesięcy."
},
"apob": "Apolipoproteina B (ApoB)",
"lp-a": "Lipoproteina(a)",
@@ -9184,7 +9531,7 @@
"acceptImageOnly": "JPEG, PNG lub WebP",
"extracting": "Odczytywanie wyniku…",
"readingOnDevice": "Odczytywanie na Twoim urządzeniu…",
- "localModeHint": "Odczytywane na Twoim urządzeniu i wysyłane jako tekst. Mniej dokładne — sprawdź każdą wartość.",
+ "localModeHint": "Odczytywane na Twoim urządzeniu i wysyłane jako tekst. Mniej dokładne, więc sprawdź każdą wartość.",
"rangeFromScan": "Zakres referencyjny pobrany ze skanu",
"rangeVerify": "Zweryfikuj zakres referencyjny",
"reviewTitle": "Sprawdź przed zapisaniem",
@@ -9199,12 +9546,12 @@
"dateLabel": "Data",
"refLowLabel": "Dolna granica",
"refHighLabel": "Górna granica",
- "newBiomarker": "Nowy biomarker — zostanie utworzony",
+ "newBiomarker": "Nowy biomarker, zostanie utworzony",
"linksExisting": "Łączy z istniejącym {name}",
"duplicateWarning": "Pasujący wynik już istnieje",
"lowConfidence": "Sprawdź tę wartość",
"lowConfidenceBadge": "Niska pewność",
- "valueUnreadable": "Wartość nieczytelna — wprowadź ją ręcznie",
+ "valueUnreadable": "Wartość nieczytelna, wprowadź ją ręcznie",
"saveSelected": "Zapisz {count} wybranych",
"discardAll": "Odrzuć wszystko",
"savedToast": "Zapisano {count} wyników laboratoryjnych",
@@ -9320,6 +9667,10 @@
"vaccinations": {
"label": "Szczepienia",
"description": "Książeczka szczepień uporządkowana: dawka, seria, następne przypomnienie."
+ },
+ "timeline": {
+ "label": "Oś czasu",
+ "description": "Lata w jednym widoku: choroby, leki i wizyty obok Twoich wartości, a do tego Twoje własne wydarzenia życiowe."
}
},
"moduleOff": {
@@ -9351,8 +9702,8 @@
},
"form": {
"date": "Data",
- "catalogLabel": "Szczepionka (katalog)",
- "identityHint": "Wybierz z katalogu lub wpisz poniżej — wystarczy jedno.",
+ "catalogLabel": "Szczepionka",
+ "identityHint": "Wybierz szczepionkę z listy lub wpisz ją poniżej. Wystarczy jedno.",
"catalogNone": "Wybierz szczepionkę",
"catalogSearch": "Szukaj szczepionek",
"catalogNoMatch": "Nie znaleziono wpisu w katalogu",
@@ -9429,7 +9780,7 @@
},
"empty": {
"title": "Nie zarejestrowano jeszcze żadnego szczepienia",
- "description": "Przepisz swoją kartę szczepień wiersz po wierszu — wystarczą data i szczepionka."
+ "description": "Przepisz swoją kartę szczepień wiersz po wierszu. Wystarczą data i szczepionka."
},
"catalog": {
"tetanus": "Tężec",
@@ -9472,10 +9823,31 @@
"overdue": "Dawka przypominająca zaległa od {date}",
"dueSoon": "Dawka przypominająca {date}",
"current": "Następna dawka przypominająca {date}"
+ },
+ "custom": {
+ "title": "Własne szczepionki",
+ "add": "Dodaj własną szczepionkę",
+ "name": "Nazwa",
+ "components": "Chroni przed",
+ "seriesDoses": "Dawki w szczepieniu podstawowym",
+ "boosterMonths": "Dawka przypominająca co (miesiące)",
+ "saved": "Szczepionka zapisana",
+ "deleted": "Szczepionka usunięta",
+ "deleteConfirm": "Usunąć tę szczepionkę? Zapisane już dawki zachowają swoją nazwę.",
+ "saveFailed": "Nie udało się zapisać szczepionki.",
+ "nameTaken": "Masz już szczepionkę o tej nazwie.",
+ "editTitle": "Edytuj własną szczepionkę",
+ "description": "Szczepionka spoza katalogu, liczona jak każdy wpis z katalogu.",
+ "componentsHint": "Wybierz wszystkie choroby, przed którymi chroni.",
+ "boosterHint": "Zostaw puste, jeśli nie ma dawki przypominającej.",
+ "protectsAgainst": "Chroni przed: {list}.",
+ "deleteTitle": "Usunąć własną szczepionkę?",
+ "sectionDescription": "Szczepionki dodane przez Ciebie, bo nie ma ich w katalogu.",
+ "catalogGroup": "Katalog"
}
},
"illness": {
- "title": "Dziennik chorób",
+ "title": "Choroba",
"subtitle": "Retrospektywny zapis Twoich epizodów chorób i dolegliwości.",
"settings": {
"viewDescription": "Wybierz, czy epizody mają być wyświetlane jako karty czy zwarta lista.",
@@ -9562,7 +9934,7 @@
},
"correlation": {
"title": "Jak przebiegało",
- "learning": "Wciąż się uczę — zapisz jeszcze kilka dni, aby zobaczyć tu wzorce.",
+ "learning": "Wciąż się uczę. Zapisz jeszcze kilka dni, aby zobaczyć tu wzorce.",
"preOnsetTitle": "Jak się zapowiadało",
"preOnsetBody": "Sygnały, które odbiegły od Twojej bazy przed oznaczonym początkiem.",
"nadirTitle": "Co się zmieniło",
@@ -9589,7 +9961,7 @@
"countOne": "W tym okresie zapisałeś 1 epizod.",
"countOther": "W tym okresie zapisałeś {count} epizodów.",
"typicalGap": "Typowa luka rekonwalescencji: {days} dni między poprawą a ustabilizowaniem wartości.",
- "typicalGapWithDriver": "Typowa luka rekonwalescencji: {days} dni – zwykle czujesz się lepiej, zanim {vital} się ustabilizuje.",
+ "typicalGapWithDriver": "Typowa luka rekonwalescencji: {days} dni. Zwykle czujesz się lepiej, zanim {vital} się ustabilizuje.",
"typicalGapLearning": "Jeszcze kilka zakończonych epizodów i pojawi się tu typowa luka rekonwalescencji.",
"none": "Brak epizodów zapisanych w tym okresie.",
"analyze": "Przeanalizuj lukę rekonwalescencji"
@@ -9614,7 +9986,7 @@
"loadError": "Nie udało się wczytać tego epizodu.",
"backToList": "Powrót do dziennika",
"detail": {
- "note": "Note"
+ "note": "Notatka"
},
"edit": {
"title": "Edytuj epizod",
@@ -9663,7 +10035,7 @@
"dismiss": "Nie teraz",
"applied": "Dodano",
"dismissed": "Odrzucono",
- "failed": "Nie udało się dodać — spróbuj ponownie",
+ "failed": "Nie udało się dodać. Spróbuj ponownie",
"checkup": {
"title": "Dodać przypomnienie o badaniu?"
},
@@ -9691,9 +10063,9 @@
"reviewPrefix": "Przegląd: {when}",
"accept": "Przyjmij plan",
"decline": "Odrzuć",
- "accepted": "Plan przyjęty — Coach będzie o nim pamiętał.",
+ "accepted": "Plan przyjęty. Coach będzie o nim pamiętał.",
"declined": "Propozycja odrzucona.",
- "failed": "Nie udało się — spróbuj ponownie.",
+ "failed": "Nie udało się. Spróbuj ponownie.",
"markMet": "Cel osiągnięty",
"abandon": "Zakończ plan",
"remove": "Usuń",
@@ -9715,10 +10087,10 @@
},
"refusal": {
"outOfScope": "Mogę pomóc wyłącznie z danymi zdrowotnymi z Twojego dziennika: pomiarami, lekami, nastrojem, trendami. Na pytania spoza tego zakresu nie odpowiadam.",
- "promptInjection": "Zauważyłem polecenia, które próbują obejść moje wytyczne. Jestem Coachem HealthLog i podsumowuję wyłącznie Twoje własne dane zdrowotne — sformułuj pytanie w tych ramach.",
+ "promptInjection": "Zauważyłem polecenia, które próbują obejść moje wytyczne. Jestem Coachem HealthLog i podsumowuję wyłącznie Twoje własne dane zdrowotne. Sformułuj pytanie w tych ramach.",
"conversationPoisoned": "Wcześniejsza wiadomość w tej rozmowie zawiera polecenia, które próbują obejść moje wytyczne. Rozpocznij nową rozmowę."
},
- "noRecordedData": "Nie masz zapisanych żadnych pomiarów dla tego, o co pytasz, więc nie mogę podać Ci żadnej liczby. Gdy coś zapiszesz, zapytaj ponownie — wtedy oprę się na Twoich własnych wartościach.",
+ "noRecordedData": "Nie masz zapisanych żadnych pomiarów dla tego, o co pytasz, więc nie mogę podać Ci żadnej liczby. Gdy coś zapiszesz, zapytaj ponownie, a wtedy oprę się na Twoich własnych wartościach.",
"step": {
"read": "Czytam: {domain}",
"readWindow": "Czytam: {domain}, {window}",
@@ -9726,9 +10098,6 @@
"showResult": "Otwieram wcześniejszą tabelę",
"reuse": "Korzystam z tabeli z poprzedniej odpowiedzi",
"headerActive": "{label}…",
- "headerDoneOne": "Sprawdzono 1 źródło",
- "headerDoneFew": "Sprawdzono {count} źródła",
- "headerDoneOther": "Sprawdzono {count} źródeł",
"listLabel": "Co sprawdził Coach",
"toggleShow": "Pokaż, co sprawdził Coach",
"toggleHide": "Ukryj, co sprawdził Coach",
@@ -9754,7 +10123,9 @@
"illness": "Choroba i powrót do zdrowia",
"cycle": "Cykl",
"correlations": "Związki między wskaźnikami",
- "snapshot": "Podsumowanie danych"
+ "snapshot": "Podsumowanie danych",
+ "environment": "Pogoda i powietrze",
+ "day": "Jeden dzień"
},
"window": {
"last7days": "ostatnie 7 dni",
@@ -9878,7 +10249,8 @@
"relatedMetric": "Sprawdź też: {metric}",
"continue": "Szukaj dalej",
"settingLabel": "Proponuj kolejne pytania",
- "settingHint": "Pokazuje do trzech propozycji pod ostatnią odpowiedzią Coacha."
+ "settingHint": "Pokazuje do trzech propozycji pod ostatnią odpowiedzią Coacha.",
+ "changeAssumption": "Zamiast tego: {value}"
},
"clarify": {
"cardLabel": "Coach ma pytanie",
@@ -9891,6 +10263,12 @@
"last90days": "Ostatnie 90 dni",
"lastYear": "Ostatnie 12 miesięcy",
"allTime": "Cały okres"
+ },
+ "comparison": {
+ "baseline90d": "Porównaj z moimi zwykłymi 90 dniami"
+ },
+ "anchor": {
+ "illness": "Od choroby z {date}"
}
},
"reuse": {
@@ -9925,7 +10303,7 @@
},
"mentalHealth": {
"pageTitle": "Dobrostan psychiczny",
- "pageDescription": "Cztery zwalidowane, dobrowolne testy — liczy się przebieg w czasie, nie pojedynczy wynik.",
+ "pageDescription": "Cztery zwalidowane, dobrowolne testy: liczy się przebieg w czasie, nie pojedynczy wynik.",
"attributionLabel": "Źródło",
"attributionShow": "Źródło i licencja: {instrument}",
"choosePrompt": "Wybierz sprawdzenie",
@@ -9936,10 +10314,10 @@
"saved": "Zapisano",
"error": "Coś poszło nie tak. Spróbuj ponownie.",
"instrument": {
- "phq9": "PHQ-9 · nastrój",
- "gad7": "GAD-7 · lęk",
- "who5": "WHO-5 · samopoczucie",
- "sci": "SCI · sen"
+ "phq9": "PHQ-9 (nastrój)",
+ "gad7": "GAD-7 (lęk)",
+ "who5": "WHO-5 (samopoczucie)",
+ "sci": "SCI (sen)"
},
"instrumentSub": {
"phq9": "Przesiew depresji, 9 pytań",
@@ -9960,7 +10338,7 @@
"2": "Bardzo utrudniły",
"3": "Skrajnie utrudniły"
},
- "functionalOptionalHint": "Opcjonalne — możesz wysłać bez odpowiedzi na to pytanie.",
+ "functionalOptionalHint": "Opcjonalne: możesz wysłać bez odpowiedzi na to pytanie.",
"result": {
"title": "Twój wynik",
"totalLabel": "Wynik łączny",
@@ -10150,7 +10528,7 @@
},
"openDetail": "Pokaż szczegóły",
"historyLoadError": "Nie udało się wczytać historii testów",
- "historyLoadErrorHint": "To nie znaczy, że nie masz wyników — nie udało się pobrać listy. Spróbuj ponownie.",
+ "historyLoadErrorHint": "To nie znaczy, że nie masz wyników; nie udało się pobrać listy. Spróbuj ponownie.",
"privacyNote": "Twoje odpowiedzi są przechowywane w postaci zaszyfrowanej i nigdy nie trafiają do Coacha AI."
},
"records": {
@@ -10380,12 +10758,34 @@
"daylight": "Światło dzienne",
"precip": "Opady",
"pressureMean": "Ciśnienie atmosferyczne",
- "pressureDelta": "Wahanie ciśnienia"
+ "pressureDelta": "Wahanie ciśnienia",
+ "pm25": "Pył drobny (PM2,5)",
+ "pm10": "Pył (PM10)",
+ "no2": "Dwutlenek azotu",
+ "ozone8h": "Ozon (maksimum 8-godzinne)",
+ "europeanAqi": "Europejski indeks jakości powietrza",
+ "uvIndexMax": "Indeks UV (maksimum)",
+ "dustMax": "Pył saharyjski",
+ "pollenMax": "Pyłki (najwyższa wartość)"
+ },
+ "chip": {
+ "day": "Pogoda {date}",
+ "pollenHigh": "Dużo pyłków: {kinds}",
+ "hotNight": "Ciepła noc",
+ "veryPoorAir": "Bardzo złe powietrze"
+ },
+ "pollen": {
+ "alder": "olcha",
+ "birch": "brzoza",
+ "grass": "trawy",
+ "mugwort": "bylica",
+ "olive": "oliwka",
+ "ragweed": "ambrozja"
}
},
"documents": {
"title": "Dokumenty",
- "subtitle": "Każde pismo, wynik i skan w jednym przeszukiwalnym miejscu — zaszyfrowane w spoczynku.",
+ "subtitle": "Każde pismo, wynik i skan w jednym przeszukiwalnym miejscu, zaszyfrowane w spoczynku.",
"pageUpload": "Prześlij",
"ownerOnlyUpload": "Dokumenty może dodawać tylko właściciel kartoteki.",
"kind": {
@@ -10405,11 +10805,13 @@
"searchPlaceholder": "Szukaj tytułu lub nazwy pliku…",
"typeGroup": "Typ dokumentu",
"conditionGroup": "Schorzenie",
+ "procedureGroup": "Zabiegi i operacje",
"yearGroup": "Rok",
"clear": "Wyczyść filtry",
"typeAll": "Wszystkie typy",
"typeCount": "{count} typy",
"conditionAll": "Wszystkie schorzenia",
+ "procedureAll": "Wszystkie zabiegi",
"yearAll": "Wszystkie lata"
},
"card": {
@@ -10425,15 +10827,15 @@
},
"error": {
"fileTooLarge": "Ten plik przekracza limit {maxSize}.",
- "quotaExceeded": "Miejsce jest pełne — użyto {used} z {quota}.",
+ "quotaExceeded": "Miejsce jest pełne: użyto {used} z {quota}.",
"unsupportedType": "Ten typ pliku nie jest obsługiwany.",
"purged": "Ten dokument został trwale usunięty i nie można go przywrócić.",
"duplicateExists": "Identyczny dokument jest już zapisany.",
- "rateLimited": "Zbyt wiele przesyłań — spróbuj ponownie za kilka minut.",
+ "rateLimited": "Zbyt wiele przesyłań. Spróbuj ponownie za kilka minut.",
"generic": "Przesyłanie nie powiodło się. Spróbuj ponownie."
},
"toast": {
- "duplicate": "Już zapisano — istniejący dokument został wyróżniony.",
+ "duplicate": "Już zapisano. Istniejący dokument został wyróżniony.",
"deleted": "Dokument usunięty",
"restoreFailed": "Nie udało się przywrócić dokumentu."
},
@@ -10456,7 +10858,7 @@
},
"empty": {
"title": "Twój sejf dokumentów jest pusty",
- "description": "Przechowuj pisma lekarskie, wyniki laboratoryjne, obrazowanie, recepty i zaświadczenia o szczepieniach — zaszyfrowane i przeszukiwalne według typu, schorzenia i roku.",
+ "description": "Przechowuj pisma lekarskie, wyniki laboratoryjne, obrazowanie, recepty i zaświadczenia o szczepieniach. Są zaszyfrowane i można je przeszukiwać według typu, schorzenia i roku.",
"action": "Prześlij pierwszy dokument",
"noMatchesTitle": "Brak pasujących dokumentów",
"noMatchesDescription": "Nic nie pasuje do bieżącego wyszukiwania i filtrów."
@@ -10487,6 +10889,7 @@
"saveError": "Nie udało się zapisać zmiany.",
"deleteError": "Nie udało się usunąć dokumentu.",
"download": "Pobierz",
+ "openInNewTab": "Otwórz w nowej karcie",
"delete": "Usuń",
"titleLabel": "Tytuł",
"titlePlaceholder": "Dodaj tytuł",
@@ -10496,9 +10899,9 @@
"linkCondition": "Powiąż",
"noConditions": "Niepowiązany ze schorzeniem",
"previewUnavailable": "Brak podglądu dla tego typu pliku",
- "uploadedMeta": "Dodano {date} · {size}",
+ "uploadedMeta": "Dodano {date}, {size}",
"importedFrom": "Zaimportowano z: {system}",
- "importedFromWithId": "Zaimportowano z: {system} · ID {id}",
+ "importedFromWithId": "Zaimportowano z: {system}, ID {id}",
"sourceOther": "inny system",
"share": "Udostępnij",
"labValuesLink": "Zobacz {count} wyników laboratoryjnych",
@@ -10598,7 +11001,7 @@
"CONDITION": "Schorzenie",
"MEDICATION_STATEMENT": "Lek"
},
- "statedRange": "podany zakres {range}",
+ "statedRange": "Podany zakres {range}",
"needsCheck": "Sprawdź tę wartość z dokumentem, a potem ją zapisz.",
"verify": "Sprawdź",
"verifySave": "Zapisz wartość",
@@ -10714,6 +11117,21 @@
"finishedHint": "Wyszukaj ponownie lub zmień filtr, aby wybrać więcej dokumentów.",
"maxReached": "Możesz zaimportować naraz najwyżej {max} dokumentów.",
"noMatchHint": "Spróbuj części nazwy albo wyczyść tag i daty."
+ },
+ "customize": "Dostosuj widok",
+ "layout": {
+ "viewDescription": "Karty z miniaturą albo zwarta lista.",
+ "arrangementHeading": "Układ",
+ "arrangementDescription": "Każdy miesiąc osobno albo wszystko po kolei na całą szerokość.",
+ "stacked": "Miesiące jeden pod drugim",
+ "flow": "Ciągły",
+ "saveFailed": "Nie udało się zapisać widoku."
+ },
+ "deleteConfirm": {
+ "titleOne": "Usunąć ten dokument?",
+ "titleMany": "Usunąć dokumenty ({count})?",
+ "body": "Zaraz potem możesz to cofnąć. Po 30 dniach zostanie usunięte na stałe.",
+ "action": "Usuń"
}
},
"selfDescription": {
@@ -10800,7 +11218,7 @@
"genericDescription": "Pomiar, który śledzisz. Dodawaj wartości, aby obserwować go w czasie.",
"emptyTitle": "Brak wartości",
"emptyDescription": "Dodaj pierwszą wartość, aby zacząć przedstawiać tę metrykę na wykresie.",
- "valuesTitle": "{metric} — wszystkie wartości",
+ "valuesTitle": "{metric}: wszystkie wartości",
"valuesDescription": "Wszystkie zapisane wartości tej metryki.",
"backToMetric": "Powrót do {metric}"
}
@@ -10847,35 +11265,41 @@
"lastAttemptReason": {
"unitMismatch": "Otrzymane wartości nie pasowały do jednostek, których ta aplikacja oczekuje dla tych składników odżywczych, więc nie udało się ich zapisać.",
"valueOutOfRange": "Otrzymane ilości były wyższe, niż ta aplikacja może wiarygodnie zapisać, więc nie udało się ich zapisać.",
- "dayInvalid": "Wpisy zawierały datę, której serwer nie mógł zaakceptować. Może się to zdarzyć, gdy zegar lub strefa czasowa po stronie wysyłającej są nieprawidłowe — warto to sprawdzić.",
+ "dayInvalid": "Wpisy zawierały datę, której serwer nie mógł zaakceptować. Może się to zdarzyć, gdy zegar lub strefa czasowa po stronie wysyłającej są nieprawidłowe, więc warto to sprawdzić.",
"upsertFailed": "Te wpisy dotarły do serwera, ale problem po naszej stronie uniemożliwił ich zapisanie. Nie musisz nic robić.",
"unknown": "Te wpisy dotarły, ale nie udało się ich zapisać."
}
},
"hydration": {
- "referenceMeta": "Zalecane spożycie {value} {unit}/dzień · EFSA"
+ "referenceMeta": "Zalecane spożycie {value} {unit}/dzień (EFSA)"
},
"caffeine": {
- "ceilingMeta": "Bezpieczny limit {value} {unit}/dzień · EFSA"
+ "ceilingMeta": "Bezpieczny limit {value} {unit}/dzień (EFSA)"
},
"micronutrients": {
"title": "Witaminy i minerały",
- "rowMeta": "{days} z {window} dni · zalecane {value} {unit} (EFSA)",
+ "rowMeta": "{days} z {window} dni, zalecane {value} {unit} (EFSA)",
"rowMetaNoRef": "{days} z {window} dni",
"footer": "{tracked} z {total} śledzonych mikroskładników ma dane w tym okresie.",
"emptyTitle": "Brak jeszcze zsynchronizowanych danych o mikroskładnikach",
"loadError": "Nie udało się wczytać danych o składnikach odżywczych"
},
- "sparseNote": "Niska wartość zwykle oznacza tu rzadkie zapisywanie, a nie niedobór — dzienne sumy z Apple Health odzwierciedlają to, co zapisały twoje aplikacje, a nie całkowite spożycie w diecie."
+ "sparseNote": "Niska wartość zwykle oznacza tu rzadkie zapisywanie, a nie niedobór. Dzienne sumy z Apple Health odzwierciedlają to, co zapisały twoje aplikacje, a nie całkowite spożycie w diecie."
},
"daily": {
"line": {
- "score": "Twój dzisiejszy wynik zdrowia to {score}.",
- "allClear": "Dziś nic nie wymaga Twojej uwagi."
+ "allClear": "Dziś nic nie wymaga Twojej uwagi.",
+ "scoreReady": "Twój dzisiejszy wynik zdrowia jest gotowy."
},
"push": {
"title": "Twoje poranne podsumowanie",
- "bodyProvisional": "{line} · Brakuje jeszcze snu z zeszłej nocy."
+ "bodyProvisional": "{line}\nBrakuje jeszcze snu z zeszłej nocy.",
+ "titleScore": "Poranny wynik {score}",
+ "band": {
+ "green": "Dziś wszystko wygląda stabilnie.",
+ "yellow": "Dziś warto przyjrzeć się kilku rzeczom.",
+ "red": "Dziś niektóre obszary wymagają uwagi."
+ }
},
"item": {
"doseWindow": {
@@ -10893,27 +11317,27 @@
},
"syncIssue": {
"title": "Synchronizacja wymaga uwagi",
- "body": "{integration} się nie synchronizuje — połącz ponownie, aby dane były aktualne."
+ "body": "{integration} się nie synchronizuje. Połącz ponownie, aby dane były aktualne."
},
"coachCheckin": {
"title": "Czas przyjrzeć się planowi",
- "body": "Minął mniej więcej tydzień, odkąd ustaliłeś ten plan — zachowaj go, dostosuj lub odpuść. Bez presji.",
- "bodyPlan": "Około tygodnia temu postanowiłeś: „{plan}”. Zachowaj to, dostosuj lub odpuść — bez presji.",
+ "body": "Minął mniej więcej tydzień, odkąd ustaliłeś ten plan. Zachowaj go, dostosuj lub odpuść. Bez presji.",
+ "bodyPlan": "Około tygodnia temu postanowiłeś: „{plan}”. Zachowaj to, dostosuj lub odpuść, bez presji.",
"adjustPrompt": "Chciałbym dostosować mój plan kontrolny.",
"adjustPromptPlan": "Chciałbym dostosować ten plan: „{plan}”"
},
"tensionWindow": {
"title": "Może chwila napięcia dzisiaj",
"body": {
- "morning": "Twoje tętno przez dłuższą chwilę rano utrzymywało się powyżej wartości spoczynkowej, gdy byłeś w bezruchu — możliwe napięcie.",
- "afternoon": "Twoje tętno przez dłuższą chwilę po południu utrzymywało się powyżej wartości spoczynkowej, gdy byłeś w bezruchu — możliwe napięcie.",
- "evening": "Twoje tętno przez dłuższą chwilę wieczorem utrzymywało się powyżej wartości spoczynkowej, gdy byłeś w bezruchu — możliwe napięcie.",
- "night": "Twoje tętno przez dłuższą chwilę w nocy utrzymywało się powyżej wartości spoczynkowej, gdy byłeś w bezruchu — możliwe napięcie."
+ "morning": "Twoje tętno przez dłuższą chwilę rano utrzymywało się powyżej wartości spoczynkowej, gdy byłeś w bezruchu. Możliwe napięcie.",
+ "afternoon": "Twoje tętno przez dłuższą chwilę po południu utrzymywało się powyżej wartości spoczynkowej, gdy byłeś w bezruchu. Możliwe napięcie.",
+ "evening": "Twoje tętno przez dłuższą chwilę wieczorem utrzymywało się powyżej wartości spoczynkowej, gdy byłeś w bezruchu. Możliwe napięcie.",
+ "night": "Twoje tętno przez dłuższą chwilę w nocy utrzymywało się powyżej wartości spoczynkowej, gdy byłeś w bezruchu. Możliwe napięcie."
}
},
"ecgNewRecording": {
"title": "Nowy zapis EKG",
- "body": "Twoje urządzenie zarejestrowało nowe EKG — gotowe do podglądu.",
+ "body": "Twoje urządzenie zarejestrowało nowe EKG, gotowe do podglądu.",
"bodyVerdict": "Twoje urządzenie zarejestrowało nowe EKG i zgłosiło: {verdict}.",
"verdict": {
"irregular": "możliwy nieregularny rytm",
@@ -10933,9 +11357,9 @@
},
"upcomingVisit": {
"title": "Zbliża się wizyta",
- "bodyToday": "{what} — dzisiaj",
- "bodyTomorrow": "{what} — jutro",
- "bodyDayAfterTomorrow": "{what} — pojutrze"
+ "bodyToday": "{what}, dzisiaj",
+ "bodyTomorrow": "{what}, jutro",
+ "bodyDayAfterTomorrow": "{what}, pojutrze"
}
},
"action": {
@@ -10954,10 +11378,34 @@
"scoreLabel": "Wynik zdrowia",
"deltaVsBaseline": "{delta} wzgl. Twojej wartości bazowej",
"deltaFlat": "Zgodnie z Twoją wartością bazową",
- "sleepPending": "Sen z ostatniej nocy jeszcze nie dotarł — dzisiejszy odczyt jest wstępny.",
+ "sleepPending": "Sen z ostatniej nocy jeszcze nie dotarł, więc dzisiejszy odczyt jest wstępny.",
"worthALook": "Warto sprawdzić",
"allClear": "Dziś nic nie wymaga Twojej uwagi.",
- "ringLink": "Otwórz szczegóły: {metric}"
+ "ringLink": "Otwórz szczegóły: {metric}",
+ "heading": "Dziś",
+ "steadyWeeksOne": "Stabilnie od {count} tygodnia",
+ "steadyWeeksFew": "Stabilnie od {count} tygodni",
+ "steadyWeeksOther": "Stabilnie od {count} tygodni",
+ "valueWithUnit": "{value} {unit}",
+ "rangeWithUnit": "{low} do {high} {unit}",
+ "rangeAtThisTime": "{range} o tej porze dnia",
+ "vital": {
+ "RESTING_HEART_RATE": "Tętno spoczynkowe",
+ "HEART_RATE_VARIABILITY": "Zmienność rytmu serca",
+ "RESPIRATORY_RATE": "Częstość oddechów",
+ "OXYGEN_SATURATION": "Saturacja",
+ "BODY_TEMPERATURE": "Temperatura ciała",
+ "SKIN_TEMPERATURE": "Temperatura skóry",
+ "BLOOD_PRESSURE_SYS": "Ciśnienie skurczowe",
+ "BLOOD_PRESSURE_DIA": "Ciśnienie rozkurczowe",
+ "BLOOD_GLUCOSE": "Glukoza we krwi",
+ "PULSE": "Tętno",
+ "WEIGHT": "Waga",
+ "generic": "Parametr życiowy"
+ },
+ "steadyAtLeastWeeksOne": "Stabilnie od co najmniej {count} tygodnia",
+ "steadyAtLeastWeeksFew": "Stabilnie od co najmniej {count} tygodni",
+ "steadyAtLeastWeeksOther": "Stabilnie od co najmniej {count} tygodni"
},
"milestone": {
"metric": {
@@ -10993,6 +11441,68 @@
"body": "Cały miesiąc z {metric} w zwykłym zakresie. Wspaniale stabilnie."
}
}
+ },
+ "lead": {
+ "vitalOutside": {
+ "above": "{metric}: {value}, powyżej Twojego zwykłego zakresu {range}.",
+ "below": "{metric}: {value}, poniżej Twojego zwykłego zakresu {range}."
+ },
+ "vitalsOutside": {
+ "aboveOne": "{metric}: {value}, powyżej Twojego zwykłego zakresu {range}. Poza zakresem jest też {count} inny parametr.",
+ "aboveFew": "{metric}: {value}, powyżej Twojego zwykłego zakresu {range}. Poza zakresem są też {count} inne parametry.",
+ "aboveOther": "{metric}: {value}, powyżej Twojego zwykłego zakresu {range}. Poza zakresem jest też {count} innych parametrów.",
+ "belowOne": "{metric}: {value}, poniżej Twojego zwykłego zakresu {range}. Poza zakresem jest też {count} inny parametr.",
+ "belowFew": "{metric}: {value}, poniżej Twojego zwykłego zakresu {range}. Poza zakresem są też {count} inne parametry.",
+ "belowOther": "{metric}: {value}, poniżej Twojego zwykłego zakresu {range}. Poza zakresem jest też {count} innych parametrów."
+ },
+ "sleep": {
+ "more": "Ostatnia noc: {duration} snu, o {diff} dłużej niż zwykle.",
+ "less": "Ostatnia noc: {duration} snu, o {diff} krócej niż zwykle.",
+ "usual": "Ostatnia noc: {duration} snu, mniej więcej tyle co zwykle.",
+ "plain": "Ostatnia noc: {duration} snu."
+ },
+ "vitalsInRangeOne": "Twój ostatni parametr życiowy mieści się w zwykłym zakresie.",
+ "vitalsInRangeFew": "Wszystkie {count} ostatnie parametry życiowe mieszczą się w zwykłym zakresie.",
+ "vitalsInRangeOther": "Wszystkie {count} ostatnich parametrów życiowych mieści się w zwykłym zakresie.",
+ "vitalInRange": "{metric}: {value}, w Twoim zwykłym zakresie {range}."
+ },
+ "todayFact": {
+ "restMode": {
+ "label": "Tryb odpoczynku",
+ "value": "Dzień {day}"
+ },
+ "medications": {
+ "label": "Leki",
+ "value": "Przyjęto {taken} z {scheduled}"
+ },
+ "appointment": {
+ "label": "Wizyta",
+ "today": "Dziś {time}, {what}",
+ "tomorrow": "Jutro {time}, {what}"
+ },
+ "sleep": {
+ "label": "Ostatnia noc",
+ "plain": "{duration}",
+ "usual": "{duration}, jak zwykle",
+ "more": "{duration}, o {diff} dłużej niż zwykle",
+ "less": "{duration}, o {diff} krócej niż zwykle"
+ },
+ "vitals": {
+ "label": "Parametry życiowe",
+ "allInOne": "Sprawdzono {count}, w zakresie",
+ "allInFew": "Sprawdzono {count}, wszystkie w zakresie",
+ "allInOther": "Sprawdzono {count}, wszystkie w zakresie",
+ "oneOut": {
+ "above": "{metric} powyżej zakresu",
+ "below": "{metric} poniżej zakresu"
+ },
+ "someOut": "{count} z {total} poza zakresem"
+ },
+ "cycle": {
+ "label": "Cykl",
+ "value": "{phase}, dzień {day}",
+ "dayOnly": "Dzień {day}"
+ }
}
},
"reportSelection": {
@@ -11122,7 +11632,7 @@
"errorScope": "Tych sekcji nie da się udostępnić w ten sposób. Wybierz co najmniej jedną sekcję albo udostępnij całą kartotekę.",
"errorManageBrowserOnly": "Dostęp zarządzający można przyznać tylko w przeglądarce.",
"errorStepUp": "Potwierdź drugi składnik logowania i spróbuj ponownie.",
- "errorOffline": "To nie dotarło do serwera. Twoje zaproszenie nadal tu jest — sprawdź połączenie i spróbuj ponownie."
+ "errorOffline": "To nie dotarło do serwera. Twoje zaproszenie nadal tu jest. Sprawdź połączenie i spróbuj ponownie."
},
"given": {
"title": "Kto może otworzyć moją kartę",
@@ -11177,7 +11687,7 @@
"managed": {
"title": "Profile, którymi się opiekujesz",
"description": "Karty zdrowia bez własnego logowania.",
- "explainer": "Zarządzany profil to karta zdrowia osoby, która sama się nie loguje — dziecka albo kogoś, kim się opiekujesz. Nie ma logowania ani adresu e-mail, a Ty stajesz się pierwszą osobą opiekującą się profilem. Później możesz dodać kolejne, a każda z was może wejść do karty i ją uzupełniać.",
+ "explainer": "Zarządzany profil to karta zdrowia osoby, która sama się nie loguje, na przykład dziecka albo kogoś, kim się opiekujesz. Nie ma logowania ani adresu e-mail, a Ty stajesz się pierwszą osobą opiekującą się profilem. Później możesz dodać kolejne, a każda z was może wejść do karty i ją uzupełniać.",
"nameLabel": "Nazwa",
"namePlaceholder": "Nazwa, po której rozpoznasz tę kartę",
"nameHint": "Do 80 znaków. Wyświetla się wszędzie tam, gdzie pojawia się ta karta.",
@@ -11199,7 +11709,7 @@
"creating": "Tworzenie…",
"created": "{name} jest gotowy. Do tej karty przejdziesz z menu konta.",
"errorStepUp": "Potwierdź drugi składnik logowania i spróbuj ponownie.",
- "errorOffline": "To nie dotarło do serwera. Nic się nie zmieniło — sprawdź połączenie i spróbuj ponownie.",
+ "errorOffline": "To nie dotarło do serwera. Nic się nie zmieniło. Sprawdź połączenie i spróbuj ponownie.",
"errorInvalid": "Serwer odrzucił te dane. Sprawdź nazwę, datę i strefę czasową.",
"errorFailed": "Nie udało się utworzyć profilu.",
"edit": "Edytuj",
@@ -11210,7 +11720,7 @@
"editErrorFailed": "Nie udało się zapisać zmian.",
"delete": "Usuń profil",
"deleteTitle": "Usunąć {name}?",
- "deleteBody": "Wszystko, co zapisano dla {name}, znika razem z profilem — pomiary, leki, dokumenty i notatki. Tego nie da się cofnąć.",
+ "deleteBody": "Wszystko, co zapisano dla {name}, znika razem z profilem: pomiary, leki, dokumenty i notatki. Tego nie da się cofnąć.",
"deleteGuardiansOne": "{count} osoba opiekuje się dziś tą kartą i ją straci.",
"deleteGuardiansFew": "{count} osoby opiekują się dziś tą kartą i ją stracą.",
"deleteGuardiansOther": "{count} osób opiekuje się dziś tą kartą i ją straci.",
@@ -11239,7 +11749,47 @@
"guardianErrorFailed": "Nie udało się wysłać zaproszenia.",
"errorMfaRequired": "To wymaga drugiego składnika logowania, a Twoje konto jeszcze go nie ma. Skonfiguruj go w Ustawieniach → Bezpieczeństwo i spróbuj ponownie.",
"rosterLoadError": "Nie udało się wczytać, kto opiekuje się tą kartą.",
- "errorRateLimit": "Zbyt wiele profili utworzonych w ostatniej godzinie. Odczekaj chwilę i spróbuj ponownie."
+ "errorRateLimit": "Zbyt wiele profili utworzonych w ostatniej godzinie. Odczekaj chwilę i spróbuj ponownie.",
+ "handover": {
+ "action": "Przekaż",
+ "title": "Przekaż osobie {name}",
+ "description": "Daj osobie {name} własne logowanie. Nic nie jest kopiowane ani przenoszone: karta staje się jej kontem.",
+ "guardiansLabel": "Dostęp po przekazaniu",
+ "guardiansHint": "Twoja propozycja. {name} zobaczy ją i zdecyduje po zalogowaniu.",
+ "access": {
+ "end": "Brak dostępu",
+ "read": "Tylko podgląd",
+ "manage": "Zarządzanie"
+ },
+ "expiryLabel": "Link ważny przez",
+ "expiry1": "1 dzień",
+ "expiry7": "7 dni",
+ "expiry14": "14 dni",
+ "minorHint": "{name} ma mniej niż 16 lat. Warto zachować dostęp do podglądu i omówić ten krok wspólnie.",
+ "irreversible": "Gdy {name} użyje linku, przekazania nie da się cofnąć. Karta będzie należeć do tej osoby i to ona zdecyduje, kto zachowa dostęp.",
+ "afterwards": "{name} osobiście potwierdzi zastrzeżenie i każdą zgodę na AI. Przypomnienia dotyczące tej karty przestaną do Ciebie trafiać.",
+ "replaceNote": "Utworzenie nowego linku wycofuje otwarty.",
+ "create": "Utwórz link",
+ "creating": "Tworzenie…",
+ "linkOnce": "Ten link jest widoczny tylko teraz. Przekaż go osobie {name} osobiście lub zaufanym kanałem albo pozwól zeskanować kod.",
+ "qrAlt": "Kod QR linku przekazania dla {name}",
+ "urlLabel": "Link przekazania",
+ "copy": "Kopiuj",
+ "copied": "Link skopiowany",
+ "copyError": "Nie udało się skopiować linku. Zaznacz go i skopiuj ręcznie.",
+ "linkExpires": "Ważny do {date}.",
+ "done": "Gotowe",
+ "pending": "Link przekazania jest otwarty do {date}.",
+ "replace": "Nowy link",
+ "revoke": "Wycofaj link",
+ "revoked": "Link wycofany",
+ "revokeFailed": "Nie udało się wycofać linku. Spróbuj ponownie.",
+ "errorFailed": "Nie udało się utworzyć linku przekazania.",
+ "errorRosterChanged": "W międzyczasie zmieniło się, kto opiekuje się kartą. Zamknij to i otwórz ponownie.",
+ "oidcOnly": "Przekazanie profilu nie jest możliwe, dopóki ten serwer pozwala tylko na logowanie jednokrotne (SSO).",
+ "loadError": "Nie udało się wczytać stanu przekazania.",
+ "entryHint": "Gdy {name} chce samodzielnie prowadzić tę kartę."
+ }
},
"toast": {
"savedTo": "Zapisano w karcie użytkownika {name}"
@@ -11358,7 +11908,13 @@
"symptomEventDelete": "{name} usunął(-ęła) wpis objawu",
"symptomDefinitionCreate": "{name} dodał(a) objaw",
"symptomDefinitionUpdate": "{name} zmienił(a) objaw",
- "symptomDefinitionDelete": "{name} usunął(-ęła) objaw"
+ "symptomDefinitionDelete": "{name} usunął(-ęła) objaw",
+ "customVaccineCreate": "{name} dodał własną szczepionkę",
+ "customVaccineUpdate": "{name} zmienił własną szczepionkę",
+ "customVaccineDelete": "{name} usunął własną szczepionkę",
+ "lifeEventCreate": "{name} dodał wydarzenie życiowe",
+ "lifeEventUpdate": "{name} zmienił wydarzenie życiowe",
+ "lifeEventDelete": "{name} usunął wydarzenie życiowe"
},
"section": {
"measurements": {
@@ -11396,7 +11952,7 @@
},
"actionError": {
"retry": "Spróbuj ponownie",
- "offline": "To nie dotarło do serwera. Nic się nie zmieniło — sprawdź połączenie i spróbuj ponownie.",
+ "offline": "To nie dotarło do serwera. Nic się nie zmieniło. Sprawdź połączenie i spróbuj ponownie.",
"rateLimited": "Zbyt wiele prób pod rząd. Odczekaj minutę i spróbuj ponownie.",
"acceptExpired": "To zaproszenie wygasło. Poproś o nowe.",
"acceptWithdrawn": "To zaproszenie zostało wycofane.",
@@ -11408,12 +11964,28 @@
"notFound": "Tego dostępu już nie ma.",
"revokeFailed": "Nie udało się zakończyć dostępu.",
"renounceFailed": "Nie udało się zwrócić dostępu.",
- "profileNotFound": "Tego profilu już nie ma — mógł zostać usunięty albo Twój dostęp do niego się zakończył.",
+ "profileNotFound": "Tego profilu już nie ma. Mógł zostać usunięty albo Twój dostęp do niego się zakończył.",
"deleteProfileFailed": "Nie udało się usunąć profilu.",
"removeGuardianFailed": "Nie udało się zakończyć tego dostępu."
},
"anamnesis": {
"relativeNote": "Wpis w wywiadzie rodzinnym opisuje bliskich tej osoby. Dodaj go tylko wtedy, gdy chciałaby, żeby został tu zapisany."
+ },
+ "handoverDecision": {
+ "title": "Kto zachowuje dostęp do Twojej karty",
+ "description": "Zdecyduj, co od teraz mogą osoby, które prowadziły Twoją kartę.",
+ "proposed": "Proponowane: {access}",
+ "settled": "Już zmienione w Udostępnionym dostępie",
+ "empty": "Nikt inny nie miał dostępu do Twojej karty.",
+ "later": "Możesz to zmienić w każdej chwili w Ustawieniach, Udostępniony dostęp.",
+ "save": "Potwierdź",
+ "saving": "Zapisywanie…",
+ "decideLater": "Później",
+ "errorFailed": "Nie udało się zapisać decyzji. Spróbuj ponownie.",
+ "loadError": "Nie udało się wczytać osób, które prowadziły Twoją kartę.",
+ "notificationsTitle": "Ustaw własne przypomnienia",
+ "notificationsBody": "Do tej pory przypomnienia dla tej karty trafiały do osób, które ją prowadziły. Od teraz trafiają do Ciebie, gdy tylko dodasz kanał powiadomień.",
+ "notificationsAction": "Otwórz powiadomienia"
}
},
"encounters": {
@@ -11434,7 +12006,7 @@
"both": "Obie strony",
"none": "Nie podano"
},
- "bodySiteWithSide": "{site} · {side}",
+ "bodySiteWithSide": "{site} ({side})",
"sectionTitle": "Wizyty",
"sectionDescription": "Wizyty u lekarza i umówione terminy w jednym miejscu.",
"add": "Wizyta",
@@ -11449,7 +12021,7 @@
"created": "Wizyta zapisana",
"updated": "Wizyta zaktualizowana",
"saveFailed": "Nie udało się zapisać wizyty.",
- "checkupNotClosed": "Wizyta zapisana. Badanie pozostaje otwarte — twój dostęp tego nie obejmuje.",
+ "checkupNotClosed": "Wizyta zapisana. Badanie pozostaje otwarte, bo twój dostęp tego nie obejmuje.",
"status": {
"planned": "Zaplanowana",
"done": "Odbyta",
@@ -11466,7 +12038,7 @@
"dateDerivesStatusHint": "Data w przyszłości oznacza umówiony termin.",
"status": "Status",
"practitioner": "Gabinet lub lekarz",
- "practitionerHint": "Opcjonalne — nazwę możesz dodać później.",
+ "practitionerHint": "Opcjonalne: nazwę możesz dodać później.",
"practitionerNone": "Nie podano",
"practitionerSearch": "Szukaj nazwiska lub gabinetu",
"practitionerNoMatch": "Nie znaleziono wpisu.",
@@ -11524,9 +12096,8 @@
},
"practitioners": {
"title": "Gabinety i lekarze",
- "description": "Twoja własna książka adresowa — wpisz gabinet raz, wybieraj go wszędzie.",
+ "description": "Twoja własna książka adresowa: wpisz gabinet raz, wybieraj go wszędzie.",
"linkLabel": "Książka adresowa",
- "add": "Wpis",
"searchPlaceholder": "Szukaj nazwiska, gabinetu lub specjalizacji",
"loadError": "Nie udało się wczytać książki adresowej.",
"emptyTitle": "Brak wpisów",
@@ -11623,7 +12194,6 @@
"unreadableLabel": "Nieczytelny objaw",
"intensityPill": "{value}/10",
"entry": {
- "sheetTitle": "Zapisz objaw",
"sheetDescription": "Jeden z twoich objawów, jak silny i kiedy.",
"symptom": "Objaw",
"symptomRequired": "Wybierz objaw.",
@@ -11672,5 +12242,426 @@
"title": "Zapisane objawy",
"filed": "zapisano przy tej chorobie"
}
+ },
+ "day": {
+ "scoreOfMax": "z {max}",
+ "cycleDay": "Dzień cyklu {n}",
+ "cyclePhaseLine": {
+ "MENSTRUAL": "Dzień cyklu {n}, faza menstruacyjna",
+ "FOLLICULAR": "Dzień cyklu {n}, faza folikularna",
+ "OVULATORY": "Dzień cyklu {n}, faza owulacyjna",
+ "LUTEAL": "Dzień cyklu {n}, faza lutealna"
+ },
+ "previousDay": "Poprzedni dzień",
+ "nextDay": "Następny dzień",
+ "close": "Zamknij dzień",
+ "hidePanel": "Ukryj dzień",
+ "showPanel": "Pokaż {date}",
+ "openDay": "Otwórz dzień",
+ "viewWholeDay": "Zobacz cały dzień",
+ "captureForDay": "Dodaj dla tego dnia",
+ "empty": "Brak wpisów dla tego dnia.",
+ "notShared": "Niektóre obszary nie są Ci udostępnione.",
+ "inTimeline": "Na osi czasu",
+ "askCoach": "Zapytaj Coacha o ten dzień",
+ "loadFailed": "Nie udało się wczytać tego dnia.",
+ "dayOf": "Dzień {n} z {m}",
+ "dayN": "Dzień {n}",
+ "usualRange": "Zwykle od {lo} do {hi}",
+ "groups": {
+ "scores": "Wyniki",
+ "running": "W toku tego dnia",
+ "values": "Wartości",
+ "happened": "Co się działo"
+ },
+ "notable": {
+ "extremeHigh": "Najwyższa wartość dzienna od {since}",
+ "extremeLow": "Najniższa wartość dzienna od {since}",
+ "firstValue": "Pierwszy pomiar tego rodzaju",
+ "gap": "{days} dni bez pomiaru"
+ },
+ "previousDayShortcut": "Poprzedni dzień (Alt + strzałka w lewo)",
+ "nextDayShortcut": "Następny dzień (Alt + strzałka w prawo)",
+ "captureShort": "Zapisz",
+ "inTimelineShort": "Oś czasu",
+ "askCoachPrompt": "Co się działo {date}?",
+ "coachChips": "Dni w tej odpowiedzi",
+ "description": "Wszystko, co Twoje wpisy zawierają dla tego dnia.",
+ "openDocument": "Otwórz dokument",
+ "previousReading": "Wcześniej, {date}",
+ "sheetExpand": "Rozwiń na pełną wysokość",
+ "sheetShrink": "Zmniejsz",
+ "valuesLegend": "Punkt to ten dzień, pasek to 30 dni wcześniej",
+ "valuesAllOne": "Wszystkie wartości",
+ "valuesAllFew": "Wszystkie {count} wartości",
+ "valuesAllOther": "Wszystkie {count} wartości",
+ "valuesFewer": "Mniej wartości",
+ "countEntriesOne": "1 wpis",
+ "countEntriesFew": "{count} wpisy",
+ "countEntriesOther": "{count} wpisów",
+ "meanOfReadingsOne": "Średnia z 1 pomiaru",
+ "meanOfReadingsFew": "Średnia z {count} pomiarów",
+ "meanOfReadingsOther": "Średnia z {count} pomiarów",
+ "sinceLastVisit": {
+ "title": "Od ostatniej wizyty {date}",
+ "countOne": "1 wpis",
+ "countFew": "{count} wpisy",
+ "countOther": "{count} wpisów",
+ "allOne": "Pokaż wszystko",
+ "allFew": "Pokaż wszystkie {count}",
+ "allOther": "Pokaż wszystkie {count}",
+ "fewer": "Pokaż mniej",
+ "note": "Zestawione z Twoich wpisów, bez oceny. Każda data otwiera cały dzień.",
+ "report": "Raport lekarski od {date} do dziś"
+ },
+ "travel": "W podróży",
+ "event": {
+ "started": "Początek: {label}",
+ "ended": "Koniec: {label}",
+ "paused": "Wstrzymano: {label}",
+ "resumed": "Wznowiono: {label}",
+ "illnessBegan": "Początek: {label}",
+ "illnessResolved": "Wyzdrowienie: {label}"
+ },
+ "pickDate": "Wybierz datę",
+ "pickerTitle": "Przejdź do dnia",
+ "today": "Dzisiaj",
+ "hasEntries": "z wpisami",
+ "dayNSince": "Dzień {n}, od {date}",
+ "sinceDate": "Od {date}"
+ },
+ "timeline": {
+ "title": "Oś czasu",
+ "subtitle": "Co i kiedy się działo, obok Twoich wartości.",
+ "standing": "Stałe",
+ "startUnknown": "początek nieznany",
+ "zoom": {
+ "all": "Wszystko",
+ "year": "12 miesięcy",
+ "quarter": "3 miesiące",
+ "range": "Zakres"
+ },
+ "lanes": {
+ "life": "Życie",
+ "illness": "Choroby",
+ "allergies": "Alergie",
+ "medications": "Leki",
+ "vaccinations": "Szczepienia",
+ "visits": "Lekarz i zabiegi",
+ "labs": "Badania",
+ "documents": "Dokumenty",
+ "cycle": "Cykl"
+ },
+ "readiness": {
+ "title": "Co już zawiera Twoja oś czasu",
+ "subtitle": "Uczciwy przegląd, zanim ją otworzysz.",
+ "menuEntry": "Stan danych",
+ "hint": "Puste ścieżki pozostają ukryte, dopóki coś się nie pojawi. Wyłączone moduły nigdy się nie pojawiają. Ten przegląd znajdziesz później w sekcji Stan danych na osi czasu.",
+ "later": "Później",
+ "open": "Otwórz oś czasu",
+ "verdict": {
+ "carries": "Już ma treść.",
+ "thin": "Jeszcze skromna. Na razie oś czasu pokazuje głównie Twoje wartości."
+ },
+ "status": {
+ "carries": "wypełniona",
+ "thin": "skromna",
+ "empty": "pusta"
+ },
+ "lanes": {
+ "values": "Wartości",
+ "mood": "Nastrój",
+ "environment": "Otoczenie",
+ "life": "Wydarzenia życiowe"
+ },
+ "meter": "{carrying} z {total} ścieżek ma treść",
+ "carriesDetail": "{carrying} z {total} ścieżek daje przegląd od {since}.",
+ "loadFailed": "Nie udało się wczytać przeglądu.",
+ "card": {
+ "title": "Co już zawiera oś czasu",
+ "summary": "Sięga do {since}; {carrying} z {total} ścieżek ma już treść.",
+ "dismiss": "Ukryj"
+ },
+ "detail": {
+ "valuesOne": "{count} pomiar od {since}",
+ "valuesFew": "{count} pomiary od {since}",
+ "valuesOther": "{count} pomiarów od {since}",
+ "illnessOne": "{count} epizod; przewlekłe: {chronic}",
+ "illnessFew": "{count} epizody; przewlekłe: {chronic}",
+ "illnessOther": "{count} epizodów; przewlekłe: {chronic}",
+ "allergiesOne": "{count} alergia",
+ "allergiesFew": "{count} alergie",
+ "allergiesOther": "{count} alergii",
+ "medicationsOne": "{count} lek; bez daty rozpoczęcia: {missingStart}",
+ "medicationsFew": "{count} leki; bez daty rozpoczęcia: {missingStart}",
+ "medicationsOther": "{count} leków; bez daty rozpoczęcia: {missingStart}",
+ "medicationsDatedOne": "{count} lek z datą rozpoczęcia",
+ "medicationsDatedFew": "{count} leki z datą rozpoczęcia",
+ "medicationsDatedOther": "{count} leków z datą rozpoczęcia",
+ "vaccinationsOne": "{count} szczepienie",
+ "vaccinationsFew": "{count} szczepienia",
+ "vaccinationsOther": "{count} szczepień",
+ "visitsOne": "{count} wizyta, z zabiegiem: {procedures}",
+ "visitsFew": "{count} wizyty, z zabiegiem: {procedures}",
+ "visitsOther": "{count} wizyt, z zabiegiem: {procedures}",
+ "visitsPlannedOne": "{count} wizyta, tylko zaplanowana",
+ "visitsPlannedFew": "{count} wizyty, tylko zaplanowane",
+ "visitsPlannedOther": "{count} wizyt, tylko zaplanowane",
+ "labsOne": "{count} dzień z wynikami",
+ "labsFew": "{count} dni z wynikami",
+ "labsOther": "{count} dni z wynikami",
+ "documentsOne": "{count} dokument z datą",
+ "documentsFew": "{count} dokumenty z datą",
+ "documentsOther": "{count} dokumentów z datą",
+ "lifeOne": "{count} wydarzenie życiowe",
+ "lifeFew": "{count} wydarzenia życiowe",
+ "lifeOther": "{count} wydarzeń życiowych",
+ "cycleOne": "{count} cykl",
+ "cycleFew": "{count} cykle",
+ "cycleOther": "{count} cykli",
+ "moodOne": "{count} wpis od {since}",
+ "moodFew": "{count} wpisy od {since}",
+ "moodOther": "{count} wpisów od {since}",
+ "environmentOne": "{count} dzień z danymi o środowisku",
+ "environmentFew": "{count} dni z danymi o środowisku",
+ "environmentOther": "{count} dni z danymi o środowisku",
+ "empty": "Jeszcze brak"
+ },
+ "gap": {
+ "valuesEmpty": "Połącz źródła",
+ "illnessEmpty": "Dodaj epizod",
+ "medicationsWithoutStart": "Uzupełnij",
+ "visitsEmpty": "Dodaj wizytę",
+ "documentsUndated": "Przejrzyj",
+ "labsEmpty": "Dodaj wynik",
+ "vaccinationsEmpty": "Dodaj szczepienia",
+ "lifeEventsEmpty": "Dodaj"
+ },
+ "gapLine": {
+ "medicationsWithoutStartOne": "{count} lek bez daty rozpoczęcia",
+ "medicationsWithoutStartFew": "{count} leki bez daty rozpoczęcia",
+ "medicationsWithoutStartOther": "{count} leków bez daty rozpoczęcia",
+ "documentsUndatedOne": "{count} dokument bez daty",
+ "documentsUndatedFew": "{count} dokumenty bez daty",
+ "documentsUndatedOther": "{count} dokumentów bez daty",
+ "valuesEmpty": "Jeszcze brak wartości z dłuższego okresu",
+ "illnessEmpty": "Jeszcze brak chorób z datą początku",
+ "visitsEmpty": "Jeszcze brak odbytych wizyt",
+ "labsEmpty": "Jeszcze brak wyników badań",
+ "vaccinationsEmpty": "Jeszcze brak szczepień",
+ "lifeEventsEmpty": "Brak wydarzeń życiowych"
+ }
+ },
+ "legendSpan": "Okres",
+ "legendEvent": "Zdarzenie",
+ "legendMeanQuarter": "Średnia kwartalna",
+ "legendMeanMonth": "Średnia miesięczna",
+ "legendMeanWeek": "Średnia tygodniowa",
+ "legendThin": "Mniej niż 3 pomiary",
+ "legendGap": "Krótka przerwa bez pomiarów",
+ "today": "Dziś",
+ "noDataYet": "Jeszcze brak danych",
+ "layers": "Warstwy",
+ "layersLanes": "Ścieżki",
+ "zoomLabel": "Zakres widoku",
+ "chartLabel": "Oś czasu od {from} do {to}",
+ "keyboardHint": "Strzałki przesuwają wybór, Home i End przeskakują na krańce, Enter otwiera dzień.",
+ "loadFailed": "Nie udało się wczytać osi czasu.",
+ "empty": {
+ "title": "Na osi czasu jeszcze nic nie ma",
+ "description": "Wpisy z datą pojawią się tutaj, gdy tylko jakieś będą."
+ },
+ "values": {
+ "menuTitle": "Wartości pod ścieżkami",
+ "trigger": "Wartości ({count})",
+ "noneInWindow": "Brak wartości w tym okresie",
+ "series": {
+ "BLOOD_PRESSURE_SYS": "Ciśnienie skurczowe",
+ "BLOOD_PRESSURE_DIA": "Ciśnienie rozkurczowe"
+ },
+ "noValue": "brak wartości",
+ "bloodPressure": "Ciśnienie krwi",
+ "readingsOne": "1 pomiar",
+ "readingsFew": "średnia z {count} pomiarów",
+ "readingsOther": "średnia z {count} pomiarów"
+ },
+ "table": {
+ "caption": "Wpisy na osi czasu",
+ "lane": "Ścieżka",
+ "entry": "Wpis",
+ "start": "Początek",
+ "end": "Koniec",
+ "seriesCaption": "Średnie według okresu",
+ "period": "Okres"
+ },
+ "selection": {
+ "empty": "Brak wpisów w tym okresie",
+ "since": "od {date}",
+ "range": "od {from} do {to}",
+ "mean": "Ø {values}",
+ "week": "Tydzień od {date}",
+ "hintClick": "Kliknij punkt w czasie, aby zobaczyć go tutaj",
+ "hintTap": "Dotknij punktu w czasie, aby zobaczyć go tutaj"
+ },
+ "chronicle": {
+ "months": "Miesiące",
+ "years": "Lata",
+ "groupingLabel": "Grupowanie",
+ "empty": "Brak wpisów od {from} do {to}",
+ "emptySingle": "Brak wpisów: {period}",
+ "ended": "Koniec: {label}",
+ "afterDaysOne": "po 1 dniu",
+ "afterDaysFew": "po {count} dniach",
+ "afterDaysOther": "po {count} dniach",
+ "notable": {
+ "extremeHigh": "Najwyższa wartość od dawna",
+ "extremeLow": "Najniższa wartość od dawna",
+ "firstValue": "Pierwsza wartość pomiaru",
+ "gap": "Dłuższa przerwa w wartościach"
+ },
+ "notableMeta": "Otwórz dzień, aby zobaczyć szczegóły"
+ },
+ "standingAllergiesOne": "{count} alergia",
+ "standingAllergiesFew": "{count} alergie",
+ "standingAllergiesOther": "{count} alergii",
+ "legendMeanDay": "Średnia dzienna",
+ "range": {
+ "from": "Od",
+ "to": "Do",
+ "label": "Wybrany zakres",
+ "open": "Wybierz zakres",
+ "clear": "Pokaż wszystko",
+ "apply": "Zastosuj"
+ },
+ "item": {
+ "labValuesOne": "1 wynik badania",
+ "labValuesFew": "{count} wyniki badań",
+ "labValuesOther": "{count} wyników badań"
+ }
+ },
+ "lifeEvents": {
+ "title": "Wydarzenie życiowe",
+ "add": "Wydarzenie życiowe",
+ "privacy": "Tytuł i notatka są zapisywane w postaci zaszyfrowanej. Nie trafiają do żadnej AI.",
+ "emptyHint": "Przeprowadzka, narodziny dziecka czy nowa praca dają latom punkt odniesienia.",
+ "save": "Zapisz",
+ "cancel": "Anuluj",
+ "delete": "Usuń wydarzenie życiowe",
+ "saved": "Zapisano wydarzenie życiowe",
+ "deleted": "Usunięto wydarzenie życiowe",
+ "fields": {
+ "title": "Co się wydarzyło?",
+ "category": "Rodzaj",
+ "when": "Kiedy",
+ "precision": "Dokładność",
+ "addEnd": "Data zakończenia, jeśli był to okres",
+ "endDate": "Do",
+ "note": "Notatka",
+ "optionalTag": "opcjonalnie",
+ "notePlaceholder": "Coś, co zechcesz wiedzieć później",
+ "removeEnd": "Usuń datę zakończenia"
+ },
+ "category": {
+ "FAMILY": "Rodzina",
+ "HOME": "Mieszkanie",
+ "WORK": "Praca",
+ "LOSS": "Strata",
+ "OTHER": "Inne"
+ },
+ "precision": {
+ "DAY": "Dzień",
+ "MONTH": "Miesiąc",
+ "YEAR": "Rok"
+ },
+ "edit": "Edytuj wydarzenie życiowe",
+ "errors": {
+ "titleRequired": "Nadaj wydarzeniu nazwę.",
+ "titleTooLong": "Najwyżej 120 znaków.",
+ "categoryRequired": "Wybierz rodzaj.",
+ "dateRequired": "Podaj prawidłową datę.",
+ "endBeforeStart": "Koniec jest przed początkiem.",
+ "noteTooLong": "Najwyżej 2000 znaków."
+ },
+ "saveFailed": "Nie udało się zapisać.",
+ "deleteFailed": "Nie udało się usunąć.",
+ "deleteConfirm": {
+ "title": "Usunąć to wydarzenie życiowe?",
+ "body": "Tytuł i notatka znikną."
+ }
+ },
+ "palette": {
+ "title": "Szukaj",
+ "description": "Znajdź stronę, ustawienie, moduł, akcję lub wpis w swoich danych.",
+ "placeholder": "Szukaj stron, ustawień, swoich wpisów…",
+ "results": "Wyniki",
+ "noResults": "Brak wyników dla „{query}”.",
+ "moduleOn": "Wł.",
+ "moduleOff": "Wył.",
+ "hintMove": "Wybierz",
+ "hintOpen": "Otwórz",
+ "hintClose": "Zamknij",
+ "groups": {
+ "recent": "Ostatnie",
+ "pages": "Strony",
+ "insights": "Insights",
+ "settings": "Ustawienia",
+ "modules": "Moduły",
+ "actions": "Akcje",
+ "records": "W twoich wpisach"
+ },
+ "actions": {
+ "capture": "Nowy wpis",
+ "coach": "Zapytaj Coacha",
+ "backup": "Utwórz kopię zapasową",
+ "today": "Otwórz dzisiaj",
+ "shortcuts": "Pokaż skróty klawiszowe"
+ },
+ "records": {
+ "loading": "Przeszukiwanie twoich wpisów…",
+ "empty": "Brak wyników w twoich wpisach dla „{query}”.",
+ "error": "Nie udało się przeszukać twoich wpisów. Spróbuj ponownie za chwilę.",
+ "truncated": "Przeszukano tylko najnowsze wpisy.",
+ "kind": {
+ "visit": "Wizyta",
+ "condition": "Choroba",
+ "document": "Dokument",
+ "vaccination": "Szczepienie",
+ "lab": "Wynik badania",
+ "medication": "Lek",
+ "symptom": "Objaw",
+ "workout": "Trening",
+ "note": "Notatka"
+ },
+ "source": {
+ "mood": "Notatka o nastroju",
+ "measurement": "Notatka do pomiaru",
+ "illness": "Dziennik choroby"
+ }
+ }
+ },
+ "shortcuts": {
+ "title": "Skróty klawiszowe",
+ "description": "Klawisze działające w całej aplikacji, z wyjątkiem pisania w polu.",
+ "menuEntry": "Skróty klawiszowe",
+ "then": "potem",
+ "search": "Szukaj",
+ "help": "Pokaż tę listę",
+ "close": "Zamknij okno lub panel",
+ "previousDay": "Poprzedni dzień",
+ "nextDay": "Następny dzień",
+ "toggleDay": "Otwórz lub zamknij dzień",
+ "dayHint": "Poprzedni i następny dzień działają, gdy dzień jest otwarty.",
+ "groups": {
+ "navigation": "Nawigacja",
+ "actions": "Akcje",
+ "day": "Dzień"
+ }
+ },
+ "aboutPage": {
+ "eyebrow": "Informacje",
+ "title": "O HealthLog",
+ "lastUpdated": "Ostatnia aktualizacja: {date}",
+ "project": "Projekt",
+ "credits": "Podziękowania"
}
}
diff --git a/next.config.ts b/next.config.ts
index 2edfcfba3..672f10a91 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,8 +1,31 @@
+import { createHash } from "node:crypto";
+import { readdirSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+
import type { NextConfig } from "next";
import bundleAnalyzer from "@next/bundle-analyzer";
import { version as PKG_VERSION } from "./package.json";
+/**
+ * The message catalogs' content hash, the cache key of the boot script
+ * (`/i18n/.js?v=…`, `src/app/layout.tsx`). The service worker serves
+ * that URL cache-first, so a key that does not move with the catalog keeps
+ * an old catalog alive: keyed by the package version, every build of one
+ * release (a beta, a redeploy) shared one URL, and a key added since the
+ * first of them read as its raw name.
+ */
+function catalogVersion(): string {
+ const dir = join(process.cwd(), "messages");
+ const hash = createHash("sha256");
+ for (const file of readdirSync(dir)
+ .filter((f) => f.endsWith(".json"))
+ .sort()) {
+ hash.update(file).update(readFileSync(join(dir, file)));
+ }
+ return hash.digest("hex").slice(0, 16);
+}
+
const nextConfig: NextConfig = {
output: "standalone",
// `next build` type-checks the whole tsconfig program since 16.3, which
@@ -20,6 +43,7 @@ const nextConfig: NextConfig = {
// had to discover "pull-to-refresh" themselves after every release.
env: {
NEXT_PUBLIC_APP_VERSION: PKG_VERSION,
+ NEXT_PUBLIC_I18N_CATALOG_VERSION: catalogVersion(),
},
// v1.4.33 IW2 — strip `console.*` calls from the production bundle
// (keep `console.error` + `console.warn` so the GlitchTip reporter
diff --git a/package.json b/package.json
index 8f2a1b5f0..9e0f428bd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "healthlog",
- "version": "1.39.9",
+ "version": "1.42.2",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
@@ -53,89 +53,90 @@
"mcp:stdio": "npx tsx src/cli/mcp-stdio.ts"
},
"dependencies": {
- "@aws-sdk/client-s3": "^3.1075.0",
- "@aws-sdk/lib-storage": "3.1075.0",
+ "@aws-sdk/client-s3": "^3.1146.0",
+ "@aws-sdk/lib-storage": "3.1147.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.9.1",
- "@modelcontextprotocol/sdk": "^1.30.0",
- "@napi-rs/canvas": "1.0.9",
- "@node-rs/argon2": "^2.1.0",
+ "@modelcontextprotocol/sdk": "^1.32.1",
+ "@napi-rs/canvas": "1.0.10",
+ "@node-rs/argon2": "^2.2.2",
"@parse/node-apn": "^8.1.0",
"@prisma/adapter-pg": "^7.10.0",
"@prisma/client": "^7.10.0",
"@simplewebauthn/browser": "^14.0.0",
- "@simplewebauthn/server": "^14.0.2",
- "@tanstack/react-query": "5.103.2",
- "@tanstack/react-virtual": "^3.14.12",
+ "@simplewebauthn/server": "^14.0.3",
+ "@tanstack/react-query": "5.104.1",
+ "@tanstack/react-virtual": "^3.14.13",
"@types/sax": "^1.2.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
- "dotenv": "^17.4.2",
+ "dotenv": "^18.0.6",
"fflate": "^0.8.3",
"jose": "^6.2.12",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8",
- "lucide-react": "^1.44.0",
- "mmdb-lib": "^3.0.2",
- "next": "16.3.6",
+ "lucide-react": "^1.53.0",
+ "mmdb-lib": "^3.0.3",
+ "next": "16.3.8",
"next-themes": "^0.4.6",
- "nodemailer": "10.0.9",
- "otpauth": "^9.5.1",
- "p-limit": "^7.3.1",
- "pg": "^8.23.0",
- "pg-boss": "^12.34.0",
+ "nodemailer": "10.0.16",
+ "otpauth": "^9.5.2",
+ "p-limit": "^7.3.3",
+ "pg": "^8.23.1",
+ "pg-boss": "^12.37.1",
"prisma": "^7.10.0",
"qrcode": "^1.5.4",
- "radix-ui": "^1.6.7",
+ "radix-ui": "^1.7.0",
"react": "19.3.0",
- "react-day-picker": "^10.0.1",
+ "react-day-picker": "^10.0.2",
"react-dom": "19.3.0",
- "react-hook-form": "7.83.0",
+ "react-hook-form": "7.89.0",
"recharts": "^3.10.1",
"rrule": "^2.8.1",
"sax": "^1.6.1",
"sonner": "^2.0.8",
- "tailwind-merge": "^3.6.0",
+ "tailwind-merge": "^3.7.0",
"tesseract.js": "^7.0.0",
"undici": "^8.11.2",
"web-push": "^3.6.7",
- "yaml": "^2.8.4",
+ "yaml": "^2.9.1",
"zod": "^4.4.3",
- "zod-openapi": "^6.0.1",
+ "zod-openapi": "^6.0.2",
"zxcvbn-typescript": "^5.0.1"
},
"devDependencies": {
"@axe-core/playwright": "^4.13.0",
- "@next/bundle-analyzer": "^16.3.3",
- "@playwright/test": "^1.63.0",
+ "@next/bundle-analyzer": "^16.3.8",
+ "@playwright/test": "^1.64.0",
"@tailwindcss/postcss": "^4",
"@tesseract.js-data/deu": "1.0.0",
"@tesseract.js-data/eng": "1.0.0",
- "@testcontainers/postgresql": "^12.1.0",
- "@types/node": "26.6.2",
+ "@testcontainers/postgresql": "^12.2.0",
+ "@types/node": "26.6.4",
"@types/pg": "^8.23.1",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@types/web-push": "^3.6.4",
- "@vitejs/plugin-react": "^6.1.1",
- "@vitest/coverage-v8": "^4.1.5",
+ "@vitejs/plugin-react": "^6.1.2",
+ "@vitest/coverage-v8": "^5.0.3",
+ "ajv": "^8.20.0",
"eslint": "^9",
- "eslint-config-next": "16.3.6",
- "knip": "^6.32.3",
- "lefthook": "^2.1.12",
+ "eslint-config-next": "16.3.8",
+ "knip": "^6.40.0",
+ "lefthook": "^2.2.1",
"pdf-parse": "^2.4.5",
- "pdfjs-dist": "6.3.289",
+ "pdfjs-dist": "6.4.299",
"prettier": "^3.9.9",
"prettier-plugin-tailwindcss": "^0.8.1",
- "shadcn": "^4.16.1",
+ "shadcn": "^4.21.4",
"tailwindcss": "^4",
- "testcontainers": "^12.1.0",
+ "testcontainers": "^12.2.0",
"tw-animate-css": "^1.4.0",
"typescript": "^6",
- "vitest": "^4.1.11"
+ "vitest": "^5.0.3"
}
}
diff --git a/playwright.config.ts b/playwright.config.ts
index bf46cf31d..796f9b4fe 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -110,7 +110,10 @@ export default defineConfig({
name: "chromium-desktop",
// The disk-layer spec needs a live service worker, which the shared
// `use` block blocks for every other spec. It runs in its own project.
- testIgnore: ["v137-record-session-fence-offline.spec.ts"],
+ testIgnore: [
+ "v137-record-session-fence-offline.spec.ts",
+ "pwa-offline.spec.ts",
+ ],
use: {
...devices["Desktop Chrome"],
viewport: { width: 1280, height: 720 },
@@ -130,10 +133,14 @@ export default defineConfig({
// cannot pass is worse than no check, so the spec gets a project rather
// than a caveat.
//
- // It uses no route mocks for exactly this reason, and it is the only
- // spec in this project.
+ // It uses no route mocks for exactly this reason. v1.42 — the offline
+ // page journey (`pwa-offline.spec.ts`) joined it on the same terms:
+ // what the worker itself serves without a connection, no mocks.
name: "chromium-service-worker",
- testMatch: ["v137-record-session-fence-offline.spec.ts"],
+ testMatch: [
+ "v137-record-session-fence-offline.spec.ts",
+ "pwa-offline.spec.ts",
+ ],
use: {
...devices["Desktop Chrome"],
viewport: { width: 1280, height: 720 },
@@ -185,6 +192,11 @@ export default defineConfig({
// through stable data-slots, not a mobile layout, so it runs in one
// project.
"vaccinations.spec.ts",
+ // The timeline journey moves its account's timeline module on and
+ // off; two projects on that one account would flip it under each
+ // other. It checks the phone chronicle itself by resizing inside the
+ // desktop project.
+ "timeline.spec.ts",
// The blood-glucose journey writes readings to the one shared account
// and then reads GLOBAL verdicts off it — three rows in the list, one
// tile per meal-time context — while also moving that account's
@@ -215,6 +227,7 @@ export default defineConfig({
"doctor-report-delegate.spec.ts",
// Runs only in the service-worker project.
"v137-record-session-fence-offline.spec.ts",
+ "pwa-offline.spec.ts",
// The Apple Health import journey uploads an archive into the one
// shared account and its refusal control asserts that the account's
// Apple-Health row count did not move. A second project running the
@@ -276,6 +289,11 @@ export default defineConfig({
// account under the first. They prove server state through stable
// data-slots, not a mobile layout, so they run in one project.
"ai-optional-account.spec.ts",
+ // The score-history journey moves its account's top card and range
+ // tabs; two projects on that one account would flip them under each
+ // other. It checks the phone width by resizing inside the desktop
+ // project.
+ "score-history.spec.ts",
],
use: {
// Pixel 5 — Chromium-based mobile profile so CI only needs
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 83e106bab..979bf89d3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,9 +15,13 @@ overrides:
'@hono/node-server@<1.19.15': ^1.19.15
picomatch@<4.0.4: ^4.0.4
body-parser@<2.3.0: ^2.3.0
+ source-map-js@<1.2.2: ^1.2.2
+ postcss-selector-parser@<7.1.6: ^7.1.6
+ smol-toml@<1.9.0: ^1.9.0
qs@<6.16.0: ^6.16.0
fast-uri@<3.1.8: ^3.1.8
flatted@<3.4.2: ^3.4.2
+ proxy-addr@<2.0.8: ^2.0.8
mysql2@<3.22.0: ^3.22.0
minimatch@<3.1.4: ^3.1.4
ip-address@<10.7.1: ^10.7.1
@@ -38,11 +42,11 @@ importers:
.:
dependencies:
'@aws-sdk/client-s3':
- specifier: ^3.1075.0
- version: 3.1075.0
+ specifier: ^3.1146.0
+ version: 3.1147.0
'@aws-sdk/lib-storage':
- specifier: 3.1075.0
- version: 3.1075.0(@aws-sdk/client-s3@3.1075.0)
+ specifier: 3.1147.0
+ version: 3.1147.0(@aws-sdk/client-s3@3.1147.0)
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
@@ -54,16 +58,16 @@ importers:
version: 3.2.2(react@19.3.0)
'@hookform/resolvers':
specifier: ^5.9.1
- version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.3.0))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)
+ version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.89.0(@types/react@19.3.0)(react@19.3.0))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)
'@modelcontextprotocol/sdk':
- specifier: ^1.30.0
- version: 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3)
+ specifier: ^1.32.1
+ version: 1.32.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3)
'@napi-rs/canvas':
- specifier: 1.0.9
- version: 1.0.9
+ specifier: 1.0.10
+ version: 1.0.10
'@node-rs/argon2':
- specifier: ^2.1.0
- version: 2.1.0
+ specifier: ^2.2.2
+ version: 2.2.2
'@parse/node-apn':
specifier: ^8.1.0
version: 8.1.0(supports-color@7.2.0)
@@ -72,19 +76,19 @@ importers:
version: 7.10.0
'@prisma/client':
specifier: ^7.10.0
- version: 7.10.0(prisma@7.10.0(@types/node@26.6.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)
+ version: 7.10.0(prisma@7.10.0(@types/node@26.6.4)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)
'@simplewebauthn/browser':
specifier: ^14.0.0
version: 14.0.0
'@simplewebauthn/server':
- specifier: ^14.0.2
- version: 14.0.2
+ specifier: ^14.0.3
+ version: 14.0.3
'@tanstack/react-query':
- specifier: 5.103.2
- version: 5.103.2(react@19.3.0)
+ specifier: 5.104.1
+ version: 5.104.1(react@19.3.0)
'@tanstack/react-virtual':
- specifier: ^3.14.12
- version: 3.14.12(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ specifier: ^3.14.13
+ version: 3.14.13(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@types/sax':
specifier: ^1.2.7
version: 1.2.7
@@ -98,8 +102,8 @@ importers:
specifier: ^4.4.0
version: 4.4.0
dotenv:
- specifier: ^17.4.2
- version: 17.4.2
+ specifier: ^18.0.6
+ version: 18.0.6
fflate:
specifier: ^0.8.3
version: 0.8.3
@@ -113,53 +117,53 @@ importers:
specifier: ^5.0.8
version: 5.0.8(jspdf@4.2.1)
lucide-react:
- specifier: ^1.44.0
- version: 1.44.0(react@19.3.0)
+ specifier: ^1.53.0
+ version: 1.53.0(@types/react@19.3.0)(react@19.3.0)
mmdb-lib:
- specifier: ^3.0.2
- version: 3.0.2
+ specifier: ^3.0.3
+ version: 3.0.3
next:
- specifier: 16.3.6
- version: 16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.63.0)(@types/node@26.6.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ specifier: 16.3.8
+ version: 16.3.8(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.64.0)(@types/node@26.6.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
nodemailer:
- specifier: 10.0.9
- version: 10.0.9
+ specifier: 10.0.16
+ version: 10.0.16
otpauth:
- specifier: ^9.5.1
- version: 9.5.1
+ specifier: ^9.5.2
+ version: 9.5.2
p-limit:
- specifier: ^7.3.1
- version: 7.3.1
+ specifier: ^7.3.3
+ version: 7.3.3
pg:
- specifier: ^8.23.0
- version: 8.23.0
+ specifier: ^8.23.1
+ version: 8.23.1
pg-boss:
- specifier: ^12.34.0
- version: 12.34.0
+ specifier: ^12.37.1
+ version: 12.37.1(@opentelemetry/api@1.9.1)
prisma:
specifier: ^7.10.0
- version: 7.10.0(@types/node@26.6.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
+ version: 7.10.0(@types/node@26.6.4)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
qrcode:
specifier: ^1.5.4
version: 1.5.4
radix-ui:
- specifier: ^1.6.7
- version: 1.6.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ specifier: ^1.7.0
+ version: 1.7.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react:
specifier: 19.3.0
version: 19.3.0
react-day-picker:
- specifier: ^10.0.1
- version: 10.0.1(@types/react@19.3.0)(react@19.3.0)
+ specifier: ^10.0.2
+ version: 10.0.2(@types/react@19.3.0)(react@19.3.0)
react-dom:
specifier: 19.3.0
version: 19.3.0(react@19.3.0)
react-hook-form:
- specifier: 7.83.0
- version: 7.83.0(react@19.3.0)
+ specifier: 7.89.0
+ version: 7.89.0(@types/react@19.3.0)(react@19.3.0)
recharts:
specifier: ^3.10.1
version: 3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@16.13.1)(react@19.3.0)(redux@5.0.1)
@@ -173,8 +177,8 @@ importers:
specifier: ^2.0.8
version: 2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
tailwind-merge:
- specifier: ^3.6.0
- version: 3.6.0
+ specifier: ^3.7.0
+ version: 3.7.0
tesseract.js:
specifier: ^7.0.0
version: 7.0.0
@@ -185,27 +189,27 @@ importers:
specifier: ^3.6.7
version: 3.6.7(supports-color@7.2.0)
yaml:
- specifier: ^2.8.4
- version: 2.8.4
+ specifier: ^2.9.1
+ version: 2.9.1
zod:
specifier: ^4.4.3
version: 4.4.3
zod-openapi:
- specifier: ^6.0.1
- version: 6.0.1(zod@4.4.3)
+ specifier: ^6.0.2
+ version: 6.0.2(zod@4.4.3)
zxcvbn-typescript:
specifier: ^5.0.1
version: 5.0.1
devDependencies:
'@axe-core/playwright':
specifier: ^4.13.0
- version: 4.13.0(playwright-core@1.63.0)
+ version: 4.13.0(playwright-core@1.64.0)
'@next/bundle-analyzer':
- specifier: ^16.3.3
- version: 16.3.3
+ specifier: ^16.3.8
+ version: 16.3.8
'@playwright/test':
- specifier: ^1.63.0
- version: 1.63.0
+ specifier: ^1.64.0
+ version: 1.64.0
'@tailwindcss/postcss':
specifier: ^4
version: 4.3.3
@@ -216,11 +220,11 @@ importers:
specifier: 1.0.0
version: 1.0.0
'@testcontainers/postgresql':
- specifier: ^12.1.0
- version: 12.1.0(supports-color@7.2.0)
+ specifier: ^12.2.0
+ version: 12.2.0(supports-color@7.2.0)
'@types/node':
- specifier: 26.6.2
- version: 26.6.2
+ specifier: 26.6.4
+ version: 26.6.4
'@types/pg':
specifier: ^8.23.1
version: 8.23.1
@@ -237,29 +241,32 @@ importers:
specifier: ^3.6.4
version: 3.6.4
'@vitejs/plugin-react':
- specifier: ^6.1.1
- version: 6.1.1(vite@8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4))
+ specifier: ^6.1.2
+ version: 6.1.2(vite@8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))
'@vitest/coverage-v8':
- specifier: ^4.1.5
- version: 4.1.5(vitest@4.1.11)
+ specifier: ^5.0.3
+ version: 5.0.3(vitest@5.0.3)
+ ajv:
+ specifier: ^8.20.0
+ version: 8.20.0
eslint:
specifier: ^9
version: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
eslint-config-next:
- specifier: 16.3.6
- version: 16.3.6(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ specifier: 16.3.8
+ version: 16.3.8(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
knip:
- specifier: ^6.32.3
- version: 6.32.3
+ specifier: ^6.40.0
+ version: 6.40.0
lefthook:
- specifier: ^2.1.12
- version: 2.1.12
+ specifier: ^2.2.1
+ version: 2.2.1
pdf-parse:
specifier: ^2.4.5
version: 2.4.5
pdfjs-dist:
- specifier: 6.3.289
- version: 6.3.289
+ specifier: 6.4.299
+ version: 6.4.299
prettier:
specifier: ^3.9.9
version: 3.9.9
@@ -267,14 +274,14 @@ importers:
specifier: ^0.8.1
version: 0.8.1(prettier@3.9.9)
shadcn:
- specifier: ^4.16.1
- version: 4.16.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(typescript@6.0.3)
+ specifier: ^4.21.4
+ version: 4.21.4(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)
tailwindcss:
specifier: ^4
version: 4.3.3
testcontainers:
- specifier: ^12.1.0
- version: 12.1.0(supports-color@7.2.0)
+ specifier: ^12.2.0
+ version: 12.2.0(supports-color@7.2.0)
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
@@ -282,8 +289,8 @@ importers:
specifier: ^6
version: 6.0.3
vitest:
- specifier: ^4.1.11
- version: 4.1.11(@types/node@26.6.2)(@vitest/coverage-v8@4.1.5)(msw@2.14.6(@types/node@26.6.2)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4))
+ specifier: ^5.0.3
+ version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(msw@2.14.6(@types/node@26.6.4)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))
packages:
@@ -291,113 +298,82 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
- '@aws-crypto/crc32@5.2.0':
- resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
- engines: {node: '>=16.0.0'}
-
- '@aws-crypto/crc32c@5.2.0':
- resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==}
-
- '@aws-crypto/sha1-browser@5.2.0':
- resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==}
-
- '@aws-crypto/sha256-browser@5.2.0':
- resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==}
-
- '@aws-crypto/sha256-js@5.2.0':
- resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==}
- engines: {node: '>=16.0.0'}
-
- '@aws-crypto/supports-web-crypto@5.2.0':
- resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==}
-
- '@aws-crypto/util@5.2.0':
- resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
-
- '@aws-sdk/checksums@3.1000.8':
- resolution: {integrity: sha512-v0U9S7gBIme3OTgt1LdbAF4RpvavCc+4GK1+1xqAcqtbrHsEhjQo6R45LKcjhs/+WrRJij1Y0Gztw7QPAIeUfA==}
+ '@aws-sdk/checksums@3.1001.1':
+ resolution: {integrity: sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/client-s3@3.1075.0':
- resolution: {integrity: sha512-h1A6nIl1YX6Y45enGsTK7ef3ZrOnBiQJ1qF5R2K/nMWfsu6A9mc2Y5T66nxerABzyjjyyvign3MrzafnFoQKmA==}
+ '@aws-sdk/client-s3@3.1147.0':
+ resolution: {integrity: sha512-uTrcJxnJKPr8bc1z7cMxopYSQyAm2dPfiMK+UiBr51L8MzimDxHbW/d9GR6X/LFbWDpMzfFPLhB4UohDrCSaPQ==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/core@3.974.23':
- resolution: {integrity: sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==}
+ '@aws-sdk/core@3.978.1':
+ resolution: {integrity: sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-env@3.972.49':
- resolution: {integrity: sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==}
+ '@aws-sdk/credential-provider-env@3.972.72':
+ resolution: {integrity: sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-http@3.972.51':
- resolution: {integrity: sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==}
+ '@aws-sdk/credential-provider-http@3.972.74':
+ resolution: {integrity: sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-ini@3.972.56':
- resolution: {integrity: sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==}
+ '@aws-sdk/credential-provider-ini@3.973.17':
+ resolution: {integrity: sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-login@3.972.55':
- resolution: {integrity: sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==}
+ '@aws-sdk/credential-provider-login@3.972.79':
+ resolution: {integrity: sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-node@3.972.58':
- resolution: {integrity: sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==}
+ '@aws-sdk/credential-provider-node@3.972.84':
+ resolution: {integrity: sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-process@3.972.49':
- resolution: {integrity: sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==}
+ '@aws-sdk/credential-provider-process@3.972.72':
+ resolution: {integrity: sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-sso@3.972.55':
- resolution: {integrity: sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==}
+ '@aws-sdk/credential-provider-sso@3.973.16':
+ resolution: {integrity: sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-web-identity@3.972.55':
- resolution: {integrity: sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==}
+ '@aws-sdk/credential-provider-web-identity@3.972.78':
+ resolution: {integrity: sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/lib-storage@3.1075.0':
- resolution: {integrity: sha512-Npdac3/Fv994iCdFFeqloPel+95Zv6dRiRUpfey6W/CqpB/QdnYP60U4YBhBgJj6V5020Pm3ad0HV4WzCE5hHQ==}
+ '@aws-sdk/lib-storage@3.1147.0':
+ resolution: {integrity: sha512-f2eAbdiABtmJOgojJlAI7epzHDC9BO9bij788FDIk1LM1gvN6ibQLPx192f+oCL8A4gc4BH6qnxJZRBb8B3REA==}
engines: {node: '>=20.0.0'}
peerDependencies:
- '@aws-sdk/client-s3': ^3.1075.0
+ '@aws-sdk/client-s3': ^3.1147.0
- '@aws-sdk/middleware-flexible-checksums@3.974.33':
- resolution: {integrity: sha512-qMgQSPemQq2/eW/e/0+SpY4kYR5L7dUgBiVdEc5bd+ztHNv07ZMYiI+sTiir3TgKndFfglSw/VFi7oZJ6bZ63g==}
+ '@aws-sdk/middleware-sdk-s3@3.972.77':
+ resolution: {integrity: sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/middleware-sdk-s3@3.972.54':
- resolution: {integrity: sha512-GDfDQ0gwLFRKN9gWIKcmVrHJ3e7XagnY7N1LLzMVNgnOnuY7f/ALgmy3CuBjosWD95T/Z6e+gs1IeWmLPkyLKQ==}
+ '@aws-sdk/nested-clients@3.997.46':
+ resolution: {integrity: sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/nested-clients@3.997.23':
- resolution: {integrity: sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==}
+ '@aws-sdk/signature-v4-multi-region@3.996.47':
+ resolution: {integrity: sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/signature-v4-multi-region@3.996.35':
- resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==}
+ '@aws-sdk/token-providers@3.1138.0':
+ resolution: {integrity: sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/token-providers@3.1074.0':
- resolution: {integrity: sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==}
+ '@aws-sdk/types@3.974.6':
+ resolution: {integrity: sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/types@3.973.13':
- resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==}
+ '@aws-sdk/xml-builder@3.972.41':
+ resolution: {integrity: sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/util-locate-window@3.965.5':
- resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==}
- engines: {node: '>=20.0.0'}
-
- '@aws-sdk/xml-builder@3.972.31':
- resolution: {integrity: sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==}
- engines: {node: '>=20.0.0'}
-
- '@aws/lambda-invoke-store@0.2.4':
- resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==}
+ '@aws/lambda-invoke-store@0.3.0':
+ resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
engines: {node: '>=18.0.0'}
'@axe-core/playwright@4.13.0':
@@ -487,8 +463,8 @@ packages:
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
engines: {node: '>=6.9.0'}
- '@babel/parser@7.29.8':
- resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
+ '@babel/parser@7.29.9':
+ resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -510,8 +486,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-typescript@7.29.7':
- resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==}
+ '@babel/plugin-transform-typescript@7.29.9':
+ resolution: {integrity: sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -530,8 +506,8 @@ packages:
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
- '@babel/traverse@7.29.8':
- resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
+ '@babel/traverse@7.29.10':
+ resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==}
engines: {node: '>=6.9.0'}
'@babel/types@7.29.8':
@@ -809,160 +785,160 @@ packages:
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
- '@img/sharp-darwin-arm64@0.35.4':
- resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
+ '@img/sharp-darwin-arm64@0.35.5':
+ resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
- '@img/sharp-darwin-x64@0.35.4':
- resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
+ '@img/sharp-darwin-x64@0.35.5':
+ resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
- '@img/sharp-freebsd-wasm32@0.35.4':
- resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
+ '@img/sharp-freebsd-wasm32@0.35.5':
+ resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
engines: {node: '>=20.9.0'}
os: [freebsd]
- '@img/sharp-libvips-darwin-arm64@1.3.3':
- resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
+ resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
cpu: [arm64]
os: [darwin]
- '@img/sharp-libvips-darwin-x64@1.3.3':
- resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
+ '@img/sharp-libvips-darwin-x64@1.3.4':
+ resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-linux-arm64@1.3.3':
- resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
+ '@img/sharp-libvips-linux-arm64@1.3.4':
+ resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@img/sharp-libvips-linux-arm@1.3.3':
- resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
+ '@img/sharp-libvips-linux-arm@1.3.4':
+ resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
cpu: [arm]
os: [linux]
libc: [glibc]
- '@img/sharp-libvips-linux-ppc64@1.3.3':
- resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
+ resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@img/sharp-libvips-linux-riscv64@1.3.3':
- resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
+ resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@img/sharp-libvips-linux-s390x@1.3.3':
- resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
+ '@img/sharp-libvips-linux-s390x@1.3.4':
+ resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@img/sharp-libvips-linux-x64@1.3.3':
- resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
+ '@img/sharp-libvips-linux-x64@1.3.4':
+ resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
- resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
+ resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@img/sharp-libvips-linuxmusl-x64@1.3.3':
- resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
+ resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@img/sharp-linux-arm64@0.35.4':
- resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
+ '@img/sharp-linux-arm64@0.35.5':
+ resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@img/sharp-linux-arm@0.35.4':
- resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
+ '@img/sharp-linux-arm@0.35.5':
+ resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
- '@img/sharp-linux-ppc64@0.35.4':
- resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
+ '@img/sharp-linux-ppc64@0.35.5':
+ resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@img/sharp-linux-riscv64@0.35.4':
- resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
+ '@img/sharp-linux-riscv64@0.35.5':
+ resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@img/sharp-linux-s390x@0.35.4':
- resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
+ '@img/sharp-linux-s390x@0.35.5':
+ resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@img/sharp-linux-x64@0.35.4':
- resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
+ '@img/sharp-linux-x64@0.35.5':
+ resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@img/sharp-linuxmusl-arm64@0.35.4':
- resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
+ '@img/sharp-linuxmusl-arm64@0.35.5':
+ resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@img/sharp-linuxmusl-x64@0.35.4':
- resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
+ '@img/sharp-linuxmusl-x64@0.35.5':
+ resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@img/sharp-wasm32@0.35.4':
- resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
+ '@img/sharp-wasm32@0.35.5':
+ resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
engines: {node: '>=20.9.0'}
- '@img/sharp-webcontainers-wasm32@0.35.4':
- resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
+ '@img/sharp-webcontainers-wasm32@0.35.5':
+ resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
- '@img/sharp-win32-arm64@0.35.4':
- resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
+ '@img/sharp-win32-arm64@0.35.5':
+ resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
- '@img/sharp-win32-ia32@0.35.4':
- resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
+ '@img/sharp-win32-ia32@0.35.5':
+ resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
- '@img/sharp-win32-x64@0.35.4':
- resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
+ '@img/sharp-win32-x64@0.35.5':
+ resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
@@ -1019,6 +995,9 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
@@ -1031,8 +1010,8 @@ packages:
'@levischuck/tiny-cbor@0.2.11':
resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==}
- '@modelcontextprotocol/sdk@1.30.0':
- resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==}
+ '@modelcontextprotocol/sdk@1.32.1':
+ resolution: {integrity: sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==}
engines: {node: '>=18'}
peerDependencies:
'@cfworker/json-schema': ^4.1.1
@@ -1051,8 +1030,8 @@ packages:
cpu: [arm64]
os: [android]
- '@napi-rs/canvas-android-arm64@1.0.9':
- resolution: {integrity: sha512-4LGXk2/0HVzE29K8SzML5WubgCp++B1FH3qgl35XmSZE+lLdr6P9VRQEnZ0MCLZMTSuJP41yyhtoiVNEuvrTIA==}
+ '@napi-rs/canvas-android-arm64@1.0.10':
+ resolution: {integrity: sha512-0sDMvme+7fj6bHERUaBs5SvEyWaORc91VUkaB4RrtG7p6dBrBNGy1of8cyEKDxygd1FlHtW+Thunj8lSixzY7A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
@@ -1063,8 +1042,8 @@ packages:
cpu: [arm64]
os: [darwin]
- '@napi-rs/canvas-darwin-arm64@1.0.9':
- resolution: {integrity: sha512-YNdfLBzY0W/Pep9fo2L6RmoNlNksnn05LRnX66W63R3ij58S25QOTcjdtEt2v8+PnCESzqZsYzUo+QPeIR44NA==}
+ '@napi-rs/canvas-darwin-arm64@1.0.10':
+ resolution: {integrity: sha512-pdHL1TEfFMrnchRYqmPWXKN7hR4XtOYmzJqnh4XhpfWD4/eeSokOGVzrdFP/iQ3sfr2rf0939gOGq9uCp8BGmg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -1075,8 +1054,8 @@ packages:
cpu: [x64]
os: [darwin]
- '@napi-rs/canvas-darwin-x64@1.0.9':
- resolution: {integrity: sha512-ceZQSknTEcy3dOXoekv59LTCkXjvnLsq+VW5PeNNDHEPQbRS5Ervkm1EaDa7WLAjiYWMLuSQTRTHao1dEX4prg==}
+ '@napi-rs/canvas-darwin-x64@1.0.10':
+ resolution: {integrity: sha512-iuuk5plGxAcxurxv2ycF2yR20y5zvaBZ4heQdhPpg3EMFD21PhlY1V+YwSRmYRWzbvKL474UYdw3WIg2qfafyw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -1087,8 +1066,8 @@ packages:
cpu: [arm]
os: [linux]
- '@napi-rs/canvas-linux-arm-gnueabihf@1.0.9':
- resolution: {integrity: sha512-XhfI0Wwv4llhd6nnWDtY3kQKjq0r+y1i91PlJlJI24ag2U9WrnwbG1qS3+fDLEyouwEVFchiKkTEHozK+5iUNA==}
+ '@napi-rs/canvas-linux-arm-gnueabihf@1.0.10':
+ resolution: {integrity: sha512-kH7GFR5Unpm77pKSEsmkqvzeQCRD7/MQ7upmeLLjg4XpQ62dX/R6THFsp6vkF1Pkco2aKUq9KhoLM4W5twdk7g==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
@@ -1100,8 +1079,8 @@ packages:
os: [linux]
libc: [glibc]
- '@napi-rs/canvas-linux-arm64-gnu@1.0.9':
- resolution: {integrity: sha512-012oiYtKaE7i9oxc8q7nraT7kDOpLcaCmFLzVe9Ty34RHDdoDzbWLrVh827CNxYh/EADX1eSikA3ymLjo/nNuw==}
+ '@napi-rs/canvas-linux-arm64-gnu@1.0.10':
+ resolution: {integrity: sha512-0/Hj7IwM9pmzdwxiqkp4YLJ8dKqASBrWQ3Y7dWUSU/4dgvCL9O6YNvX7xVUJjonQvdD3IE9ESS/nXa5srQqoVw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1114,8 +1093,8 @@ packages:
os: [linux]
libc: [musl]
- '@napi-rs/canvas-linux-arm64-musl@1.0.9':
- resolution: {integrity: sha512-Ls5UWYFFn63casTZEczbeyEg3vRDRkv9lscuGwfchtY5yLLQhgOB8SN4YGxmfJ5vTaBwZ2YUxBS3NtmjJmFXdA==}
+ '@napi-rs/canvas-linux-arm64-musl@1.0.10':
+ resolution: {integrity: sha512-fmjKM0P2MLF9O6XzCQsRjhB+jlLzaf6tFQU36hMPMsYup+VThYvpjjhqNLOc/Ee3tHUE3r13H2BSJx/yXz4pFA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1128,8 +1107,8 @@ packages:
os: [linux]
libc: [glibc]
- '@napi-rs/canvas-linux-riscv64-gnu@1.0.9':
- resolution: {integrity: sha512-hLKEGxV7ZiRHqndePTokgDMdBlo/rDfzg7P4p4QIv9pUhuYobnu3R2NIFLCRghG0nwfo+s2sw+c1xZFeCmEAsw==}
+ '@napi-rs/canvas-linux-riscv64-gnu@1.0.10':
+ resolution: {integrity: sha512-O/BUrGwrs3pVP9PpD/3++ZiOpjHMJlmiB7aBgswP8rbe4egPfiFkFxmcCczoLvZa72TaQX5lJ8Udedb6rc/Hvg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
@@ -1142,8 +1121,8 @@ packages:
os: [linux]
libc: [glibc]
- '@napi-rs/canvas-linux-x64-gnu@1.0.9':
- resolution: {integrity: sha512-6kaz3w0QMy77PDWk6rJ1ksIihdad3qzEyX2o2oGT8GwCaypfT5mhjr8buOO5hstyLxcWXDScuz56RsINLtBPIQ==}
+ '@napi-rs/canvas-linux-x64-gnu@1.0.10':
+ resolution: {integrity: sha512-48HkZPQeAN/R+9NPpY64tceoyCUW5xYYtHKZnC+BG11qiihXJCbH+xfbgGU+OdYp1Q4s84HDl9ILU0KBK6SBOQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1156,15 +1135,15 @@ packages:
os: [linux]
libc: [musl]
- '@napi-rs/canvas-linux-x64-musl@1.0.9':
- resolution: {integrity: sha512-xrGvmS3v55hmZ86ls/kBLVNMUTYio3f6Ik0DireemG994VfPAwiA3ZXA0Uf1bByctkB3NQ1Sfb+H5bkdUnnzfQ==}
+ '@napi-rs/canvas-linux-x64-musl@1.0.10':
+ resolution: {integrity: sha512-QYHVi7WZ9v+Wm9OQyHLMTnJ8QiBPVX94BgbCHFJC/4bCFMfvX7fRzo4FJPgkRP0ISz3ulYNow/FCbQOPjzhYjg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@napi-rs/canvas-win32-arm64-msvc@1.0.9':
- resolution: {integrity: sha512-yjmVS3ArZeRVCP7jqbPq4rpZa/BhTeI7ELE2XqJg3snICQBDevLZyArxswHkiTnT34KRic33/4fLirrHI+SY8A==}
+ '@napi-rs/canvas-win32-arm64-msvc@1.0.10':
+ resolution: {integrity: sha512-bWK+YajM/8sL0mMgLtug2s063FVBPzFV4QAn987CQomMKlmzrBY6Trz43KY+24hq7IOa3qNhUAgP/XMEF50ZEQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -1175,8 +1154,8 @@ packages:
cpu: [x64]
os: [win32]
- '@napi-rs/canvas-win32-x64-msvc@1.0.9':
- resolution: {integrity: sha512-QlSYQdMQslB81nlABo9wNfQ6npFhE7/O+saCZdqVGueGanyRk4jCogD5EwQenfP3kIq9e+mm6GreQBjX5MrA8g==}
+ '@napi-rs/canvas-win32-x64-msvc@1.0.10':
+ resolution: {integrity: sha512-9WtzW85PaIhtB27iXaR/9RrLCqHLbqD3Z8Q+kh17NjFxwQXFXU5vWbYH4vEH+drM307TTyK3hidZgpt09G5T3g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1185,17 +1164,10 @@ packages:
resolution: {integrity: sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==}
engines: {node: '>= 10'}
- '@napi-rs/canvas@1.0.9':
- resolution: {integrity: sha512-QviPdJImDi/jMAvBfqaw+19BndMd/sizXVW3NnpMd3VJGz++QXkOHcP9kWR/smHG0hNjHeyuHFyrx/5lD0oNcQ==}
+ '@napi-rs/canvas@1.0.10':
+ resolution: {integrity: sha512-V+qptzsGnPMFxEbhwc6eDNMe5eDrfiBIp8qGpuRjAVIX4Kn2zxvwQ1Xco6Fwe+3tYIe3iLW8LM9D+SvjyJhqbQ==}
engines: {node: '>= 10'}
- '@napi-rs/wasm-runtime@1.2.3':
- resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==}
- engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
- peerDependencies:
- '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
- '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
-
'@napi-rs/wasm-runtime@1.2.4':
resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==}
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
@@ -1203,155 +1175,155 @@ packages:
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
- '@next/bundle-analyzer@16.3.3':
- resolution: {integrity: sha512-pJFJrjUnxac2RBEgRIhXqoRkaSxMGQEGvvLhPKA2h1hXAzq60DzRj8Vp8+um2cMuHRuLcwRFYNlcuS74/hZ7AQ==}
+ '@next/bundle-analyzer@16.3.8':
+ resolution: {integrity: sha512-8VBk5/pjANz7fxbQZrDwUMTAs598Vckb4PE7BSYrDIPthJskKCXEHOK+WEMgDD6X2xWlKjrXPo2r41N/G1azSw==}
- '@next/env@16.3.6':
- resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==}
+ '@next/env@16.3.8':
+ resolution: {integrity: sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==}
- '@next/eslint-plugin-next@16.3.6':
- resolution: {integrity: sha512-jowwDX+7DOlDIjJLgTMxudw+k37QnWu1JkZLkSi9MaJBfDYcfhAPMKBhXL0idYzFN/AGg//axnOR4cLkHX/Rng==}
+ '@next/eslint-plugin-next@16.3.8':
+ resolution: {integrity: sha512-eCR9RcLTZrVS+K3+VOhi0xMdUUGNuriCHHhU1ZoABC/QQ/Gp0OWbLKQxV2/9DkmihYdYRcQWFmZXW8M1AMUXYw==}
- '@next/swc-darwin-arm64@16.3.6':
- resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==}
+ '@next/swc-darwin-arm64@16.3.8':
+ resolution: {integrity: sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.3.6':
- resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==}
+ '@next/swc-darwin-x64@16.3.8':
+ resolution: {integrity: sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.3.6':
- resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==}
+ '@next/swc-linux-arm64-gnu@16.3.8':
+ resolution: {integrity: sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-arm64-musl@16.3.6':
- resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==}
+ '@next/swc-linux-arm64-musl@16.3.8':
+ resolution: {integrity: sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@next/swc-linux-x64-gnu@16.3.6':
- resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==}
+ '@next/swc-linux-x64-gnu@16.3.8':
+ resolution: {integrity: sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-x64-musl@16.3.6':
- resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==}
+ '@next/swc-linux-x64-musl@16.3.8':
+ resolution: {integrity: sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@next/swc-win32-arm64-msvc@16.3.6':
- resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==}
+ '@next/swc-win32-arm64-msvc@16.3.8':
+ resolution: {integrity: sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.3.6':
- resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==}
+ '@next/swc-win32-x64-msvc@16.3.8':
+ resolution: {integrity: sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
- '@noble/hashes@2.2.0':
- resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==}
+ '@noble/hashes@2.4.0':
+ resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==}
engines: {node: '>= 20.19.0'}
- '@node-rs/argon2-android-arm-eabi@2.1.0':
- resolution: {integrity: sha512-hdWo5kb4eFRbHjdu4O6dVlRPI/CR1vbkJpe3Z9kF2s0Kp42428wg8AxI+8Cv4mygdW309BpUBf3sZaqXBNpdpw==}
+ '@node-rs/argon2-android-arm-eabi@2.2.2':
+ resolution: {integrity: sha512-c11BOrPLBWNC1cDyzGB+Bil+flmxfZbB2hv8EhcobmAsFKMyqkHPNhUY4qhcjv1U+4+Vmw4G8H+l224iO2g/Dw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
- '@node-rs/argon2-android-arm64@2.1.0':
- resolution: {integrity: sha512-MHby1n9UzlVma3GiEYWNNqRhn2Z/90QwF2PFckGwucdz0HfdVX+lSKEcyfwcbvS5sJ9GAzhop3hXlUPxUYe72Q==}
+ '@node-rs/argon2-android-arm64@2.2.2':
+ resolution: {integrity: sha512-dpMYIoxhN/wY6hFH+3MM5kQErcH3luMekkszbkDF3NKqhB1kFRntkOLDpFY87oItBSHJrsoYkXC3vBRhjYoCjQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
- '@node-rs/argon2-darwin-arm64@2.1.0':
- resolution: {integrity: sha512-5nayvL9wepOvTO5JmdaiDZ6bgHEsI/vksnXphM3UTy/cflARr9MoD1pBrMPs921hx2otURJhG0avg9hN0Q0KPw==}
+ '@node-rs/argon2-darwin-arm64@2.2.2':
+ resolution: {integrity: sha512-vxVAXcIG0iiHizkCuImmzVhOFsRnU4Z79xiOxdISLyI+QhOAWbbTaVSXw0PNfThB3zAdkzzDJ/WWu52Hs/qHGg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@node-rs/argon2-darwin-x64@2.1.0':
- resolution: {integrity: sha512-7bFf3qH/PLQ5kU5GkxMF+dMz/tiT+0yFXILpOx4fgSFjw3wdXpGZt3wH4i6dHMURDcwYlNap0hJaX4h2E3JGng==}
+ '@node-rs/argon2-darwin-x64@2.2.2':
+ resolution: {integrity: sha512-r7GbSFpFZZ9EAV9cvIm9prUjOupK7QQXTyP+FJ6bWSMq0h+qRDBlUOXQXduDIIrLEGoBVelixKUtf4ksZ0ljoQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@node-rs/argon2-freebsd-x64@2.1.0':
- resolution: {integrity: sha512-01mVdl+7bjSW0to5xcEzueq7BuYbz3476/dmLMXziJf8fhykzr723YRNcGjOcHSU5fxNrElY9uVeUoow16vBrQ==}
+ '@node-rs/argon2-freebsd-x64@2.2.2':
+ resolution: {integrity: sha512-AmWxDSmQgprilD6NX3jmxCgcDvWAW7kGJnnWQo+0qav6nfMXdPZYgSezK/skmw1GBWipr4yZMIUzpeNHnVpMxg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
- '@node-rs/argon2-linux-arm-gnueabihf@2.1.0':
- resolution: {integrity: sha512-NxZILF+Hqav6yzJTUFuZXTUC1VagTO4Vfd2HSlzkwankUa1B7goo0NLY2tUPw6MdqtLojkqVG5MZ26AaF49tHw==}
+ '@node-rs/argon2-linux-arm-gnueabihf@2.2.2':
+ resolution: {integrity: sha512-M1gnPBTr9GFeEUWkQ53qCJeTsX03C3kcUeHnDbN7dU2xkI58wMP4Z1sEBN7WmKJ/fTHYEgti4truz7r4A82DLg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
- '@node-rs/argon2-linux-arm64-gnu@2.1.0':
- resolution: {integrity: sha512-jpx2VwJLyAF/cM64HRigFBk67GwtQBcssYyVhvHqfmCEZz/CP//SV9bxE+uJOGIPKJo7wDg/5f2k3JY0PNMROg==}
+ '@node-rs/argon2-linux-arm64-gnu@2.2.2':
+ resolution: {integrity: sha512-4CYDlIdITlLSVcAXYUtEoGUahRaayPXM+4Rcfg3BZx9WGi6UB8HUycs6GNMTPYXV5kVXCpyE2u1HmYUpgmIzOQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@node-rs/argon2-linux-arm64-musl@2.1.0':
- resolution: {integrity: sha512-xXAaPUlnPx/44qwaSekTG/g6/h5QCyrTayPLmvewJNyYz17vyBRmFKWLgv9Q3+NJwSvV88nVnCu/xkQbEPQaog==}
+ '@node-rs/argon2-linux-arm64-musl@2.2.2':
+ resolution: {integrity: sha512-FYlfj3XgQ1xrx6HzQbKbUcKiYMG/3iSi/X81KyhHaItTJ7vZlMwrbVLl9yqDmvotKUF6VjrhrlZ8KRbbzchseA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@node-rs/argon2-linux-x64-gnu@2.1.0':
- resolution: {integrity: sha512-ICnfyFaxZzr8OdAEwTPsAjx6gpNla1sI3miZJWAVCtZlw98Wo3Wd4H7tvW6S2NRJ1BIOf4l8Z7aX7h3fimXn5w==}
+ '@node-rs/argon2-linux-x64-gnu@2.2.2':
+ resolution: {integrity: sha512-ULXLs8UFYZANlwiROM7AeccxZndr2Xk3IstADq3jAUhnwV2W75mxMBTABUZ+7AdKRhPLOrxU77OgnMWlDkYJQg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@node-rs/argon2-linux-x64-musl@2.1.0':
- resolution: {integrity: sha512-f56jehb/IPTGBWWrvMjDGZWlQm/hKkH2VK/MxJ9u3kUR9PCacdtLFZmX4sL+qKeo4wsyhlD1EvlZF6nV1FSi3Q==}
+ '@node-rs/argon2-linux-x64-musl@2.2.2':
+ resolution: {integrity: sha512-zgjIwc9TyGodn62vGc+36DGwFqVV5e7L8Ptd2FQWuBxe5aFrcFxvtDGn1/qfYwoHLv09utgtR+RBAPCXepWG3Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@node-rs/argon2-win32-arm64-msvc@2.1.0':
- resolution: {integrity: sha512-KH8lBdjoZLUiVG1GjJQIQPKhmddJmEbJbL+e3S7BKVq1wKl1mx+ClmhGtMtYp8ZtEIw8/BYIFUIjL3sBuWGEuQ==}
+ '@node-rs/argon2-win32-arm64-msvc@2.2.2':
+ resolution: {integrity: sha512-3nLhvLie/ec5xfxUBXXqGMb7oGeqF+YXrcGckqaTaGkPtOBozho0Kh2IuP/zi9BcOriirTG2LW3gtIOfpuh+mQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@node-rs/argon2-win32-ia32-msvc@2.1.0':
- resolution: {integrity: sha512-zZ7NDHoKTgrjoUehNN7xvP+dA/M9krOp42l0MKns6TnMkwrp5nes9agEZsUy+0t0JoR8OQ8D7EUCNLh5BEn++w==}
+ '@node-rs/argon2-win32-ia32-msvc@2.2.2':
+ resolution: {integrity: sha512-6RJKD6cFl7UeiYJLjRsXPywVXSFSaSDdr6xGpztNDV31QHCEecWnsnNmKxzi4gVpln2F9XeWEtIgM4Nqw1hLbA==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
- '@node-rs/argon2-win32-x64-msvc@2.1.0':
- resolution: {integrity: sha512-JN/yNiX7u8Cw9XXTbRrjnlgzFjCiEQe6x97wJnMIsJ3vty89q0SAg504Otd9qy2ofQS2F5CCWvrOXhm/oyHaAQ==}
+ '@node-rs/argon2-win32-x64-msvc@2.2.2':
+ resolution: {integrity: sha512-dQOO9S4QKBaVul0Uyk/2quJc/OBeHNZRJP+GZZTy1A5kYIaAfIJGHUYLY6oFoAlcuLIkdFouLaHX3fCl8tZm/w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
- '@node-rs/argon2@2.1.0':
- resolution: {integrity: sha512-VBOWfM2u58/to3DFqTGJ2U5cJKQwmjN2zxzsQNZ5a2o8Z6aUrhvqQh8NdgotIF1Y0tMsBNtzOBDBdfvvkwJDSQ==}
+ '@node-rs/argon2@2.2.2':
+ resolution: {integrity: sha512-NtdlPPWtR5U4BxX5EGJYza+UklNaoWORmkaKCMqf/nyKeXWKVwNt8KfQoRpNT7OB1DcEwmePafO4wZyW+sbfCQ==}
engines: {node: '>= 10'}
'@nodelib/fs.scandir@2.1.5':
@@ -1382,124 +1354,128 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
- '@oxc-parser/binding-android-arm-eabi@0.143.0':
- resolution: {integrity: sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==}
+ '@opentelemetry/api@1.9.1':
+ resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
+ engines: {node: '>=8.0.0'}
+
+ '@oxc-parser/binding-android-arm-eabi@0.150.0':
+ resolution: {integrity: sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
- '@oxc-parser/binding-android-arm64@0.143.0':
- resolution: {integrity: sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==}
+ '@oxc-parser/binding-android-arm64@0.150.0':
+ resolution: {integrity: sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@oxc-parser/binding-darwin-arm64@0.143.0':
- resolution: {integrity: sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==}
+ '@oxc-parser/binding-darwin-arm64@0.150.0':
+ resolution: {integrity: sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@oxc-parser/binding-darwin-x64@0.143.0':
- resolution: {integrity: sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==}
+ '@oxc-parser/binding-darwin-x64@0.150.0':
+ resolution: {integrity: sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@oxc-parser/binding-freebsd-x64@0.143.0':
- resolution: {integrity: sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==}
+ '@oxc-parser/binding-freebsd-x64@0.150.0':
+ resolution: {integrity: sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@oxc-parser/binding-linux-arm-gnueabihf@0.143.0':
- resolution: {integrity: sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==}
+ '@oxc-parser/binding-linux-arm-gnueabihf@0.150.0':
+ resolution: {integrity: sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxc-parser/binding-linux-arm-musleabihf@0.143.0':
- resolution: {integrity: sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==}
+ '@oxc-parser/binding-linux-arm-musleabihf@0.150.0':
+ resolution: {integrity: sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxc-parser/binding-linux-arm64-gnu@0.143.0':
- resolution: {integrity: sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==}
+ '@oxc-parser/binding-linux-arm64-gnu@0.150.0':
+ resolution: {integrity: sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-arm64-musl@0.143.0':
- resolution: {integrity: sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==}
+ '@oxc-parser/binding-linux-arm64-musl@0.150.0':
+ resolution: {integrity: sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@oxc-parser/binding-linux-ppc64-gnu@0.143.0':
- resolution: {integrity: sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==}
+ '@oxc-parser/binding-linux-ppc64-gnu@0.150.0':
+ resolution: {integrity: sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-riscv64-gnu@0.143.0':
- resolution: {integrity: sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==}
+ '@oxc-parser/binding-linux-riscv64-gnu@0.150.0':
+ resolution: {integrity: sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-riscv64-musl@0.143.0':
- resolution: {integrity: sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==}
+ '@oxc-parser/binding-linux-riscv64-musl@0.150.0':
+ resolution: {integrity: sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
- '@oxc-parser/binding-linux-s390x-gnu@0.143.0':
- resolution: {integrity: sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==}
+ '@oxc-parser/binding-linux-s390x-gnu@0.150.0':
+ resolution: {integrity: sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-x64-gnu@0.143.0':
- resolution: {integrity: sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==}
+ '@oxc-parser/binding-linux-x64-gnu@0.150.0':
+ resolution: {integrity: sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-x64-musl@0.143.0':
- resolution: {integrity: sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==}
+ '@oxc-parser/binding-linux-x64-musl@0.150.0':
+ resolution: {integrity: sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@oxc-parser/binding-openharmony-arm64@0.143.0':
- resolution: {integrity: sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==}
+ '@oxc-parser/binding-openharmony-arm64@0.150.0':
+ resolution: {integrity: sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
- '@oxc-parser/binding-win32-arm64-msvc@0.143.0':
- resolution: {integrity: sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==}
+ '@oxc-parser/binding-win32-arm64-msvc@0.150.0':
+ resolution: {integrity: sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@oxc-parser/binding-win32-ia32-msvc@0.143.0':
- resolution: {integrity: sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==}
+ '@oxc-parser/binding-win32-ia32-msvc@0.150.0':
+ resolution: {integrity: sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
- '@oxc-parser/binding-win32-x64-msvc@0.143.0':
- resolution: {integrity: sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==}
+ '@oxc-parser/binding-win32-x64-msvc@0.150.0':
+ resolution: {integrity: sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -1507,8 +1483,8 @@ packages:
'@oxc-project/types@0.139.0':
resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==}
- '@oxc-project/types@0.143.0':
- resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==}
+ '@oxc-project/types@0.150.0':
+ resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
'@oxc-resolver/binding-android-arm-eabi@11.24.2':
resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==}
@@ -1680,8 +1656,8 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
- '@playwright/test@1.63.0':
- resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==}
+ '@playwright/test@1.64.0':
+ resolution: {integrity: sha512-oTN9FNYuN6bqQMwiho6jJKLCG2/QOYCixGXuCjRmzOq23eODeukuY2XiAVoVpAECbuezMt1MHQjAiQxK8F+Y+g==}
engines: {node: '>=20'}
hasBin: true
@@ -1787,8 +1763,8 @@ packages:
'@radix-ui/primitive@1.1.7':
resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==}
- '@radix-ui/react-accessible-icon@1.1.15':
- resolution: {integrity: sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==}
+ '@radix-ui/react-accessible-icon@1.1.16':
+ resolution: {integrity: sha512-I+T/Hc6SY3WbFVZ9Ne1o0DJbpjQpTpLIBZcVzYiHCQ9z1yGUd/G88u5SKyM6eJ223txAOS7bp7bb9k0Ocfkjwg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1800,8 +1776,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-accordion@1.2.20':
- resolution: {integrity: sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==}
+ '@radix-ui/react-accordion@1.2.21':
+ resolution: {integrity: sha512-2fmc6COuKLq2k3P7JZrOt9Zf8HT9nTQhHN5V+A3ih7ftqoyDcoEUrPoLq8ngr7QwTd7hpQ2xoUKWtvy1Xw9RAQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1813,8 +1789,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-alert-dialog@1.1.23':
- resolution: {integrity: sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==}
+ '@radix-ui/react-alert-dialog@1.1.24':
+ resolution: {integrity: sha512-dRvB59HhcjIWcCPKzK5Px1yQ8pIULQQrxqgyepCqin7tNXi/xvmC86WkyMOKhuCbjz3+v9Q6oE7/tUN5NQ9dCg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1826,8 +1802,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-arrow@1.1.15':
- resolution: {integrity: sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==}
+ '@radix-ui/react-arrow@1.1.16':
+ resolution: {integrity: sha512-nXPI1tzJsnQw8oXvB2r49rO9TIbbJ8VFeaWDiBsqY8+iwO5xb7WjUsrXDagjAgPKTdhW8WODQnAltUhBx8I+sg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1839,8 +1815,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-aspect-ratio@1.1.15':
- resolution: {integrity: sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==}
+ '@radix-ui/react-aspect-ratio@1.1.16':
+ resolution: {integrity: sha512-w2u6oiTzb7WkWwV3ZClX/llYYlTbilUOkHIKkwb+PWWytQ/IdEVRNLIChjbcXy/mkqcKKM3wEhrOilJW+uPHDg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1852,8 +1828,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-avatar@1.2.6':
- resolution: {integrity: sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==}
+ '@radix-ui/react-avatar@1.2.7':
+ resolution: {integrity: sha512-6AizCpbU3Zngojp9vQak8Lt4AXwdKJ2lkRYAfpoETraT+qKVNeRqytiADzneHetFEB0Wu4+qeSfmZEq0PiVDmQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1865,8 +1841,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-checkbox@1.3.11':
- resolution: {integrity: sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==}
+ '@radix-ui/react-checkbox@1.3.12':
+ resolution: {integrity: sha512-gNOS2lLrpQxNLWHhCX0sayfVRiyB5gQp7nQIzpl+PM1Hkab52utsKtD27alAr78uqhpvX51kwqDp71hM5hNSwg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1878,8 +1854,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-collapsible@1.1.20':
- resolution: {integrity: sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==}
+ '@radix-ui/react-collapsible@1.1.21':
+ resolution: {integrity: sha512-IzkusZHCZBnED5skW+b+syl8d+e3+zelvxZ2Ed9lfawqEBfa5osPj53sTJ+q/uNmGAj9e7wMYgyzwPu/luWJ4Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1891,8 +1867,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-collection@1.1.15':
- resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==}
+ '@radix-ui/react-collection@1.1.16':
+ resolution: {integrity: sha512-yEQrjfI5eBbeYEJXOASpm+gs9eNHY8+UjkZ81GzcuwdLrSMOlEM8jnGoGdeGo92oiBMkuwZUhHTsnsrDiJLcyA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1922,8 +1898,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-context-menu@2.3.7':
- resolution: {integrity: sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==}
+ '@radix-ui/react-context-menu@2.3.8':
+ resolution: {integrity: sha512-s8ABBkEuAOyhDub0Y99f8lVRlVsd28mUnOHtuqGg8LuHnefkmPN5rYzA2g8MAPLjAZMisnDHhN4dBHUM7ZF7Rg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1944,8 +1920,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-dialog@1.1.23':
- resolution: {integrity: sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==}
+ '@radix-ui/react-dialog@1.2.0':
+ resolution: {integrity: sha512-l5CX8HdNL+9cc4s7HkNyplumSA3FIx68sg9dttBHKr286fRBwq1MBniDVTeI/tkof92/APntMQvC1YG1Khb0Cg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1957,8 +1933,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-direction@1.1.4':
- resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==}
+ '@radix-ui/react-direction@1.1.5':
+ resolution: {integrity: sha512-/NlO/rOqQabmg+GGr86vCmbJVhqTv85NfM9g6Gknf/JVygXeZ6e6HM/RW1omerW9HsFtecwNqgEAKV6UqDsvpA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
@@ -1966,8 +1942,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-dismissable-layer@1.1.19':
- resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==}
+ '@radix-ui/react-dismissable-layer@1.1.20':
+ resolution: {integrity: sha512-35WHRrycfnj2m4LvPiKMKHGLM3Y2WvUA744CkRdxjs1BGDX1c8PVMfmiSGTUwnHRbqdtIoxveYTXvcJSt7VfgQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -1979,8 +1955,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-dropdown-menu@2.1.24':
- resolution: {integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==}
+ '@radix-ui/react-dropdown-menu@2.1.25':
+ resolution: {integrity: sha512-45QO76jn/a+cRl6OPgZKCKnIMdfYZaOa6SuAJeKHFiM3orX6bnscuFQJDx3qS4Agp/RE+eKv+9gmUL8t3Ma7bw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2001,8 +1977,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-focus-scope@1.1.16':
- resolution: {integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==}
+ '@radix-ui/react-focus-scope@1.2.0':
+ resolution: {integrity: sha512-I5S7vsAOozx2lWSBAyuXjWIJ/A8qx4fjbv6115WoaRYQSb1yZDaeNHonUAQzXyE8oD+0whJHbQRl4HIE2gQ0Xw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2014,8 +1990,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-form@0.1.16':
- resolution: {integrity: sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==}
+ '@radix-ui/react-form@0.2.0':
+ resolution: {integrity: sha512-5ELeC+bQUHXEjU51svw11DynjGFlaUjo5QOUCr5hptvLLtBiqWtva2O3IWdjrFWQwitw6k6KpHHEyI61/kCVeg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2027,8 +2003,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-hover-card@1.1.23':
- resolution: {integrity: sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==}
+ '@radix-ui/react-hover-card@1.1.24':
+ resolution: {integrity: sha512-0w/tZHRo1fv4WsfcyaOdi5eDftFA8wQkr8WCOOuxw7fx9S2ERfshQioM269aFZsmyAHd0X1LWoZ2BQTlA/JYUw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2049,8 +2025,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-label@2.1.15':
- resolution: {integrity: sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==}
+ '@radix-ui/react-label@2.1.16':
+ resolution: {integrity: sha512-2vTQj72YFmeoTxWKJEMbO/9uQt6zzLgbzi0+nWBffq49oo6KhIDP/D+fA2im0wsxBCAX7QfaF3xgsYBVdDbNFQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2062,8 +2038,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-menu@2.1.24':
- resolution: {integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==}
+ '@radix-ui/react-menu@2.1.25':
+ resolution: {integrity: sha512-e8JaQc0ijBbTx37YVI09Nfq8ThUWJ4L1rvZ3Hvaufehco9/ug9+U3PHz9ue6w3gKd91UcKf9fgDWejeSO+VSKQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2075,8 +2051,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-menubar@1.1.24':
- resolution: {integrity: sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==}
+ '@radix-ui/react-menubar@1.1.25':
+ resolution: {integrity: sha512-JIQiKcXsgl6BtwcMe7BMLvCGH2p9Czgcmxl6Ie9Zoi+MW1mmitkcm5We01m/ndLYVI8PpRJnM/RcLV0eOCxaJQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2088,8 +2064,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-navigation-menu@1.2.22':
- resolution: {integrity: sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==}
+ '@radix-ui/react-navigation-menu@1.3.0':
+ resolution: {integrity: sha512-hZqTqDwc8Z5aI4rGIyPqCSJ2JoSNorqX/F1/gqNHkZzWJO6JuYzL7hn5sWOZJjpkfT+cbxWWRbKVU/IeiOM9ag==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2101,8 +2077,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-one-time-password-field@0.1.16':
- resolution: {integrity: sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==}
+ '@radix-ui/react-one-time-password-field@0.1.17':
+ resolution: {integrity: sha512-+6jmOqjSNBr7F1uSc7HSDqD3XL54A2qA99I9rIH4JoOD/jDScw8tUmDz6HCAkjytbrE/cdL66i+QkeDyZ0WNyw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2114,8 +2090,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-password-toggle-field@0.1.11':
- resolution: {integrity: sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==}
+ '@radix-ui/react-password-toggle-field@0.1.12':
+ resolution: {integrity: sha512-+SWSgf3HRpE4zUira+p6t5KUR473PSqewtQgj9qzl3znZPRWPCGycwXNTfHwkiRTD3tEXwNIXJg0i1NTteiULw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2127,8 +2103,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-popover@1.1.23':
- resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==}
+ '@radix-ui/react-popover@1.2.0':
+ resolution: {integrity: sha512-e70Nh+0iQ+hRAIM2nAGib5BhaDL2Rjlzxw9kIzsU2nz+t3/t82Z5qtPhY5nTnfj8FbmrU6RRIKH8RF3ynCe+vw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2140,8 +2116,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-popper@1.3.7':
- resolution: {integrity: sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==}
+ '@radix-ui/react-popper@1.3.8':
+ resolution: {integrity: sha512-y92/9iIdG1FyNvow0Y0qP5WJZ7oRTAKDVKCy9kJnhZfItwIKYtt0KKQeOhBKfXz0ETizduKwDRbrQHrTQScs5A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2153,8 +2129,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-portal@1.1.17':
- resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==}
+ '@radix-ui/react-portal@1.1.18':
+ resolution: {integrity: sha512-ygFIJ+QXNKf72AAq8x3hd22+ws7pYpmc1S0E4kW0Je+qD4qEb3FvxeKVQRGrVDMOkQaYfsB+Qs+KDh81a1gAOw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2166,8 +2142,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-presence@1.1.10':
- resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==}
+ '@radix-ui/react-presence@1.1.11':
+ resolution: {integrity: sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2179,8 +2155,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-primitive@2.1.10':
- resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==}
+ '@radix-ui/react-primitive@2.1.11':
+ resolution: {integrity: sha512-RzcaSw5aKl0OPhBJ1wbolKSF6nVvN+0o/QLeIVq+09npiPdnCkPggy2k6gI6yetxl4ZyrlBPYFgCB8BQJZwXcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2205,8 +2181,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-progress@1.1.16':
- resolution: {integrity: sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==}
+ '@radix-ui/react-progress@1.1.17':
+ resolution: {integrity: sha512-A+RfMARd9u+UaVbLQDmysGlAGuaFQrF+uxF3v+7+xU8tkKoYxtCSs+6HHnIKJVgam2sGZ7c0hLRPWSnmnFyGJQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2218,8 +2194,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-radio-group@1.4.7':
- resolution: {integrity: sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==}
+ '@radix-ui/react-radio-group@1.4.8':
+ resolution: {integrity: sha512-MaOOZ1TlaYoOeSDnHeVIONss0rp5ngOTY5HJY+Xkq16PAopnj21iU9r9m0O/77cBYu8Ksaq3BcjlaOdpbfbzzg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2231,8 +2207,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-roving-focus@1.1.19':
- resolution: {integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==}
+ '@radix-ui/react-roving-focus@1.1.20':
+ resolution: {integrity: sha512-+ZjbkU21fTSJdn4nq/IFKUdKS+JSZHORBZ8ma1f6YpOMNzRt0xf3sVAWVK/+ktHCajPy4HYDsyHXZIZmOMxzIA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2244,8 +2220,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-scroll-area@1.2.18':
- resolution: {integrity: sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==}
+ '@radix-ui/react-scroll-area@1.3.0':
+ resolution: {integrity: sha512-1WSuAXaahfeLmFX/dfYo/ftg1filMfaWjVye2d40VlgtWMMq/NOVocfukZEXkG+dwrAoNI+qpo9bjhNSJ/XC8g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2257,8 +2233,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-select@2.3.7':
- resolution: {integrity: sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==}
+ '@radix-ui/react-select@2.3.8':
+ resolution: {integrity: sha512-Tz9xOpV9Rbca5WTk7pzmfAECV3zb++uScT9mP3Px7ziAhH7SlK53b0uwMw8VubPhRK4x7uDKCy5fwevT8I0fAg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2270,8 +2246,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-separator@1.1.15':
- resolution: {integrity: sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==}
+ '@radix-ui/react-separator@1.1.16':
+ resolution: {integrity: sha512-vugPjGQBXbJVVkftKqv/BVeZ108SyRGT3FR1oa14az6qyLgX8jPhnZYHgdSB7Gw8UJusqwb9eaK6mluOnsn/Vg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2283,8 +2259,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-slider@1.4.7':
- resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==}
+ '@radix-ui/react-slider@1.5.0':
+ resolution: {integrity: sha512-ZOBLK7KZvGL02V+4VwVbj8NYM4vPrAtyZdrgHSv+AMS9VL+5yAdnQF3DZT4Y8cbCp1Yn4xdp92orvez8fJa77w==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2305,8 +2281,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-slot@1.3.3':
- resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==}
+ '@radix-ui/react-slot@1.4.0':
+ resolution: {integrity: sha512-IuMVHZ+Ah5fkctu3X3qeFrV/ZrBcuWuDTFUUPfS/L5hvoTjzhp/HdWGE14CsQombPdEidsa33HVlYZXi49PZ1Q==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
@@ -2314,8 +2290,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-switch@1.3.7':
- resolution: {integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==}
+ '@radix-ui/react-switch@1.3.8':
+ resolution: {integrity: sha512-rr+zuoAeHWK3m6H4HgjOUiu3VsrQ/9JkjwY/uMnTgHJGPe9WNzDP/MOuIV7S+65p42DIxjGL0hAHktqBq82+2A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2327,8 +2303,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-tabs@1.1.21':
- resolution: {integrity: sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==}
+ '@radix-ui/react-tabs@1.1.22':
+ resolution: {integrity: sha512-GmVH+P3wx1ZPMuH7WajQzygZIbqgnsb5h8z4Ow3sQMuDqgcqISSBNom6MLVNJvfrkuJTtqfXc3ZolHjX+w0CHg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2340,8 +2316,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-toast@1.2.23':
- resolution: {integrity: sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==}
+ '@radix-ui/react-toast@1.2.24':
+ resolution: {integrity: sha512-XR9pDEz9CH0ez98zPXQZOpF46anvbarkoOpjXpPSX7gm21vEl5Tzoe1PSQd3EY1OTOHac4lNq+TPx/2Z7jObdQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2353,8 +2329,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-toggle-group@1.1.19':
- resolution: {integrity: sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==}
+ '@radix-ui/react-toggle-group@1.1.20':
+ resolution: {integrity: sha512-O+Sjiv64DfnByHYtnKEXmn63uUskU/X61vjf69X1v8JFn/ZvuNNLdJTBfYurVTSDv4/RJnJGtGgb/uvuS/rRJw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2379,8 +2355,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-toggle@1.1.18':
- resolution: {integrity: sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==}
+ '@radix-ui/react-toggle@1.1.19':
+ resolution: {integrity: sha512-l7FQHtuZjUJ0vKLdhKu5LzZ8Jw6K60QEC1tC22QNLuXN2PkydSoqEhKTCqwxBFZJ52NTCjzudDvl9sUsZeUbPw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2392,8 +2368,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-toolbar@1.1.19':
- resolution: {integrity: sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==}
+ '@radix-ui/react-toolbar@1.1.20':
+ resolution: {integrity: sha512-5gyzL6B5/zjBwU3ATBWAVZ4xXQC5F8E9CB4BmYC1UvZ08pN2OJQoooeIwJtZZ/2rz4sTiQmbGfdDvmU73wRJCQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2405,8 +2381,8 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-tooltip@1.2.16':
- resolution: {integrity: sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==}
+ '@radix-ui/react-tooltip@1.3.0':
+ resolution: {integrity: sha512-4HM4b1Cft1CBXCGDRDG3S/fh8GedBuVOMNh145wMIfTbVqcuv+tyj03nZqYgFevWJMYo58H0s+8P6s6NaUHWtA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2517,8 +2493,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-use-size@1.1.4':
- resolution: {integrity: sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==}
+ '@radix-ui/react-use-size@1.1.5':
+ resolution: {integrity: sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
@@ -2526,8 +2502,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-visually-hidden@1.2.11':
- resolution: {integrity: sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==}
+ '@radix-ui/react-visually-hidden@1.2.12':
+ resolution: {integrity: sha512-/iPLVHRBzKWvekN+5xaDmXw80f+nleYYceomVORrGjuJe0LF2SCbSH4f/LG4hu32q92QSuPZx4hmSACYBVBfzQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -2657,60 +2633,52 @@ packages:
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
+ '@shadcn/registry@0.1.3':
+ resolution: {integrity: sha512-82/OmKAVM9pChDHFVAsDP9PcimPGSKZGdbcdwdGGGcSI69ImpN4ZnqsfZ4hkOFEMal92t3y9djYJHMjO4PcVZA==}
+ engines: {node: '>=20.18.1'}
+
'@simplewebauthn/browser@14.0.0':
resolution: {integrity: sha512-1odWVqeEBTl7lJ9zMKLEsmTlnyrDO5iRcTvfMKKk1WThUnp/i8JJdffdj2icP+tty159s4PgwE3BiMoEW9NFow==}
- '@simplewebauthn/server@14.0.2':
- resolution: {integrity: sha512-g+m/xv9/8FRWsDY22CwsTYRzt3WwS9fJrR3P7eY3Ti01FXH5FQ36Xn55v6D+5tb6VajlqOj4hywP/Vxsb6a3Sg==}
+ '@simplewebauthn/server@14.0.3':
+ resolution: {integrity: sha512-D39/0fiDiYmGV+nraY9s63D1Ho2AX/waMLy22/9gUadTUuvbAnstfb4bpbHYbwZuV5ZLDYN1SmzDKuiHzKAA4Q==}
engines: {node: '>=20.0.0'}
'@sindresorhus/merge-streams@4.0.0':
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
- '@smithy/core@3.26.0':
- resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==}
- engines: {node: '>=18.0.0'}
-
'@smithy/core@3.33.3':
resolution: {integrity: sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==}
engines: {node: '>=18.0.0'}
- '@smithy/credential-provider-imds@4.4.2':
- resolution: {integrity: sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==}
+ '@smithy/core@3.35.1':
+ resolution: {integrity: sha512-i4YPS4B6ts7bjn7UwLnGjiZdprOvHvgGobFZsYK3GIY3E5hIqtj0rReU69BcTpGp+fvtraSNXeG1l+jtJvF55w==}
engines: {node: '>=18.0.0'}
- '@smithy/fetch-http-handler@5.5.2':
- resolution: {integrity: sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==}
+ '@smithy/core@3.35.2':
+ resolution: {integrity: sha512-ejTukBuP/Pg3t9N8Ux+0zKsXeHWm7p6W3HbXFOyltKasSxZwCFmvTut7kBdZSCF3m9y1bXXyVu6p2xuMFcsTyQ==}
engines: {node: '>=18.0.0'}
- '@smithy/is-array-buffer@2.2.0':
- resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==}
- engines: {node: '>=14.0.0'}
-
- '@smithy/node-http-handler@4.8.2':
- resolution: {integrity: sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==}
+ '@smithy/credential-provider-imds@4.5.2':
+ resolution: {integrity: sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==}
engines: {node: '>=18.0.0'}
- '@smithy/signature-v4@5.5.2':
- resolution: {integrity: sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==}
+ '@smithy/fetch-http-handler@5.8.0':
+ resolution: {integrity: sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==}
engines: {node: '>=18.0.0'}
- '@smithy/types@4.15.0':
- resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==}
+ '@smithy/node-http-handler@4.12.1':
+ resolution: {integrity: sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==}
engines: {node: '>=18.0.0'}
- '@smithy/types@4.18.0':
- resolution: {integrity: sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==}
+ '@smithy/signature-v4@5.7.4':
+ resolution: {integrity: sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==}
engines: {node: '>=18.0.0'}
- '@smithy/util-buffer-from@2.2.0':
- resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==}
- engines: {node: '>=14.0.0'}
-
- '@smithy/util-utf8@2.3.0':
- resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==}
- engines: {node: '>=14.0.0'}
+ '@smithy/types@4.19.0':
+ resolution: {integrity: sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==}
+ engines: {node: '>=18.0.0'}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -2813,22 +2781,22 @@ packages:
'@tailwindcss/postcss@4.3.3':
resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==}
- '@tanstack/query-core@5.103.2':
- resolution: {integrity: sha512-I8DkFXls5jXLqtm8+QpOhEmG07hIblhSZFzafg9wHsMSEvMzULy9hK17wU1T/ahfhMbtITJhbxutwwCoihkR7A==}
+ '@tanstack/query-core@5.104.1':
+ resolution: {integrity: sha512-TiRghcrGkUTE+M4JBxvpPSpBhxvIzQairw+cksrHYGr+2uwC77yW1SY9nYxnYRYOppvucf8rEg6b5byoz4rTpw==}
- '@tanstack/react-query@5.103.2':
- resolution: {integrity: sha512-B+fWiYZBc+0uUD5zDZAeLw9dKj7XEsdnuu6zRZ+no6LNKpeE3P3bJ+N6HINjcIlWR6fW3vUoTneEaGa2V6ehqw==}
+ '@tanstack/react-query@5.104.1':
+ resolution: {integrity: sha512-Zz70EgjahNI7aCz8BZxM3vUGU44ycxEAzMuCUjLeA7SVhhOkg3ZgPjPJPvHjs6Rmky/dtmnu1WzjGHNHmhCoZQ==}
peerDependencies:
react: ^18 || ^19
- '@tanstack/react-virtual@3.14.12':
- resolution: {integrity: sha512-EOJkBss/FRqzFrCpX8M96lIh+ZMc0jPt0LILhxWQ3tOHQ3XdVvuE0jceS2yZXpDtayNfpjeg2EnuYPm90CxIlA==}
+ '@tanstack/react-virtual@3.14.13':
+ resolution: {integrity: sha512-JbDTAwtzZ99aOeCrAfW5EsE5KSq5RWh6Af2dtFwyLIIk48Ja7vm6n4axu/43T3vfjFnEGJalxJ1wyYjSQD6bSg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- '@tanstack/virtual-core@3.17.10':
- resolution: {integrity: sha512-eMorKWIi2nekElu+8DgSMJR4iR+sTDeh+W8VjKiMH79yUfEDJ3W9aL/96/wwbxNjyV6EfN39HKszSxjd90zAqQ==}
+ '@tanstack/virtual-core@3.17.11':
+ resolution: {integrity: sha512-+ILjvtHup6Y2hzQ6YzwMgX1Q+oQpxEGOXCEsCNaPoIP0VxMbizIBTmYTDtkerkIQS8/CbP1BRuyt8V/8BCsy1g==}
'@tesseract.js-data/deu@1.0.0':
resolution: {integrity: sha512-3ter1p2gFAiCd5CMC+PodZ9YY2UDomkKj+ytlKeeZqOUmhLx1SDnYPf5zog0g5MBbxYveW+8omI3PUflqHfZ4Q==}
@@ -2836,15 +2804,12 @@ packages:
'@tesseract.js-data/eng@1.0.0':
resolution: {integrity: sha512-mbTumm6KQPUHyzTPQaF3ObXYnx0SqqfV2nabqFVQBwD6Kl7PhGSLSzOlfFTWy0P3BjghaSKA2W9GB19Jk+ZcTg==}
- '@testcontainers/postgresql@12.1.0':
- resolution: {integrity: sha512-Pjf2VSVNirEPfz36nidyrVAnZvc2YhajOznY4VgyEsvfTd5qiMNOuPq96drREvxAUtXl5SFLX7vXj7sSq4aTcA==}
+ '@testcontainers/postgresql@12.2.0':
+ resolution: {integrity: sha512-14oNBbKdqIkWTiw8XwP6YqWnuBebiij8m4fF03WTSHvNt5FfvQpGTLwIV3p07UpDnqWRFvY50YFzbiYdB+SWuA==}
'@ts-morph/common@0.27.0':
resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
- '@tybys/wasm-util@0.10.3':
- resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
-
'@tybys/wasm-util@0.10.4':
resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==}
@@ -2935,8 +2900,8 @@ packages:
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
- '@types/node@26.6.2':
- resolution: {integrity: sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==}
+ '@types/node@26.6.4':
+ resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==}
'@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
@@ -2970,8 +2935,8 @@ packages:
'@types/ssh2@0.5.52':
resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==}
- '@types/ssh2@1.15.5':
- resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==}
+ '@types/ssh2@1.15.6':
+ resolution: {integrity: sha512-oGdxhBqcRTwSTKFm+9EiKzkNVYRLEFkcW44lhguvBalGJbWfGnDt/ezwSUZc+SF9m9bMc3VyklNAtp7zICjS5w==}
'@types/statuses@2.0.6':
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
@@ -2988,63 +2953,63 @@ packages:
'@types/web-push@3.6.4':
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
- '@typescript-eslint/eslint-plugin@8.70.0':
- resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==}
+ '@typescript-eslint/eslint-plugin@8.71.0':
+ resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.70.0
+ '@typescript-eslint/parser': ^8.71.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/parser@8.70.0':
- resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==}
+ '@typescript-eslint/parser@8.71.0':
+ resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/project-service@8.70.0':
- resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==}
+ '@typescript-eslint/project-service@8.71.0':
+ resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/scope-manager@8.70.0':
- resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==}
+ '@typescript-eslint/scope-manager@8.71.0':
+ resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.70.0':
- resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==}
+ '@typescript-eslint/tsconfig-utils@8.71.0':
+ resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/type-utils@8.70.0':
- resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==}
+ '@typescript-eslint/type-utils@8.71.0':
+ resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/types@8.70.0':
- resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==}
+ '@typescript-eslint/types@8.71.0':
+ resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.70.0':
- resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==}
+ '@typescript-eslint/typescript-estree@8.71.0':
+ resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/utils@8.70.0':
- resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==}
+ '@typescript-eslint/utils@8.71.0':
+ resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/visitor-keys@8.70.0':
- resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
+ '@typescript-eslint/visitor-keys@8.71.0':
+ resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@unrs/resolver-binding-android-arm-eabi@1.12.2':
@@ -3202,13 +3167,13 @@ packages:
'@visx/vendor@4.0.0-alpha.0':
resolution: {integrity: sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==}
- '@vitejs/plugin-react@6.1.1':
- resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==}
+ '@vitejs/plugin-react@6.1.2':
+ resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
'@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
babel-plugin-react-compiler: ^1.0.0
- oxc-transform-react: ^0.145.0
+ oxc-transform-react: ^0.152.0
vite: ^8.0.0
peerDependenciesMeta:
'@rolldown/plugin-babel':
@@ -3218,20 +3183,25 @@ packages:
oxc-transform-react:
optional: true
- '@vitest/coverage-v8@4.1.5':
- resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==}
+ '@vitest/coverage-v8@5.0.3':
+ resolution: {integrity: sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==}
peerDependencies:
- '@vitest/browser': 4.1.5
- vitest: 4.1.5
+ '@vitest/browser': 5.0.3
+ vitest: 5.0.3
peerDependenciesMeta:
'@vitest/browser':
optional: true
- '@vitest/expect@4.1.11':
- resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
+ '@vitest/istanbul-lib-coverage@1.0.2':
+ resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==}
+ engines: {node: '>=22'}
- '@vitest/mocker@4.1.11':
- resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
+ '@vitest/istanbul-lib-report@1.0.2':
+ resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==}
+ engines: {node: '>=22'}
+
+ '@vitest/mocker@5.0.3':
+ resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -3241,26 +3211,8 @@ packages:
vite:
optional: true
- '@vitest/pretty-format@4.1.11':
- resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
-
- '@vitest/pretty-format@4.1.5':
- resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==}
-
- '@vitest/runner@4.1.11':
- resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
-
- '@vitest/snapshot@4.1.11':
- resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
-
- '@vitest/spy@4.1.11':
- resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
-
- '@vitest/utils@4.1.11':
- resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
-
- '@vitest/utils@4.1.5':
- resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==}
+ '@vitest/spy@5.0.3':
+ resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
@@ -3284,8 +3236,8 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
- acorn@8.18.0:
- resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
+ acorn@8.19.0:
+ resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -3323,8 +3275,8 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
- ansi-regex@6.2.2:
- resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ ansi-regex@6.4.0:
+ resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
@@ -3407,12 +3359,12 @@ packages:
ast-types-flow@0.0.8:
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
- ast-types@0.16.1:
- resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
+ ast-types@0.16.3:
+ resolution: {integrity: sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==}
engines: {node: '>=4'}
- ast-v8-to-istanbul@1.0.0:
- resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==}
+ ast-v8-to-istanbul@1.0.7:
+ resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==}
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
@@ -3444,8 +3396,8 @@ packages:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
- b4a@1.8.1:
- resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==}
+ b4a@1.9.0:
+ resolution: {integrity: sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==}
peerDependencies:
react-native-b4a: '*'
peerDependenciesMeta:
@@ -3459,16 +3411,16 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
- bare-events@2.9.1:
- resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==}
+ bare-events@2.9.2:
+ resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==}
peerDependencies:
bare-abort-controller: '*'
peerDependenciesMeta:
bare-abort-controller:
optional: true
- bare-fs@4.8.0:
- resolution: {integrity: sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==}
+ bare-fs@4.8.2:
+ resolution: {integrity: sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==}
engines: {bare: '>=1.28.0'}
peerDependencies:
bare-buffer: '*'
@@ -3476,11 +3428,11 @@ packages:
bare-buffer:
optional: true
- bare-path@3.1.1:
- resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==}
+ bare-path@3.1.2:
+ resolution: {integrity: sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==}
- bare-stream@2.13.3:
- resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==}
+ bare-stream@2.13.4:
+ resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==}
peerDependencies:
bare-abort-controller: '*'
bare-buffer: '*'
@@ -3493,8 +3445,8 @@ packages:
bare-events:
optional: true
- bare-url@2.5.2:
- resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==}
+ bare-url@2.5.4:
+ resolution: {integrity: sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
@@ -3503,8 +3455,13 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
- baseline-browser-mapping@2.11.11:
- resolution: {integrity: sha512-/yImnXwyTvgMkhgekLHok/Rx5vO6E0BmStWlSqKWMVm2a2ITuZ1Tn+9bgLS+gZRdZmWtd8nxuhHpdmCUOWsTQQ==}
+ baseline-browser-mapping@2.11.26:
+ resolution: {integrity: sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ baseline-browser-mapping@2.11.27:
+ resolution: {integrity: sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -3544,8 +3501,8 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- browserslist@4.28.7:
- resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==}
+ browserslist@4.29.3:
+ resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
@@ -3569,8 +3526,8 @@ packages:
resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==}
engines: {node: '>=10.0.0'}
- bundle-name@4.1.0:
- resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
+ bundle-name@4.1.1:
+ resolution: {integrity: sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==}
engines: {node: '>=18'}
byline@5.0.0:
@@ -3609,8 +3566,11 @@ packages:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
- caniuse-lite@1.0.30001806:
- resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==}
+ caniuse-lite@1.0.30001814:
+ resolution: {integrity: sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==}
+
+ caniuse-lite@1.0.30001815:
+ resolution: {integrity: sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==}
canvg@3.0.11:
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
@@ -3667,6 +3627,11 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
+ cn@0.2.6:
+ resolution: {integrity: sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==}
+ engines: {node: '>=20'}
+ hasBin: true
+
code-block-writer@13.0.3:
resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
@@ -3746,15 +3711,6 @@ packages:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
- cosmiconfig@9.0.2:
- resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==}
- engines: {node: '>=14'}
- peerDependencies:
- typescript: '>=4.9.5'
- peerDependenciesMeta:
- typescript:
- optional: true
-
cpu-features@0.0.10:
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
engines: {node: '>=10.0.0'}
@@ -3919,8 +3875,8 @@ packages:
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
engines: {node: '>=18'}
- default-browser@5.5.0:
- resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
+ default-browser@5.5.1:
+ resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==}
engines: {node: '>=18'}
define-data-property@1.1.4:
@@ -3966,8 +3922,8 @@ packages:
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
- docker-compose@1.4.2:
- resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==}
+ docker-compose@1.5.0:
+ resolution: {integrity: sha512-O+eD6c63+BJORUWddXA7uAlI6H1KLDPd/aS14k73nXYpFliM48C3xrrKB3sXsR1Fp/rHUZQe7+fJ+QA7+jepyw==}
engines: {node: '>= 6.0.0'}
docker-modem@5.0.7:
@@ -3993,6 +3949,11 @@ packages:
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
engines: {node: '>=12'}
+ dotenv@18.0.6:
+ resolution: {integrity: sha512-ZMVmohV6CvAJtj++pZNyggluUt6hbRdQAxc2+Z635RL6/AG/ruXv5lHnybUFoxMPL/yrTL/tsF4paZu2l7RA6Q==}
+ engines: {node: '>=12'}
+ hasBin: true
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -4012,8 +3973,8 @@ packages:
effect@3.20.0:
resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==}
- electron-to-chromium@1.5.399:
- resolution: {integrity: sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==}
+ electron-to-chromium@1.5.451:
+ resolution: {integrity: sha512-vUNKPrUzw8ByEMOxloq0a71iD/mPuPxVkbDm3G5YhnxTzOw/I6qAg2MUS0cYj/c267lWmw26yW4Vv7foxTxPLg==}
elkjs@0.11.1:
resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==}
@@ -4054,9 +4015,6 @@ packages:
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
- error-ex@1.3.4:
- resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
-
es-abstract-get@1.0.0:
resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==}
engines: {node: '>= 0.4'}
@@ -4077,8 +4035,8 @@ packages:
resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==}
engines: {node: '>= 0.4'}
- es-module-lexer@2.1.0:
- resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+ es-module-lexer@2.3.2:
+ resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
es-object-atoms@1.1.2:
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
@@ -4110,8 +4068,8 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
- eslint-config-next@16.3.6:
- resolution: {integrity: sha512-1Upt3U7BDwU+ilpe2byZjAfts9oNq4d4fv/zXEvs8/4yS+cwOQW/WCxUNy8gCDquX67SzeehDvKblVC6ZBMocQ==}
+ eslint-config-next@16.3.8:
+ resolution: {integrity: sha512-81vovwMe6NGnoFsl0KUJWzlS+y239i3fdsxs49gSEX3pDQW2cuKy+fxUPXNkQqqx6OCPheYPtBlSufc9aZ0k+w==}
peerDependencies:
eslint: '>=9.0.0'
typescript: '>=3.3.1'
@@ -4273,8 +4231,8 @@ packages:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0}
- expect-type@1.3.0:
- resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
engines: {node: '>=12.0.0'}
express-rate-limit@8.6.2:
@@ -4339,8 +4297,8 @@ packages:
fast-wrap-ansi@0.2.2:
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
- fastq@1.20.1:
- resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
+ fastq@1.20.3:
+ resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==}
fd-package-json@2.0.0:
resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==}
@@ -4404,8 +4362,8 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
- formatly@0.3.0:
- resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==}
+ formatly@0.7.1:
+ resolution: {integrity: sha512-ESuCEJE0SGgabl9q+zABI8LhJDmF+8nhpuqDzuM48HfJ9QTD5o6PqFJDFZY7w6f8xsh5lixuj5nJsQNvYOWvKQ==}
engines: {node: '>=18.3.0'}
hasBin: true
@@ -4420,8 +4378,8 @@ packages:
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
- fs-extra@11.4.0:
- resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==}
+ fs-extra@11.4.1:
+ resolution: {integrity: sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==}
engines: {node: '>=14.14'}
fsevents@2.3.3:
@@ -4457,8 +4415,8 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
- get-east-asian-width@1.6.0:
- resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
+ get-east-asian-width@1.7.0:
+ resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
@@ -4496,9 +4454,6 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
- get-tsconfig@4.14.1:
- resolution: {integrity: sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==}
-
get-tsconfig@4.14.3:
resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==}
@@ -4633,8 +4588,8 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
- ignore@7.0.9:
- resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==}
+ ignore@7.0.11:
+ resolution: {integrity: sha512-YChdK5txDjwGUvgR7oCJcLGkwi3LDh8Zx8tS7ndYciLEg/oOCL26VUBSUEtgS7EjiybcBqFxh5j0rAdDX7/bbg==}
engines: {node: '>= 4'}
immer@11.1.15:
@@ -4669,6 +4624,10 @@ packages:
resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==}
engines: {node: '>= 12'}
+ ip-address@10.7.3:
+ resolution: {integrity: sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==}
+ engines: {node: '>= 12'}
+
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@@ -4677,9 +4636,6 @@ packages:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
- is-arrayish@0.2.1:
- resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
-
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@@ -4879,18 +4835,6 @@ packages:
resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==}
engines: {node: '>=18'}
- istanbul-lib-coverage@3.2.2:
- resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
- engines: {node: '>=8'}
-
- istanbul-lib-report@3.0.1:
- resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
- engines: {node: '>=10'}
-
- istanbul-reports@3.2.0:
- resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
- engines: {node: '>=8'}
-
iterator.prototype@1.1.5:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
@@ -4923,9 +4867,6 @@ packages:
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
- json-parse-even-better-errors@2.3.1:
- resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
-
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -4986,8 +4927,8 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
- knip@6.32.3:
- resolution: {integrity: sha512-wOJ1Av8PwKqFO0wU7F64vzIcUjxhrieA3Tc2lmeK9C9prpxq+TeG2ewNH5tCaBVZwXNPp6lJkrr+IhZ20iqkMA==}
+ knip@6.40.0:
+ resolution: {integrity: sha512-5Lp/ANJu3AkW5f+J+2QG14z7xKZGi/ybb5MU1EWjeGdt6MpuIaLpLFiyBi0ZKryTiuTHiJqo/WKJRaWYimvavw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -5002,58 +4943,58 @@ packages:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
- lefthook-darwin-arm64@2.1.12:
- resolution: {integrity: sha512-GSUjqaCuxAYlPOovbjXEWE3HAIa/xOQkTTmZ937zieQldjPLTaC7+s5FHoxKywn+D9riBlofkDqG7Z4f5zzmPA==}
+ lefthook-darwin-arm64@2.2.1:
+ resolution: {integrity: sha512-SyuDBT5HqNQfi607vMHrYJu45bRN7JoIFdyxLjEPVe34UnZJCesmeYBODEYTyCfRZjPOaTbton4Padu+qvqfCQ==}
cpu: [arm64]
os: [darwin]
- lefthook-darwin-x64@2.1.12:
- resolution: {integrity: sha512-qAUHSSw/7Afi5wxkoLkxORxSicCeTBXyVLnRgD6YZra6udviozpK3Aok9Z6FIWeKc5FBijRMSNDxGPTREhsjcQ==}
+ lefthook-darwin-x64@2.2.1:
+ resolution: {integrity: sha512-qkIdM89T3OxpLp9rssPSdRQ7QKkX0l07xWxxOaZfECn5iB2xVXSD9FQSjYUMNGXtJJWOGqdKqBngGTB/ShDZtQ==}
cpu: [x64]
os: [darwin]
- lefthook-freebsd-arm64@2.1.12:
- resolution: {integrity: sha512-FHZRfKliFNbyz54zsoGdVe9muq67cNjBTZ6jrPPUjI7xUuyCwSpzA+1OpiwJT00L9pP5ZGONBqnGZKnrpC+7Uw==}
+ lefthook-freebsd-arm64@2.2.1:
+ resolution: {integrity: sha512-0jdr/9Q5cGuazrEVquv29AOD77aPc2jAmFU4EtqVVg3PhMJjAyt7bQeA9qdwHhaHhLnaeUZwMwo3IleVm+D4wg==}
cpu: [arm64]
os: [freebsd]
- lefthook-freebsd-x64@2.1.12:
- resolution: {integrity: sha512-HYQZPGy2DDEx7dbuotusJpujY5q9igOLgx36qeeQcNQuvvdGHPj2ZGSIsGKhoJ0dw5Qa4knKJoPAHEZQWdV/og==}
+ lefthook-freebsd-x64@2.2.1:
+ resolution: {integrity: sha512-JFKyNZ9qQp9NsuFyAg/CJIBurIHX3AUayFXxuoDMATWAIhhlyK0YMtpWgEFIl1M2iGLBLQ3TiLoCCRnbwndNDg==}
cpu: [x64]
os: [freebsd]
- lefthook-linux-arm64@2.1.12:
- resolution: {integrity: sha512-ipjOri2PB/sk4noPrHQuM5fcpNBjmlKo4qMtXyLnxeOxGYHWZzf0Xi0OtrXHQ//tOprcv40ADvhUuSdcGqigLw==}
+ lefthook-linux-arm64@2.2.1:
+ resolution: {integrity: sha512-na8Ezm7pK44+WsQqj9tES7v8VseM1MrNeNJhUnM8eINV22KYptWam8evfjZxY3NYxOTwn6f/Zpe/BBvcLKMaTQ==}
cpu: [arm64]
os: [linux]
- lefthook-linux-x64@2.1.12:
- resolution: {integrity: sha512-DmU6xfvqVoFdW+STyc70YI4Ry8vkHGKHx+IJ1Js/Gacq5dv+fiwNzwle3bi28EkL6P8xY67B/CfQfRj4SRU4tg==}
+ lefthook-linux-x64@2.2.1:
+ resolution: {integrity: sha512-wl7CR5uNkOiZ38SkW4l9O6wf/Nllbkw90ZZonXRFkbgrnPjLMhZGUt9ucewsPwBQ/ckU1t08JuBHeOywB/CFow==}
cpu: [x64]
os: [linux]
- lefthook-openbsd-arm64@2.1.12:
- resolution: {integrity: sha512-vb0J7bElLvoaCWQmna3HntsvoNqMsGAhfjjC99ss1OdCteufZKM3RxCVoMBEbD50Itv2c1Ua2AFVToltVFTy1Q==}
+ lefthook-openbsd-arm64@2.2.1:
+ resolution: {integrity: sha512-UFwL+AGSqkhQ15kuKl5FMRx/UqbcfcQbGFg5ggYzhT7qBOsTu3a9POXRxt3hsalBeMgkDWsv3IaPlj51YGpCAg==}
cpu: [arm64]
os: [openbsd]
- lefthook-openbsd-x64@2.1.12:
- resolution: {integrity: sha512-QOmhDWqPPK4+99scanfEmbJjUR+JYC93qhr/0bp5Dj3LaR3kVFNWc6zOQUsOTPy/LC6PG759Lej/mr/BtjUL2Q==}
+ lefthook-openbsd-x64@2.2.1:
+ resolution: {integrity: sha512-IMKEuxJLo91C1yjhJ42EadfXUb3ECMkvTKpSt17kwaiyZsvvZ8LGdrXwQZiLNJt2M5ECuTVCQUvDimM6mGraOw==}
cpu: [x64]
os: [openbsd]
- lefthook-windows-arm64@2.1.12:
- resolution: {integrity: sha512-eErEyr9AHkRFj6TxpCQeKGd0s6IrtL/VEIZ/ssg8dNfG+ycR4jAW/IAlrJSw6/ZQXb3WtWLaQ6QA5c+TLh7vmg==}
+ lefthook-windows-arm64@2.2.1:
+ resolution: {integrity: sha512-Pi8vOvObjg8HJDl8d96s7jsI6TN33klpqv6tXSXpoKEeoAwEh5Y9NawO6bzoOpLfAxf2SfMBcuaBzFQ5zE2gjA==}
cpu: [arm64]
os: [win32]
- lefthook-windows-x64@2.1.12:
- resolution: {integrity: sha512-0h+WmDVdDriTjloD/UbjPq/ZtqaaJlPAdGcVwMCEdAxfbF0FTgDbKX3igui9g2U/qG2y6QpVhtz1jeiRe7ctaw==}
+ lefthook-windows-x64@2.2.1:
+ resolution: {integrity: sha512-7FtuPrc1XzxIFLAbzneOF1hr9r/vGi2lcMpX14QG9d/mnLhwu4EyNBh48JNEqsQqrOB9QKavsNqxVl1e8zcukA==}
cpu: [x64]
os: [win32]
- lefthook@2.1.12:
- resolution: {integrity: sha512-2uOexfsrrRhCiTUWdGyDySxVHHhOFJ8MQejs27dM3hugrQB48/z1ZVlaNoxpZ1SnzQ1GaDIbO5rAvicwyiknYQ==}
+ lefthook@2.2.1:
+ resolution: {integrity: sha512-aKEwhQzGbLMkZu5vrFYKJzLvLVblWATYITmN7Ak4qRCDPCoFJhmeJZFuYf3nTXuqG9xTBE+J8xaAEPL2tpuWyw==}
hasBin: true
levn@0.4.1:
@@ -5134,9 +5075,6 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
- lines-and-columns@1.2.4:
- resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
-
locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
@@ -5200,10 +5138,14 @@ packages:
resolution: {integrity: sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==}
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
- lucide-react@1.44.0:
- resolution: {integrity: sha512-2egNApH4hX4j/qdCgRublh88+9u3mEhz9iSlW5ckm4kaQEqZbXbMr0l5u5JZLy8nmWRx2dbHGQkEDYz6C9aCgw==}
+ lucide-react@1.53.0:
+ resolution: {integrity: sha512-OxJ7elNku+iqVzOolk5caxq2LM8Mck6lhA6uZx3g2PlZ53+FvBnT6PlJxGCukCa6oF5wQwgSatn9cH1b4olWmQ==}
peerDependencies:
+ '@types/react': '*'
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
@@ -5212,12 +5154,11 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
- magicast@0.5.2:
- resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==}
+ magic-string@1.4.3:
+ resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==}
- make-dir@4.0.0:
- resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
- engines: {node: '>=10'}
+ magicast@0.5.5:
+ resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
@@ -5295,8 +5236,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
- mmdb-lib@3.0.2:
- resolution: {integrity: sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==}
+ mmdb-lib@3.0.3:
+ resolution: {integrity: sha512-xQPoBXcNjjHiOvOraFBKtA++uNWF6aCVHL9dRKFXEov8eI3QJwtgiw3qApsonFT5SpoqsEVISUTg3HIDs2DiXw==}
engines: {node: '>=10', npm: '>=6'}
mrmime@2.0.1:
@@ -5330,14 +5271,24 @@ packages:
resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
engines: {node: '>=8.0.0'}
- nan@2.28.0:
- resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==}
+ nan@2.29.0:
+ resolution: {integrity: sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==}
nanoid@3.3.18:
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanoid@3.3.19:
+ resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ nanoid@3.3.20:
+ resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
napi-postinstall@0.3.4:
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@@ -5356,8 +5307,8 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
- next@16.3.6:
- resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==}
+ next@16.3.8:
+ resolution: {integrity: sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -5394,12 +5345,12 @@ packages:
resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==}
engines: {node: '>= 6.13.0'}
- node-releases@2.0.51:
- resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
+ node-releases@2.0.58:
+ resolution: {integrity: sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==}
engines: {node: '>=18'}
- nodemailer@10.0.9:
- resolution: {integrity: sha512-BF0qcyplCwp+jMk6HCjFykBz/YhhZSsxrARhOldLwFWH+8kGjQsd2WIMIZhqEuyXRoGFi0ONbDeWDMDoL8MhLw==}
+ nodemailer@10.0.16:
+ resolution: {integrity: sha512-VL9xKErxN+el77o8eBOuYKUPMyEx7F9hfnDCuiUKJRp9v3wnWMOudYgP/SWbwSHy8rUL4cylobuXtki1LpQS7A==}
engines: {node: '>=20.0.0'}
non-error@0.1.0:
@@ -5454,8 +5405,9 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
- obug@2.1.1:
- resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+ obug@2.2.1:
+ resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
+ engines: {node: '>=12.20.0'}
ohash@2.0.12:
resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
@@ -5475,8 +5427,8 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
- open@11.0.0:
- resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
+ open@11.0.4:
+ resolution: {integrity: sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==}
engines: {node: '>=20'}
open@8.4.2:
@@ -5499,8 +5451,8 @@ packages:
resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
engines: {node: '>=18'}
- otpauth@9.5.1:
- resolution: {integrity: sha512-fJmDAHc8wImfqqqOXIlBvT1dEKrZK0Cmb2VEgScpNTolCz0PHh6ExUZGv4sLtOsWNaHCQlD+rRqaPgnoxFoZjQ==}
+ otpauth@9.5.2:
+ resolution: {integrity: sha512-GQ5emWR/x1tcExT62IBT0UfO95wZzJZyxYOJOGVeQF47SYEN9vmh0vISvDZaNMuFJRG+IaWCKtfm+t9Bfoal6w==}
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
@@ -5509,8 +5461,8 @@ packages:
resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==}
engines: {node: '>= 0.4'}
- oxc-parser@0.143.0:
- resolution: {integrity: sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA==}
+ oxc-parser@0.150.0:
+ resolution: {integrity: sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==}
engines: {node: ^20.19.0 || >=22.12.0}
oxc-resolver@11.24.2:
@@ -5524,8 +5476,8 @@ packages:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
- p-limit@7.3.1:
- resolution: {integrity: sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q==}
+ p-limit@7.3.3:
+ resolution: {integrity: sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==}
engines: {node: '>=20'}
p-locate@3.0.0:
@@ -5547,6 +5499,9 @@ packages:
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+ package-manager-detector@1.9.0:
+ resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==}
+
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
@@ -5554,10 +5509,6 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
- parse-json@5.2.0:
- resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
- engines: {node: '>=8'}
-
parse-ms@4.0.0:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'}
@@ -5610,8 +5561,8 @@ packages:
resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==}
engines: {node: '>=20.16.0 || >=22.3.0'}
- pdfjs-dist@6.3.289:
- resolution: {integrity: sha512-ZHjSVpDa3D6izMq8/04lvkhkATUmL9px6ChPaXc1k6nU2Mrhlg1/7F0bdUqCwUjw3NsPTfPZsMDUU6ZIcRaeQw==}
+ pdfjs-dist@6.4.299:
+ resolution: {integrity: sha512-AVl138zALtfaAPvADulE0PZThbYzCBS79nL4pOSL/6Sm/4AH5A21BD9VHt97OlCuzJuCpmeZtAtkinisF4Vb1g==}
engines: {node: '>=22.13.0 || >=24'}
perfect-debounce@2.1.0:
@@ -5620,16 +5571,18 @@ packages:
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
- pg-boss@12.34.0:
- resolution: {integrity: sha512-03boJOWLiiHEQPgXCPgrMv6My5Qv6ieD85AOrm/adi475oo74kn//gHOheYqWiU/2guCJMRpKQpD6F7Oln1aWQ==}
+ pg-boss@12.37.1:
+ resolution: {integrity: sha512-LghWeOIbMCiLcTY3+8MInXaJ0wk0AOkbeUZ8oE+cGO0uoheGSW3GK57eDqEbrDGpwTIpX9BVphvdvWEDYTcJ9A==}
engines: {node: '>=22.12.0'}
hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.9.0
- pg-cloudflare@1.4.0:
- resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==}
+ pg-cloudflare@1.4.1:
+ resolution: {integrity: sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==}
- pg-connection-string@2.14.0:
- resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==}
+ pg-connection-string@2.14.1:
+ resolution: {integrity: sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
@@ -5643,12 +5596,15 @@ packages:
pg-protocol@1.16.0:
resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==}
+ pg-protocol@1.16.1:
+ resolution: {integrity: sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==}
+
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
- pg@8.23.0:
- resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==}
+ pg@8.23.1:
+ resolution: {integrity: sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
@@ -5662,10 +5618,6 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
- picomatch@4.0.5:
- resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
- engines: {node: '>=12'}
-
picomatch@4.0.7:
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'}
@@ -5681,13 +5633,13 @@ packages:
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
engines: {node: '>=8'}
- playwright-core@1.63.0:
- resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==}
+ playwright-core@1.64.0:
+ resolution: {integrity: sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==}
engines: {node: '>=20'}
hasBin: true
- playwright@1.63.0:
- resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==}
+ playwright@1.64.0:
+ resolution: {integrity: sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==}
engines: {node: '>=20'}
hasBin: true
@@ -5699,22 +5651,22 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
- postcss-selector-parser@7.1.4:
- resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==}
+ postcss-selector-parser@7.1.6:
+ resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==}
engines: {node: '>=4'}
postcss@8.5.23:
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
engines: {node: ^10 || ^12 || >=14}
- postcss@8.5.25:
- resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==}
- engines: {node: ^10 || ^12 || >=14}
-
postcss@8.5.26:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.5.29:
+ resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
@@ -5739,8 +5691,8 @@ packages:
resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==}
engines: {node: '>=12'}
- powershell-utils@0.1.0:
- resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
+ powershell-utils@0.2.1:
+ resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==}
engines: {node: '>=20'}
prelude-ls@1.2.1:
@@ -5807,8 +5759,8 @@ packages:
engines: {node: '>=14'}
hasBin: true
- pretty-ms@9.3.0:
- resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
+ pretty-ms@9.3.1:
+ resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==}
engines: {node: '>=18'}
prisma@7.10.0:
@@ -5845,12 +5797,12 @@ packages:
resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==}
engines: {node: '>=18'}
- protobufjs@7.6.5:
- resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==}
+ protobufjs@7.6.6:
+ resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==}
engines: {node: '>=12.0.0'}
- proxy-addr@2.0.7:
- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
pump@3.0.4:
@@ -5882,8 +5834,8 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
- radix-ui@1.6.7:
- resolution: {integrity: sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==}
+ radix-ui@1.7.0:
+ resolution: {integrity: sha512-x+z5TV2v6u0QpAQ4dQfNMcvtIdSQ1Op2oqBEyfDXjNldRZbMTP+hcjr5Iie31bep81Axeioa+70Ww7pZDzTEQQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -5909,8 +5861,8 @@ packages:
rc9@3.1.0:
resolution: {integrity: sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==}
- react-day-picker@10.0.1:
- resolution: {integrity: sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==}
+ react-day-picker@10.0.2:
+ resolution: {integrity: sha512-QetiKtZWE9Uaw/pM/SYNv6WPYOTRnSJ2puL0GgwkmUFMIwiY0dtjH1W4B7DOVXVNQM0ZOClGpH/heOfBSzWeiA==}
engines: {node: '>=18'}
peerDependencies:
'@types/react': '>=16.8.0'
@@ -5924,11 +5876,15 @@ packages:
peerDependencies:
react: ^19.3.0
- react-hook-form@7.83.0:
- resolution: {integrity: sha512-AXt8cMCmx5a7u4uvpb2uRFVrWQhllI4pV+LSykxIac/hjt44TnQkmX9BKuQi2i+LDC62esmiLpilkav+kjVf/A==}
+ react-hook-form@7.89.0:
+ resolution: {integrity: sha512-vKcoCfy8RKZDrhSdqsRFp0uEOdS1AhLjOL6hR7Wmfzg1akNxEhaPf5WORx8SgEQPVXakdDyDkqQp+KNxVnnPHg==}
engines: {node: '>=18.0.0'}
peerDependencies:
+ '@types/react': '*'
react: ^16.8.0 || ^17 || ^18 || ^19
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -5997,8 +5953,8 @@ packages:
resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==}
engines: {node: '>= 20.19.0'}
- recast@0.23.19:
- resolution: {integrity: sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w==}
+ recast@0.23.21:
+ resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==}
engines: {node: '>= 4'}
recharts@3.10.1:
@@ -6098,8 +6054,8 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
- rrule-temporal@2.2.6:
- resolution: {integrity: sha512-izTnMn8pAFNLDuyRfF+H53YnWOF7tsA5+RcGrha4onVzOnXn8YXQ7tJE5StNZvAZXvxz0In3R/IQhbQJpi7EYQ==}
+ rrule-temporal@2.2.8:
+ resolution: {integrity: sha512-+wWUe0J02oMh1UWKIt1RTu2H0Cz+gP11O/iweK0MN2UP6JLGeepTUkbSBa90Hxq0W+ywSxM9IlJNouIUdMOwhg==}
rrule@2.8.1:
resolution: {integrity: sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==}
@@ -6161,8 +6117,8 @@ packages:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
- serialize-error@13.0.1:
- resolution: {integrity: sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA==}
+ serialize-error@13.0.2:
+ resolution: {integrity: sha512-G+YKwp9I6IvWMe5RlwqxdmTPIcq/G8HK/sU51gdV1XAJznYkRSTB0ph+zB9AfSxG0fx6xrqYA6i9HeIcHFTXow==}
engines: {node: '>=20'}
serve-static@2.2.1:
@@ -6190,13 +6146,13 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
- shadcn@4.16.1:
- resolution: {integrity: sha512-XLFzfNNIUPlUlyheFEzj0H4Vnhi9nI0nl3Nfgg8HYXW1FkUVhVT1X+mgmOUW8aWL5SeG0A+yJIV5fm3Hr9MVkQ==}
+ shadcn@4.21.4:
+ resolution: {integrity: sha512-YYfn/hsHbRH2mGwnKBx9tk+DjTQH+RUMS5iCxjKzwzuJ5uoHPqLOFUCJSeX0sXfrcBSfgQaXiOLGKNLjlIQxxw==}
engines: {node: '>=20.18.1'}
hasBin: true
- sharp@0.35.4:
- resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
+ sharp@0.35.5:
+ resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
@@ -6228,9 +6184,6 @@ packages:
resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
engines: {node: '>= 0.4'}
- siginfo@2.0.0:
- resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
-
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@@ -6245,10 +6198,18 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
- smol-toml@1.8.0:
- resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==}
+ smart-buffer@4.2.0:
+ resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
+ engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
+
+ smol-toml@1.9.0:
+ resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==}
engines: {node: '>= 18'}
+ socks@2.8.10:
+ resolution: {integrity: sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==}
+ engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+
sonner@2.0.8:
resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
peerDependencies:
@@ -6259,8 +6220,8 @@ packages:
'@types/react':
optional: true
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
source-map@0.6.1:
@@ -6288,9 +6249,6 @@ packages:
stable-hash@0.0.5:
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
- stackback@0.0.2:
- resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
-
stackblur-canvas@2.7.0:
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
engines: {node: '>=0.1.14'}
@@ -6302,8 +6260,8 @@ packages:
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
- std-env@4.1.0:
- resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
+ std-env@4.3.0:
+ resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
stdin-discarder@0.2.2:
resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
@@ -6316,8 +6274,8 @@ packages:
stream-browserify@3.0.0:
resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
- streamx@2.28.0:
- resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==}
+ streamx@2.28.1:
+ resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==}
strict-event-emitter@0.5.1:
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
@@ -6420,8 +6378,8 @@ packages:
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
engines: {node: '>=12.0.0'}
- systeminformation@5.33.1:
- resolution: {integrity: sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==}
+ systeminformation@5.33.15:
+ resolution: {integrity: sha512-s5RloL0F7xhN0ZjfHtLGnbf7EW74EI0N8NFL+1xR7Oxz9QA33obNZsl7lyfnJOhN04E/kGFPoJPlvY7TZ5fzmw==}
engines: {node: '>=10.0.0'}
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
@@ -6430,8 +6388,8 @@ packages:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
- tailwind-merge@3.6.0:
- resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
+ tailwind-merge@3.7.0:
+ resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==}
tailwindcss@4.3.3:
resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==}
@@ -6450,8 +6408,8 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
- tar-stream@3.2.0:
- resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==}
+ tar-stream@3.2.1:
+ resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==}
teex@1.0.1:
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
@@ -6465,8 +6423,8 @@ packages:
tesseract.js@7.0.0:
resolution: {integrity: sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==}
- testcontainers@12.1.0:
- resolution: {integrity: sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA==}
+ testcontainers@12.2.0:
+ resolution: {integrity: sha512-Pd0c2eU0DdIuK+Pb3c+vmBUdKjDLgxZmPecQGESFAKyn5LTqSaSw8l7htfMlLwabb/QsUILe6XkxMJw8Ly/ThA==}
engines: {node: '>= 22.22'}
text-decoder@1.2.7:
@@ -6478,19 +6436,20 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
- tinybench@2.9.0:
- resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+ tinybench@6.2.1:
+ resolution: {integrity: sha512-IAbQaPJIIdhVxi0mqy9lez1wEwje0UQf9F1vwtbelEG5Nbgn0nPOxCvU+mNeNS9gyJTAhrHp74CbUaSynQpZcQ==}
+ engines: {node: '>=20.0.0'}
- tinyexec@1.1.1:
- resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
+ tinyexec@1.3.1:
+ resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
engines: {node: '>=18'}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
- tinyrainbow@3.1.0:
- resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
+ tinyrainbow@3.2.0:
+ resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==}
engines: {node: '>=14.0.0'}
tldts-core@7.4.9:
@@ -6559,6 +6518,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
+ type-fest@4.41.0:
+ resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
+ engines: {node: '>=16'}
+
type-fest@5.10.0:
resolution: {integrity: sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==}
engines: {node: '>=20'}
@@ -6583,8 +6546,8 @@ packages:
resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==}
engines: {node: '>= 0.4'}
- typescript-eslint@8.70.0:
- resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==}
+ typescript-eslint@8.71.0:
+ resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -6595,8 +6558,8 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
- unbash@4.0.10:
- resolution: {integrity: sha512-b7zoBQvpWp0vuN5q2vK2RRBR2SvuruQAs50DApdDveBSn3eSYd84IaHodFqQIMlvY9K2VnyBUEXgwOBuGU9GBg==}
+ unbash@4.0.12:
+ resolution: {integrity: sha512-PnKvRX+Fbxl/usVlTr/31tXIOVAH+8OdC9qaxUQcvurwEnoc3Vk/ipi2Ce8IjMxzK9byfSGdsUBjqqJGc1o5lw==}
engines: {node: '>=14'}
unbox-primitive@1.1.0:
@@ -6635,8 +6598,8 @@ packages:
until-async@3.0.2:
resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
- update-browserslist-db@1.2.3:
- resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ update-browserslist-db@1.3.4:
+ resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -6741,23 +6704,23 @@ packages:
yaml:
optional: true
- vitest@4.1.11:
- resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
- engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ vitest@5.0.3:
+ resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==}
+ engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
- '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
- '@vitest/browser-playwright': 4.1.11
- '@vitest/browser-preview': 4.1.11
- '@vitest/browser-webdriverio': 4.1.11
- '@vitest/coverage-istanbul': 4.1.11
- '@vitest/coverage-v8': 4.1.11
- '@vitest/ui': 4.1.11
+ '@types/node': ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 5.0.3
+ '@vitest/browser-preview': 5.0.3
+ '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
+ '@vitest/coverage-istanbul': 5.0.3
+ '@vitest/coverage-v8': 5.0.3
+ '@vitest/ui': 5.0.3
happy-dom: '*'
jsdom: '*'
- vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ vite: ^6.4.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
@@ -6820,8 +6783,8 @@ packages:
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
- which-typed-array@1.1.23:
- resolution: {integrity: sha512-JMh8aK+1B/0bk/YNupICmH5MgCq6yNLKYQUfsZtSDLLCCmxUkHjUY+oOlIa90lO7lHN1woAfpenLdlKpXy9o+A==}
+ which-typed-array@1.1.24:
+ resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==}
engines: {node: '>= 0.4'}
which@2.0.2:
@@ -6834,9 +6797,9 @@ packages:
engines: {node: ^16.13.0 || >=18.0.0}
hasBin: true
- why-is-node-running@2.3.0:
- resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
- engines: {node: '>=8'}
+ why-is-node-running@3.2.1:
+ resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
+ engines: {node: '>=20.11'}
hasBin: true
word-wrap@1.2.5:
@@ -6870,8 +6833,8 @@ packages:
utf-8-validate:
optional: true
- wsl-utils@0.3.1:
- resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
+ wsl-utils@1.0.1:
+ resolution: {integrity: sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==}
engines: {node: '>=20'}
xtend@4.0.2:
@@ -6888,13 +6851,8 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
- yaml@2.8.4:
- resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
- engines: {node: '>= 14.6'}
- hasBin: true
-
- yaml@2.9.0:
- resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
+ yaml@2.9.1:
+ resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==}
engines: {node: '>= 14.6'}
hasBin: true
@@ -6940,8 +6898,8 @@ packages:
zlibjs@0.3.1:
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
- zod-openapi@6.0.1:
- resolution: {integrity: sha512-ITUfaevP/JcrsOavyVHFQHz788F7YcmN8wFYRZk20UM16BUafdUzxq8ZM4ahyTv3c6ESvY9ywA+MwLXzIvVVAw==}
+ zod-openapi@6.0.2:
+ resolution: {integrity: sha512-SwC5Ppi2ov8WimzNWP/XK95VqPyLDzxPYj10HgNPzhZH9gO7zx0sxdYL9eq7wOADHx1JddOApjhYDmMWQvnVnw==}
engines: {node: '>=22.14.0'}
peerDependencies:
zod: ^4.0.0
@@ -6973,249 +6931,185 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
- '@aws-crypto/crc32@5.2.0':
+ '@aws-sdk/checksums@3.1001.1':
dependencies:
- '@aws-crypto/util': 5.2.0
- '@aws-sdk/types': 3.973.13
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-crypto/crc32c@5.2.0':
+ '@aws-sdk/client-s3@3.1147.0':
dependencies:
- '@aws-crypto/util': 5.2.0
- '@aws-sdk/types': 3.973.13
+ '@aws-sdk/checksums': 3.1001.1
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/credential-provider-node': 3.972.84
+ '@aws-sdk/middleware-sdk-s3': 3.972.77
+ '@aws-sdk/signature-v4-multi-region': 3.996.47
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.2
+ '@smithy/fetch-http-handler': 5.8.0
+ '@smithy/node-http-handler': 4.12.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-crypto/sha1-browser@5.2.0':
+ '@aws-sdk/core@3.978.1':
dependencies:
- '@aws-crypto/supports-web-crypto': 5.2.0
- '@aws-crypto/util': 5.2.0
- '@aws-sdk/types': 3.973.13
- '@aws-sdk/util-locate-window': 3.965.5
- '@smithy/util-utf8': 2.3.0
- tslib: 2.8.1
-
- '@aws-crypto/sha256-browser@5.2.0':
- dependencies:
- '@aws-crypto/sha256-js': 5.2.0
- '@aws-crypto/supports-web-crypto': 5.2.0
- '@aws-crypto/util': 5.2.0
- '@aws-sdk/types': 3.973.13
- '@aws-sdk/util-locate-window': 3.965.5
- '@smithy/util-utf8': 2.3.0
- tslib: 2.8.1
-
- '@aws-crypto/sha256-js@5.2.0':
- dependencies:
- '@aws-crypto/util': 5.2.0
- '@aws-sdk/types': 3.973.13
- tslib: 2.8.1
-
- '@aws-crypto/supports-web-crypto@5.2.0':
- dependencies:
- tslib: 2.8.1
-
- '@aws-crypto/util@5.2.0':
- dependencies:
- '@aws-sdk/types': 3.973.13
- '@smithy/util-utf8': 2.3.0
- tslib: 2.8.1
-
- '@aws-sdk/checksums@3.1000.8':
- dependencies:
- '@aws-crypto/crc32': 5.2.0
- '@aws-crypto/crc32c': 5.2.0
- '@aws-crypto/util': 5.2.0
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
- tslib: 2.8.1
-
- '@aws-sdk/client-s3@3.1075.0':
- dependencies:
- '@aws-crypto/sha1-browser': 5.2.0
- '@aws-crypto/sha256-browser': 5.2.0
- '@aws-crypto/sha256-js': 5.2.0
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/credential-provider-node': 3.972.58
- '@aws-sdk/middleware-flexible-checksums': 3.974.33
- '@aws-sdk/middleware-sdk-s3': 3.972.54
- '@aws-sdk/signature-v4-multi-region': 3.996.35
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.26.0
- '@smithy/fetch-http-handler': 5.5.2
- '@smithy/node-http-handler': 4.8.2
- '@smithy/types': 4.15.0
- tslib: 2.8.1
-
- '@aws-sdk/core@3.974.23':
- dependencies:
- '@aws-sdk/types': 3.973.13
- '@aws-sdk/xml-builder': 3.972.31
- '@aws/lambda-invoke-store': 0.2.4
- '@smithy/core': 3.33.3
- '@smithy/signature-v4': 5.5.2
- '@smithy/types': 4.18.0
+ '@aws-sdk/types': 3.974.6
+ '@aws-sdk/xml-builder': 3.972.41
+ '@aws/lambda-invoke-store': 0.3.0
+ '@smithy/core': 3.35.1
+ '@smithy/signature-v4': 5.7.4
+ '@smithy/types': 4.19.0
bowser: 2.14.1
tslib: 2.8.1
- '@aws-sdk/credential-provider-env@3.972.49':
+ '@aws-sdk/credential-provider-env@3.972.72':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/credential-provider-http@3.972.51':
+ '@aws-sdk/credential-provider-http@3.972.74':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/fetch-http-handler': 5.5.2
- '@smithy/node-http-handler': 4.8.2
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/fetch-http-handler': 5.8.0
+ '@smithy/node-http-handler': 4.12.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/credential-provider-ini@3.972.56':
+ '@aws-sdk/credential-provider-ini@3.973.17':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/credential-provider-env': 3.972.49
- '@aws-sdk/credential-provider-http': 3.972.51
- '@aws-sdk/credential-provider-login': 3.972.55
- '@aws-sdk/credential-provider-process': 3.972.49
- '@aws-sdk/credential-provider-sso': 3.972.55
- '@aws-sdk/credential-provider-web-identity': 3.972.55
- '@aws-sdk/nested-clients': 3.997.23
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/credential-provider-imds': 4.4.2
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/credential-provider-env': 3.972.72
+ '@aws-sdk/credential-provider-http': 3.972.74
+ '@aws-sdk/credential-provider-login': 3.972.79
+ '@aws-sdk/credential-provider-process': 3.972.72
+ '@aws-sdk/credential-provider-sso': 3.973.16
+ '@aws-sdk/credential-provider-web-identity': 3.972.78
+ '@aws-sdk/nested-clients': 3.997.46
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/credential-provider-imds': 4.5.2
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/credential-provider-login@3.972.55':
+ '@aws-sdk/credential-provider-login@3.972.79':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/nested-clients': 3.997.23
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/nested-clients': 3.997.46
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/credential-provider-node@3.972.58':
+ '@aws-sdk/credential-provider-node@3.972.84':
dependencies:
- '@aws-sdk/credential-provider-env': 3.972.49
- '@aws-sdk/credential-provider-http': 3.972.51
- '@aws-sdk/credential-provider-ini': 3.972.56
- '@aws-sdk/credential-provider-process': 3.972.49
- '@aws-sdk/credential-provider-sso': 3.972.55
- '@aws-sdk/credential-provider-web-identity': 3.972.55
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/credential-provider-imds': 4.4.2
- '@smithy/types': 4.18.0
+ '@aws-sdk/credential-provider-env': 3.972.72
+ '@aws-sdk/credential-provider-http': 3.972.74
+ '@aws-sdk/credential-provider-ini': 3.973.17
+ '@aws-sdk/credential-provider-process': 3.972.72
+ '@aws-sdk/credential-provider-sso': 3.973.16
+ '@aws-sdk/credential-provider-web-identity': 3.972.78
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/credential-provider-imds': 4.5.2
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/credential-provider-process@3.972.49':
+ '@aws-sdk/credential-provider-process@3.972.72':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/credential-provider-sso@3.972.55':
+ '@aws-sdk/credential-provider-sso@3.973.16':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/nested-clients': 3.997.23
- '@aws-sdk/token-providers': 3.1074.0
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/nested-clients': 3.997.46
+ '@aws-sdk/token-providers': 3.1138.0
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/credential-provider-web-identity@3.972.55':
+ '@aws-sdk/credential-provider-web-identity@3.972.78':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/nested-clients': 3.997.23
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/nested-clients': 3.997.46
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/lib-storage@3.1075.0(@aws-sdk/client-s3@3.1075.0)':
+ '@aws-sdk/lib-storage@3.1147.0(@aws-sdk/client-s3@3.1147.0)':
dependencies:
- '@aws-sdk/client-s3': 3.1075.0
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@aws-sdk/client-s3': 3.1147.0
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
buffer: 5.6.0
events: 3.3.0
stream-browserify: 3.0.0
tslib: 2.8.1
- '@aws-sdk/middleware-flexible-checksums@3.974.33':
+ '@aws-sdk/middleware-sdk-s3@3.972.77':
dependencies:
- '@aws-sdk/checksums': 3.1000.8
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/signature-v4-multi-region': 3.996.47
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/middleware-sdk-s3@3.972.54':
+ '@aws-sdk/nested-clients@3.997.46':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/signature-v4-multi-region': 3.996.35
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/signature-v4-multi-region': 3.996.47
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/fetch-http-handler': 5.8.0
+ '@smithy/node-http-handler': 4.12.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/nested-clients@3.997.23':
+ '@aws-sdk/signature-v4-multi-region@3.996.47':
dependencies:
- '@aws-crypto/sha256-browser': 5.2.0
- '@aws-crypto/sha256-js': 5.2.0
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/signature-v4-multi-region': 3.996.35
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/fetch-http-handler': 5.5.2
- '@smithy/node-http-handler': 4.8.2
- '@smithy/types': 4.18.0
+ '@aws-sdk/types': 3.974.6
+ '@smithy/signature-v4': 5.7.4
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/signature-v4-multi-region@3.996.35':
+ '@aws-sdk/token-providers@3.1138.0':
dependencies:
- '@aws-sdk/types': 3.973.13
- '@smithy/signature-v4': 5.5.2
- '@smithy/types': 4.18.0
+ '@aws-sdk/core': 3.978.1
+ '@aws-sdk/nested-clients': 3.997.46
+ '@aws-sdk/types': 3.974.6
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/token-providers@3.1074.0':
+ '@aws-sdk/types@3.974.6':
dependencies:
- '@aws-sdk/core': 3.974.23
- '@aws-sdk/nested-clients': 3.997.23
- '@aws-sdk/types': 3.973.13
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/types@3.973.13':
+ '@aws-sdk/xml-builder@3.972.41':
dependencies:
- '@smithy/types': 4.18.0
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@aws-sdk/util-locate-window@3.965.5':
- dependencies:
- tslib: 2.8.1
+ '@aws/lambda-invoke-store@0.3.0': {}
- '@aws-sdk/xml-builder@3.972.31':
- dependencies:
- '@smithy/types': 4.18.0
- tslib: 2.8.1
-
- '@aws/lambda-invoke-store@0.2.4': {}
-
- '@axe-core/playwright@4.13.0(playwright-core@1.63.0)':
+ '@axe-core/playwright@4.13.0(playwright-core@1.64.0)':
dependencies:
axe-core: 4.13.0
- playwright-core: 1.63.0
+ playwright-core: 1.64.0
'@babel/code-frame@7.29.7':
dependencies:
@@ -7232,9 +7126,9 @@ snapshots:
'@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
'@babel/helpers': 7.29.7
- '@babel/parser': 7.29.8
+ '@babel/parser': 7.29.9
'@babel/template': 7.29.7
- '@babel/traverse': 7.29.8(supports-color@7.2.0)
+ '@babel/traverse': 7.29.10(supports-color@7.2.0)
'@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
@@ -7247,7 +7141,7 @@ snapshots:
'@babel/generator@7.29.8':
dependencies:
- '@babel/parser': 7.29.8
+ '@babel/parser': 7.29.9
'@babel/types': 7.29.8
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
@@ -7261,7 +7155,7 @@ snapshots:
dependencies:
'@babel/compat-data': 7.29.7
'@babel/helper-validator-option': 7.29.7
- browserslist: 4.28.7
+ browserslist: 4.29.3
lru-cache: 5.1.1
semver: 6.3.1
@@ -7273,7 +7167,7 @@ snapshots:
'@babel/helper-optimise-call-expression': 7.29.7
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@7.2.0)
- '@babel/traverse': 7.29.8(supports-color@7.2.0)
+ '@babel/traverse': 7.29.10(supports-color@7.2.0)
semver: 6.3.1
transitivePeerDependencies:
- supports-color
@@ -7282,14 +7176,14 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.29.7(supports-color@7.2.0)':
dependencies:
- '@babel/traverse': 7.29.8(supports-color@7.2.0)
+ '@babel/traverse': 7.29.10(supports-color@7.2.0)
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
'@babel/helper-module-imports@7.29.7(supports-color@7.2.0)':
dependencies:
- '@babel/traverse': 7.29.8(supports-color@7.2.0)
+ '@babel/traverse': 7.29.10(supports-color@7.2.0)
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -7299,7 +7193,7 @@ snapshots:
'@babel/core': 7.29.7(supports-color@7.2.0)
'@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
'@babel/helper-validator-identifier': 7.29.7
- '@babel/traverse': 7.29.8(supports-color@7.2.0)
+ '@babel/traverse': 7.29.10(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
@@ -7314,13 +7208,13 @@ snapshots:
'@babel/core': 7.29.7(supports-color@7.2.0)
'@babel/helper-member-expression-to-functions': 7.29.7(supports-color@7.2.0)
'@babel/helper-optimise-call-expression': 7.29.7
- '@babel/traverse': 7.29.8(supports-color@7.2.0)
+ '@babel/traverse': 7.29.10(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
'@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@7.2.0)':
dependencies:
- '@babel/traverse': 7.29.8(supports-color@7.2.0)
+ '@babel/traverse': 7.29.10(supports-color@7.2.0)
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
@@ -7336,7 +7230,7 @@ snapshots:
'@babel/template': 7.29.7
'@babel/types': 7.29.8
- '@babel/parser@7.29.8':
+ '@babel/parser@7.29.9':
dependencies:
'@babel/types': 7.29.8
@@ -7358,7 +7252,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
+ '@babel/plugin-transform-typescript@7.29.9(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
dependencies:
'@babel/core': 7.29.7(supports-color@7.2.0)
'@babel/helper-annotate-as-pure': 7.29.7
@@ -7376,7 +7270,7 @@ snapshots:
'@babel/helper-validator-option': 7.29.7
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
'@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
- '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
+ '@babel/plugin-transform-typescript': 7.29.9(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
@@ -7385,15 +7279,15 @@ snapshots:
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
- '@babel/parser': 7.29.8
+ '@babel/parser': 7.29.9
'@babel/types': 7.29.8
- '@babel/traverse@7.29.8(supports-color@7.2.0)':
+ '@babel/traverse@7.29.10(supports-color@7.2.0)':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-globals': 7.29.7
- '@babel/parser': 7.29.8
+ '@babel/parser': 7.29.9
'@babel/template': 7.29.7
'@babel/types': 7.29.8
debug: 4.4.3(supports-color@7.2.0)
@@ -7450,12 +7344,12 @@ snapshots:
enquirer: 2.4.1
env-paths: 2.2.1
execa: 5.1.1
- fdir: 6.5.0(picomatch@4.0.5)
+ fdir: 6.5.0(picomatch@4.0.7)
ignore: 5.3.2
object-treeify: 1.1.33
open: 8.4.2
- picomatch: 4.0.5
- systeminformation: 5.33.1
+ picomatch: 4.0.7
+ systeminformation: 5.33.15
undici: 7.30.0
which: 4.0.0
yocto-spinner: 1.2.2
@@ -7597,14 +7491,14 @@ snapshots:
dependencies:
lodash.camelcase: 4.3.0
long: 5.3.2
- protobufjs: 7.6.5
+ protobufjs: 7.6.6
yargs: 17.7.3
'@grpc/proto-loader@0.8.1':
dependencies:
lodash.camelcase: 4.3.0
long: 5.3.2
- protobufjs: 7.6.5
+ protobufjs: 7.6.6
yargs: 17.7.3
'@hexagon/base64@1.1.28': {}
@@ -7613,10 +7507,10 @@ snapshots:
dependencies:
hono: 4.13.7
- '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.3.0))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)':
+ '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.89.0(@types/react@19.3.0)(react@19.3.0))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)':
dependencies:
'@standard-schema/utils': 0.3.0
- react-hook-form: 7.83.0(react@19.3.0)
+ react-hook-form: 7.89.0(@types/react@19.3.0)(react@19.3.0)
optionalDependencies:
'@standard-schema/spec': 1.1.0
ajv: 8.20.0
@@ -7644,140 +7538,140 @@ snapshots:
'@img/colour@1.1.0':
optional: true
- '@img/sharp-darwin-arm64@0.35.4':
+ '@img/sharp-darwin-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.3.3
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
optional: true
- '@img/sharp-darwin-x64@0.35.4':
+ '@img/sharp-darwin-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.3.3
+ '@img/sharp-libvips-darwin-x64': 1.3.4
optional: true
- '@img/sharp-freebsd-wasm32@0.35.4':
+ '@img/sharp-freebsd-wasm32@0.35.5':
dependencies:
- '@img/sharp-wasm32': 0.35.4
+ '@img/sharp-wasm32': 0.35.5
optional: true
- '@img/sharp-libvips-darwin-arm64@1.3.3':
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-darwin-x64@1.3.3':
+ '@img/sharp-libvips-darwin-x64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-arm64@1.3.3':
+ '@img/sharp-libvips-linux-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-arm@1.3.3':
+ '@img/sharp-libvips-linux-arm@1.3.4':
optional: true
- '@img/sharp-libvips-linux-ppc64@1.3.3':
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-riscv64@1.3.3':
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-s390x@1.3.3':
+ '@img/sharp-libvips-linux-s390x@1.3.4':
optional: true
- '@img/sharp-libvips-linux-x64@1.3.3':
+ '@img/sharp-libvips-linux-x64@1.3.4':
optional: true
- '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-linuxmusl-x64@1.3.3':
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
optional: true
- '@img/sharp-linux-arm64@0.35.4':
+ '@img/sharp-linux-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.3.3
+ '@img/sharp-libvips-linux-arm64': 1.3.4
optional: true
- '@img/sharp-linux-arm@0.35.4':
+ '@img/sharp-linux-arm@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.3.3
+ '@img/sharp-libvips-linux-arm': 1.3.4
optional: true
- '@img/sharp-linux-ppc64@0.35.4':
+ '@img/sharp-linux-ppc64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-ppc64': 1.3.3
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
optional: true
- '@img/sharp-linux-riscv64@0.35.4':
+ '@img/sharp-linux-riscv64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-riscv64': 1.3.3
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
optional: true
- '@img/sharp-linux-s390x@0.35.4':
+ '@img/sharp-linux-s390x@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.3.3
+ '@img/sharp-libvips-linux-s390x': 1.3.4
optional: true
- '@img/sharp-linux-x64@0.35.4':
+ '@img/sharp-linux-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.3.3
+ '@img/sharp-libvips-linux-x64': 1.3.4
optional: true
- '@img/sharp-linuxmusl-arm64@0.35.4':
+ '@img/sharp-linuxmusl-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
optional: true
- '@img/sharp-linuxmusl-x64@0.35.4':
+ '@img/sharp-linuxmusl-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.3.3
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
optional: true
- '@img/sharp-wasm32@0.35.4':
+ '@img/sharp-wasm32@0.35.5':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
- '@img/sharp-webcontainers-wasm32@0.35.4':
+ '@img/sharp-webcontainers-wasm32@0.35.5':
dependencies:
- '@img/sharp-wasm32': 0.35.4
+ '@img/sharp-wasm32': 0.35.5
optional: true
- '@img/sharp-win32-arm64@0.35.4':
+ '@img/sharp-win32-arm64@0.35.5':
optional: true
- '@img/sharp-win32-ia32@0.35.4':
+ '@img/sharp-win32-ia32@0.35.5':
optional: true
- '@img/sharp-win32-x64@0.35.4':
+ '@img/sharp-win32-x64@0.35.5':
optional: true
'@inquirer/ansi@2.0.7':
optional: true
- '@inquirer/confirm@6.1.1(@types/node@26.6.2)':
+ '@inquirer/confirm@6.1.1(@types/node@26.6.4)':
dependencies:
- '@inquirer/core': 11.2.1(@types/node@26.6.2)
- '@inquirer/type': 4.0.7(@types/node@26.6.2)
+ '@inquirer/core': 11.2.1(@types/node@26.6.4)
+ '@inquirer/type': 4.0.7(@types/node@26.6.4)
optionalDependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
optional: true
- '@inquirer/core@11.2.1(@types/node@26.6.2)':
+ '@inquirer/core@11.2.1(@types/node@26.6.4)':
dependencies:
'@inquirer/ansi': 2.0.7
'@inquirer/figures': 2.0.7
- '@inquirer/type': 4.0.7(@types/node@26.6.2)
+ '@inquirer/type': 4.0.7(@types/node@26.6.4)
cli-width: 4.1.0
fast-wrap-ansi: 0.2.2
mute-stream: 3.0.0
signal-exit: 4.1.0
optionalDependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
optional: true
'@inquirer/figures@2.0.7':
optional: true
- '@inquirer/type@4.0.7(@types/node@26.6.2)':
+ '@inquirer/type@4.0.7(@types/node@26.6.4)':
optionalDependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
optional: true
'@isaacs/cliui@8.0.2':
@@ -7791,7 +7685,7 @@ snapshots:
'@jridgewell/gen-mapping@0.3.13':
dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/sourcemap-codec': 1.6.0
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
@@ -7803,10 +7697,12 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.5': {}
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
- '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/sourcemap-codec': 1.6.0
'@js-sdsl/ordered-map@4.4.2': {}
@@ -7818,7 +7714,7 @@ snapshots:
'@levischuck/tiny-cbor@0.2.11': {}
- '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@3.25.76)':
+ '@modelcontextprotocol/sdk@1.32.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@3.25.76)':
dependencies:
'@hono/node-server': 1.19.17(hono@4.13.7)
ajv: 8.20.0
@@ -7842,7 +7738,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3)':
+ '@modelcontextprotocol/sdk@1.32.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3)':
dependencies:
'@hono/node-server': 1.19.17(hono@4.13.7)
ajv: 8.20.0
@@ -7879,64 +7775,64 @@ snapshots:
'@napi-rs/canvas-android-arm64@0.1.80':
optional: true
- '@napi-rs/canvas-android-arm64@1.0.9':
+ '@napi-rs/canvas-android-arm64@1.0.10':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.80':
optional: true
- '@napi-rs/canvas-darwin-arm64@1.0.9':
+ '@napi-rs/canvas-darwin-arm64@1.0.10':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.80':
optional: true
- '@napi-rs/canvas-darwin-x64@1.0.9':
+ '@napi-rs/canvas-darwin-x64@1.0.10':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.80':
optional: true
- '@napi-rs/canvas-linux-arm-gnueabihf@1.0.9':
+ '@napi-rs/canvas-linux-arm-gnueabihf@1.0.10':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.80':
optional: true
- '@napi-rs/canvas-linux-arm64-gnu@1.0.9':
+ '@napi-rs/canvas-linux-arm64-gnu@1.0.10':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.80':
optional: true
- '@napi-rs/canvas-linux-arm64-musl@1.0.9':
+ '@napi-rs/canvas-linux-arm64-musl@1.0.10':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.80':
optional: true
- '@napi-rs/canvas-linux-riscv64-gnu@1.0.9':
+ '@napi-rs/canvas-linux-riscv64-gnu@1.0.10':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.80':
optional: true
- '@napi-rs/canvas-linux-x64-gnu@1.0.9':
+ '@napi-rs/canvas-linux-x64-gnu@1.0.10':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.80':
optional: true
- '@napi-rs/canvas-linux-x64-musl@1.0.9':
+ '@napi-rs/canvas-linux-x64-musl@1.0.10':
optional: true
- '@napi-rs/canvas-win32-arm64-msvc@1.0.9':
+ '@napi-rs/canvas-win32-arm64-msvc@1.0.10':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.80':
optional: true
- '@napi-rs/canvas-win32-x64-msvc@1.0.9':
+ '@napi-rs/canvas-win32-x64-msvc@1.0.10':
optional: true
'@napi-rs/canvas@0.1.80':
@@ -7952,26 +7848,19 @@ snapshots:
'@napi-rs/canvas-linux-x64-musl': 0.1.80
'@napi-rs/canvas-win32-x64-msvc': 0.1.80
- '@napi-rs/canvas@1.0.9':
+ '@napi-rs/canvas@1.0.10':
optionalDependencies:
- '@napi-rs/canvas-android-arm64': 1.0.9
- '@napi-rs/canvas-darwin-arm64': 1.0.9
- '@napi-rs/canvas-darwin-x64': 1.0.9
- '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.9
- '@napi-rs/canvas-linux-arm64-gnu': 1.0.9
- '@napi-rs/canvas-linux-arm64-musl': 1.0.9
- '@napi-rs/canvas-linux-riscv64-gnu': 1.0.9
- '@napi-rs/canvas-linux-x64-gnu': 1.0.9
- '@napi-rs/canvas-linux-x64-musl': 1.0.9
- '@napi-rs/canvas-win32-arm64-msvc': 1.0.9
- '@napi-rs/canvas-win32-x64-msvc': 1.0.9
-
- '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)':
- dependencies:
- '@emnapi/core': 1.11.2
- '@emnapi/runtime': 1.11.2
- '@tybys/wasm-util': 0.10.3
- optional: true
+ '@napi-rs/canvas-android-arm64': 1.0.10
+ '@napi-rs/canvas-darwin-arm64': 1.0.10
+ '@napi-rs/canvas-darwin-x64': 1.0.10
+ '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.10
+ '@napi-rs/canvas-linux-arm64-gnu': 1.0.10
+ '@napi-rs/canvas-linux-arm64-musl': 1.0.10
+ '@napi-rs/canvas-linux-riscv64-gnu': 1.0.10
+ '@napi-rs/canvas-linux-x64-gnu': 1.0.10
+ '@napi-rs/canvas-linux-x64-musl': 1.0.10
+ '@napi-rs/canvas-win32-arm64-msvc': 1.0.10
+ '@napi-rs/canvas-win32-x64-msvc': 1.0.10
'@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
@@ -7987,102 +7876,109 @@ snapshots:
'@tybys/wasm-util': 0.10.4
optional: true
- '@next/bundle-analyzer@16.3.3':
+ '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)':
+ dependencies:
+ '@emnapi/core': 1.11.2
+ '@emnapi/runtime': 1.11.2
+ '@tybys/wasm-util': 0.10.4
+ optional: true
+
+ '@next/bundle-analyzer@16.3.8':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
- '@next/env@16.3.6': {}
+ '@next/env@16.3.8': {}
- '@next/eslint-plugin-next@16.3.6(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))':
+ '@next/eslint-plugin-next@16.3.8(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
fast-glob: 3.3.1
transitivePeerDependencies:
- eslint
- '@next/swc-darwin-arm64@16.3.6':
+ '@next/swc-darwin-arm64@16.3.8':
optional: true
- '@next/swc-darwin-x64@16.3.6':
+ '@next/swc-darwin-x64@16.3.8':
optional: true
- '@next/swc-linux-arm64-gnu@16.3.6':
+ '@next/swc-linux-arm64-gnu@16.3.8':
optional: true
- '@next/swc-linux-arm64-musl@16.3.6':
+ '@next/swc-linux-arm64-musl@16.3.8':
optional: true
- '@next/swc-linux-x64-gnu@16.3.6':
+ '@next/swc-linux-x64-gnu@16.3.8':
optional: true
- '@next/swc-linux-x64-musl@16.3.6':
+ '@next/swc-linux-x64-musl@16.3.8':
optional: true
- '@next/swc-win32-arm64-msvc@16.3.6':
+ '@next/swc-win32-arm64-msvc@16.3.8':
optional: true
- '@next/swc-win32-x64-msvc@16.3.6':
+ '@next/swc-win32-x64-msvc@16.3.8':
optional: true
- '@noble/hashes@2.2.0': {}
+ '@noble/hashes@2.4.0': {}
- '@node-rs/argon2-android-arm-eabi@2.1.0':
+ '@node-rs/argon2-android-arm-eabi@2.2.2':
optional: true
- '@node-rs/argon2-android-arm64@2.1.0':
+ '@node-rs/argon2-android-arm64@2.2.2':
optional: true
- '@node-rs/argon2-darwin-arm64@2.1.0':
+ '@node-rs/argon2-darwin-arm64@2.2.2':
optional: true
- '@node-rs/argon2-darwin-x64@2.1.0':
+ '@node-rs/argon2-darwin-x64@2.2.2':
optional: true
- '@node-rs/argon2-freebsd-x64@2.1.0':
+ '@node-rs/argon2-freebsd-x64@2.2.2':
optional: true
- '@node-rs/argon2-linux-arm-gnueabihf@2.1.0':
+ '@node-rs/argon2-linux-arm-gnueabihf@2.2.2':
optional: true
- '@node-rs/argon2-linux-arm64-gnu@2.1.0':
+ '@node-rs/argon2-linux-arm64-gnu@2.2.2':
optional: true
- '@node-rs/argon2-linux-arm64-musl@2.1.0':
+ '@node-rs/argon2-linux-arm64-musl@2.2.2':
optional: true
- '@node-rs/argon2-linux-x64-gnu@2.1.0':
+ '@node-rs/argon2-linux-x64-gnu@2.2.2':
optional: true
- '@node-rs/argon2-linux-x64-musl@2.1.0':
+ '@node-rs/argon2-linux-x64-musl@2.2.2':
optional: true
- '@node-rs/argon2-win32-arm64-msvc@2.1.0':
+ '@node-rs/argon2-win32-arm64-msvc@2.2.2':
optional: true
- '@node-rs/argon2-win32-ia32-msvc@2.1.0':
+ '@node-rs/argon2-win32-ia32-msvc@2.2.2':
optional: true
- '@node-rs/argon2-win32-x64-msvc@2.1.0':
+ '@node-rs/argon2-win32-x64-msvc@2.2.2':
optional: true
- '@node-rs/argon2@2.1.0':
+ '@node-rs/argon2@2.2.2':
optionalDependencies:
- '@node-rs/argon2-android-arm-eabi': 2.1.0
- '@node-rs/argon2-android-arm64': 2.1.0
- '@node-rs/argon2-darwin-arm64': 2.1.0
- '@node-rs/argon2-darwin-x64': 2.1.0
- '@node-rs/argon2-freebsd-x64': 2.1.0
- '@node-rs/argon2-linux-arm-gnueabihf': 2.1.0
- '@node-rs/argon2-linux-arm64-gnu': 2.1.0
- '@node-rs/argon2-linux-arm64-musl': 2.1.0
- '@node-rs/argon2-linux-x64-gnu': 2.1.0
- '@node-rs/argon2-linux-x64-musl': 2.1.0
- '@node-rs/argon2-win32-arm64-msvc': 2.1.0
- '@node-rs/argon2-win32-ia32-msvc': 2.1.0
- '@node-rs/argon2-win32-x64-msvc': 2.1.0
+ '@node-rs/argon2-android-arm-eabi': 2.2.2
+ '@node-rs/argon2-android-arm64': 2.2.2
+ '@node-rs/argon2-darwin-arm64': 2.2.2
+ '@node-rs/argon2-darwin-x64': 2.2.2
+ '@node-rs/argon2-freebsd-x64': 2.2.2
+ '@node-rs/argon2-linux-arm-gnueabihf': 2.2.2
+ '@node-rs/argon2-linux-arm64-gnu': 2.2.2
+ '@node-rs/argon2-linux-arm64-musl': 2.2.2
+ '@node-rs/argon2-linux-x64-gnu': 2.2.2
+ '@node-rs/argon2-linux-x64-musl': 2.2.2
+ '@node-rs/argon2-win32-arm64-msvc': 2.2.2
+ '@node-rs/argon2-win32-ia32-msvc': 2.2.2
+ '@node-rs/argon2-win32-x64-msvc': 2.2.2
'@nodelib/fs.scandir@2.1.5':
dependencies:
@@ -8094,7 +7990,7 @@ snapshots:
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
- fastq: 1.20.1
+ fastq: 1.20.3
'@nolyfill/is-core-module@1.0.39': {}
@@ -8113,66 +8009,68 @@ snapshots:
'@open-draft/until@2.1.0':
optional: true
- '@oxc-parser/binding-android-arm-eabi@0.143.0':
+ '@opentelemetry/api@1.9.1': {}
+
+ '@oxc-parser/binding-android-arm-eabi@0.150.0':
optional: true
- '@oxc-parser/binding-android-arm64@0.143.0':
+ '@oxc-parser/binding-android-arm64@0.150.0':
optional: true
- '@oxc-parser/binding-darwin-arm64@0.143.0':
+ '@oxc-parser/binding-darwin-arm64@0.150.0':
optional: true
- '@oxc-parser/binding-darwin-x64@0.143.0':
+ '@oxc-parser/binding-darwin-x64@0.150.0':
optional: true
- '@oxc-parser/binding-freebsd-x64@0.143.0':
+ '@oxc-parser/binding-freebsd-x64@0.150.0':
optional: true
- '@oxc-parser/binding-linux-arm-gnueabihf@0.143.0':
+ '@oxc-parser/binding-linux-arm-gnueabihf@0.150.0':
optional: true
- '@oxc-parser/binding-linux-arm-musleabihf@0.143.0':
+ '@oxc-parser/binding-linux-arm-musleabihf@0.150.0':
optional: true
- '@oxc-parser/binding-linux-arm64-gnu@0.143.0':
+ '@oxc-parser/binding-linux-arm64-gnu@0.150.0':
optional: true
- '@oxc-parser/binding-linux-arm64-musl@0.143.0':
+ '@oxc-parser/binding-linux-arm64-musl@0.150.0':
optional: true
- '@oxc-parser/binding-linux-ppc64-gnu@0.143.0':
+ '@oxc-parser/binding-linux-ppc64-gnu@0.150.0':
optional: true
- '@oxc-parser/binding-linux-riscv64-gnu@0.143.0':
+ '@oxc-parser/binding-linux-riscv64-gnu@0.150.0':
optional: true
- '@oxc-parser/binding-linux-riscv64-musl@0.143.0':
+ '@oxc-parser/binding-linux-riscv64-musl@0.150.0':
optional: true
- '@oxc-parser/binding-linux-s390x-gnu@0.143.0':
+ '@oxc-parser/binding-linux-s390x-gnu@0.150.0':
optional: true
- '@oxc-parser/binding-linux-x64-gnu@0.143.0':
+ '@oxc-parser/binding-linux-x64-gnu@0.150.0':
optional: true
- '@oxc-parser/binding-linux-x64-musl@0.143.0':
+ '@oxc-parser/binding-linux-x64-musl@0.150.0':
optional: true
- '@oxc-parser/binding-openharmony-arm64@0.143.0':
+ '@oxc-parser/binding-openharmony-arm64@0.150.0':
optional: true
- '@oxc-parser/binding-win32-arm64-msvc@0.143.0':
+ '@oxc-parser/binding-win32-arm64-msvc@0.150.0':
optional: true
- '@oxc-parser/binding-win32-ia32-msvc@0.143.0':
+ '@oxc-parser/binding-win32-ia32-msvc@0.150.0':
optional: true
- '@oxc-parser/binding-win32-x64-msvc@0.143.0':
+ '@oxc-parser/binding-win32-x64-msvc@0.150.0':
optional: true
'@oxc-project/types@0.139.0': {}
- '@oxc-project/types@0.143.0': {}
+ '@oxc-project/types@0.150.0': {}
'@oxc-resolver/binding-android-arm-eabi@11.24.2':
optional: true
@@ -8226,7 +8124,7 @@ snapshots:
dependencies:
'@emnapi/core': 1.11.2
'@emnapi/runtime': 1.11.2
- '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)
+ '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)
optional: true
'@oxc-resolver/binding-win32-arm64-msvc@11.24.2':
@@ -8363,9 +8261,9 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
- '@playwright/test@1.63.0':
+ '@playwright/test@1.64.0':
dependencies:
- playwright: 1.63.0
+ playwright: 1.64.0
'@polka/url@1.0.0-next.29': {}
@@ -8373,23 +8271,23 @@ snapshots:
dependencies:
'@prisma/driver-adapter-utils': 7.10.0
'@types/pg': 8.23.1
- pg: 8.23.0
+ pg: 8.23.1
postgres-array: 3.0.4
transitivePeerDependencies:
- pg-native
'@prisma/client-runtime-utils@7.10.0': {}
- '@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.6.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)':
+ '@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.6.4)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)':
dependencies:
'@prisma/client-runtime-utils': 7.10.0
optionalDependencies:
- prisma: 7.10.0(@types/node@26.6.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
+ prisma: 7.10.0(@types/node@26.6.4)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
typescript: 6.0.3
- '@prisma/config@7.10.0(magicast@0.5.2)':
+ '@prisma/config@7.10.0(magicast@0.5.5)':
dependencies:
- c12: 3.3.4(magicast@0.5.2)
+ c12: 3.3.4(magicast@0.5.5)
deepmerge-ts: 8.0.2
effect: 3.20.0
empathic: 2.0.0
@@ -8501,25 +8399,25 @@ snapshots:
'@radix-ui/primitive@1.1.7': {}
- '@radix-ui/react-accessible-icon@1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-accessible-icon@1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-visually-hidden': 1.2.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-accordion@1.2.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-accordion@1.2.21(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collapsible': 1.1.21(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8527,42 +8425,42 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-alert-dialog@1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-alert-dialog@1.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-dialog': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-arrow@1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-aspect-ratio@1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-aspect-ratio@1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-avatar@1.2.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-avatar@1.2.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
@@ -8572,29 +8470,29 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-checkbox@1.3.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-checkbox@1.3.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-use-size': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-collapsible@1.1.21(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
@@ -8603,12 +8501,12 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-collection@1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-collection@1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
@@ -8627,12 +8525,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-context-menu@2.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-context-menu@2.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-menu': 2.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8646,19 +8544,19 @@ snapshots:
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-dialog@1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-focus-guards': 1.1.6(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-focus-scope': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
aria-hidden: 1.2.6
@@ -8669,17 +8567,17 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-direction@1.1.4(@types/react@19.3.0)(react@19.3.0)':
+ '@radix-ui/react-direction@1.1.5(@types/react@19.3.0)(react@19.3.0)':
dependencies:
react: 19.3.0
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-dismissable-layer@1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
@@ -8688,14 +8586,14 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-dropdown-menu@2.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-menu': 2.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8709,10 +8607,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-focus-scope@1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8720,30 +8618,31 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-form@0.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-form@0.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-label': 2.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-label': 2.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-hover-card@1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-hover-card@1.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-focus-scope': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-popper': 1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8758,32 +8657,32 @@ snapshots:
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-label@2.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-label@2.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-menu@2.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-menu@2.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-focus-guards': 1.1.6(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-focus-scope': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-popper': 1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
aria-hidden: 1.2.6
react: 19.3.0
@@ -8793,17 +8692,17 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-menubar@1.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-menubar@1.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-menu': 2.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8811,38 +8710,39 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-navigation-menu@1.2.22(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-navigation-menu@1.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-previous': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-visually-hidden': 1.2.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-one-time-password-field@0.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-one-time-password-field@0.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/number': 1.1.3
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.0)
@@ -8853,13 +8753,13 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-password-toggle-field@0.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-password-toggle-field@0.1.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.0)
@@ -8869,21 +8769,22 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-popover@1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-popover@1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-focus-guards': 1.1.6(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-focus-scope': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-popper': 1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
aria-hidden: 1.2.6
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8892,17 +8793,17 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-popper@1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-popper@1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@floating-ui/react-dom': 2.1.9(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-arrow': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-rect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-use-size': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/rect': 1.1.3
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8910,9 +8811,9 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-portal@1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-portal@1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -8920,7 +8821,7 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-presence@1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-presence@1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
@@ -8929,9 +8830,9 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-primitive@2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
@@ -8947,42 +8848,42 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-progress@1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-progress@1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-radio-group@1.4.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-radio-group@1.4.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-use-size': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-roving-focus@1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.0)
@@ -8993,15 +8894,16 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-scroll-area@1.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/number': 1.1.3
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
@@ -9010,28 +8912,28 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-select@2.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-select@2.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/number': 1.1.3
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-focus-guards': 1.1.6(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-focus-scope': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-popper': 1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-previous': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-visually-hidden': 1.2.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
aria-hidden: 1.2.6
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -9040,28 +8942,28 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-separator@1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-separator@1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-slider@1.4.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-slider@1.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/number': 1.1.3
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-previous': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-use-size': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
@@ -9075,36 +8977,36 @@ snapshots:
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-slot@1.3.3(@types/react@19.3.0)(react@19.3.0)':
+ '@radix-ui/react-slot@1.4.0(@types/react@19.3.0)(react@19.3.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-switch@1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-switch@1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-use-size': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-tabs@1.1.21(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-tabs@1.1.22(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -9112,34 +9014,34 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-toast@1.2.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-toast@1.2.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-visually-hidden': 1.2.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-toggle-group@1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-toggle-group@1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-toggle': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -9158,10 +9060,10 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-toggle@1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-toggle@1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
@@ -9169,36 +9071,36 @@ snapshots:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-toolbar@1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-toolbar@1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-separator': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-toggle-group': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
'@types/react': 19.3.0
'@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-tooltip@1.2.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-tooltip@1.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-id': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-popper': 1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-visually-hidden': 1.2.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
@@ -9280,16 +9182,16 @@ snapshots:
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-use-size@1.1.4(@types/react@19.3.0)(react@19.3.0)':
+ '@radix-ui/react-use-size@1.1.5(@types/react@19.3.0)(react@19.3.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
react: 19.3.0
optionalDependencies:
'@types/react': 19.3.0
- '@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@radix-ui/react-visually-hidden@1.2.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
@@ -9365,9 +9267,37 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
+ '@shadcn/registry@0.1.3(supports-color@7.2.0)':
+ dependencies:
+ '@babel/core': 7.29.7(supports-color@7.2.0)
+ '@babel/parser': 7.29.9
+ '@babel/plugin-transform-typescript': 7.29.9(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
+ '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
+ '@dotenvx/dotenvx': 1.75.1
+ cn: 0.2.6
+ code-block-writer: 13.0.3
+ deepmerge: 4.3.1
+ execa: 9.6.1
+ fast-glob: 3.3.3
+ fs-extra: 11.4.1
+ fuzzysort: 3.1.0
+ kleur: 4.1.5
+ ora: 8.2.0
+ postcss: 8.5.29
+ prompts: 2.4.2
+ recast: 0.23.21
+ socks: 2.8.10
+ stringify-object: 5.0.0
+ tsconfig-paths: 4.2.0
+ type-fest: 4.41.0
+ undici: 7.30.0
+ zod: 3.25.76
+ transitivePeerDependencies:
+ - supports-color
+
'@simplewebauthn/browser@14.0.0': {}
- '@simplewebauthn/server@14.0.2':
+ '@simplewebauthn/server@14.0.3':
dependencies:
'@hexagon/base64': 1.1.28
'@levischuck/tiny-cbor': 0.2.11
@@ -9382,61 +9312,47 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
- '@smithy/core@3.26.0':
- dependencies:
- '@aws-crypto/crc32': 5.2.0
- '@smithy/types': 4.18.0
- tslib: 2.8.1
-
'@smithy/core@3.33.3':
dependencies:
- '@smithy/types': 4.18.0
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@smithy/credential-provider-imds@4.4.2':
+ '@smithy/core@3.35.1':
+ dependencies:
+ '@smithy/types': 4.19.0
+ tslib: 2.8.1
+
+ '@smithy/core@3.35.2':
+ dependencies:
+ '@smithy/types': 4.19.0
+ tslib: 2.8.1
+
+ '@smithy/credential-provider-imds@4.5.2':
+ dependencies:
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
+ tslib: 2.8.1
+
+ '@smithy/fetch-http-handler@5.8.0':
dependencies:
'@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@smithy/fetch-http-handler@5.5.2':
+ '@smithy/node-http-handler@4.12.1':
dependencies:
'@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@smithy/is-array-buffer@2.2.0':
+ '@smithy/signature-v4@5.7.4':
dependencies:
+ '@smithy/core': 3.35.1
+ '@smithy/types': 4.19.0
tslib: 2.8.1
- '@smithy/node-http-handler@4.8.2':
+ '@smithy/types@4.19.0':
dependencies:
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
- tslib: 2.8.1
-
- '@smithy/signature-v4@5.5.2':
- dependencies:
- '@smithy/core': 3.33.3
- '@smithy/types': 4.18.0
- tslib: 2.8.1
-
- '@smithy/types@4.15.0':
- dependencies:
- tslib: 2.8.1
-
- '@smithy/types@4.18.0':
- dependencies:
- tslib: 2.8.1
-
- '@smithy/util-buffer-from@2.2.0':
- dependencies:
- '@smithy/is-array-buffer': 2.2.0
- tslib: 2.8.1
-
- '@smithy/util-utf8@2.3.0':
- dependencies:
- '@smithy/util-buffer-from': 2.2.0
tslib: 2.8.1
'@standard-schema/spec@1.1.0': {}
@@ -9454,7 +9370,7 @@ snapshots:
jiti: 2.7.0
lightningcss: 1.32.0
magic-string: 0.30.21
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
tailwindcss: 4.3.3
'@tailwindcss/oxide-android-arm64@4.3.3':
@@ -9516,28 +9432,28 @@ snapshots:
postcss: 8.5.26
tailwindcss: 4.3.3
- '@tanstack/query-core@5.103.2': {}
+ '@tanstack/query-core@5.104.1': {}
- '@tanstack/react-query@5.103.2(react@19.3.0)':
+ '@tanstack/react-query@5.104.1(react@19.3.0)':
dependencies:
- '@tanstack/query-core': 5.103.2
+ '@tanstack/query-core': 5.104.1
react: 19.3.0
- '@tanstack/react-virtual@3.14.12(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ '@tanstack/react-virtual@3.14.13(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@tanstack/virtual-core': 3.17.10
+ '@tanstack/virtual-core': 3.17.11
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
- '@tanstack/virtual-core@3.17.10': {}
+ '@tanstack/virtual-core@3.17.11': {}
'@tesseract.js-data/deu@1.0.0': {}
'@tesseract.js-data/eng@1.0.0': {}
- '@testcontainers/postgresql@12.1.0(supports-color@7.2.0)':
+ '@testcontainers/postgresql@12.2.0(supports-color@7.2.0)':
dependencies:
- testcontainers: 12.1.0(supports-color@7.2.0)
+ testcontainers: 12.2.0(supports-color@7.2.0)
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
@@ -9550,11 +9466,6 @@ snapshots:
minimatch: 10.2.6
path-browserify: 1.0.1
- '@tybys/wasm-util@0.10.3':
- dependencies:
- tslib: 2.8.1
- optional: true
-
'@tybys/wasm-util@0.10.4':
dependencies:
tslib: 2.8.1
@@ -9621,14 +9532,14 @@ snapshots:
'@types/docker-modem@3.0.6':
dependencies:
- '@types/node': 26.6.2
- '@types/ssh2': 1.15.5
+ '@types/node': 26.6.4
+ '@types/ssh2': 1.15.6
'@types/dockerode@4.0.1':
dependencies:
'@types/docker-modem': 3.0.6
- '@types/node': 26.6.2
- '@types/ssh2': 1.15.5
+ '@types/node': 26.6.4
+ '@types/ssh2': 1.15.6
'@types/estree@1.0.8': {}
@@ -9644,7 +9555,7 @@ snapshots:
dependencies:
undici-types: 5.26.5
- '@types/node@26.6.2':
+ '@types/node@26.6.4':
dependencies:
undici-types: 8.9.0
@@ -9652,13 +9563,13 @@ snapshots:
'@types/pg@8.23.1':
dependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
pg-protocol: 1.16.0
pg-types: 2.2.0
'@types/qrcode@1.5.6':
dependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
'@types/raf@3.4.3':
optional: true
@@ -9673,23 +9584,23 @@ snapshots:
'@types/sax@1.2.7':
dependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
'@types/set-cookie-parser@2.4.10':
dependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
optional: true
'@types/ssh2-streams@0.1.13':
dependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
'@types/ssh2@0.5.52':
dependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
'@types/ssh2-streams': 0.1.13
- '@types/ssh2@1.15.5':
+ '@types/ssh2@1.15.6':
dependencies:
'@types/node': 18.19.130
@@ -9705,59 +9616,59 @@ snapshots:
'@types/web-push@3.6.4':
dependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
- '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+ '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.70.0
- '@typescript-eslint/type-utils': 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/utils': 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.70.0
+ '@typescript-eslint/parser': 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.71.0
+ '@typescript-eslint/type-utils': 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.71.0
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
- ignore: 7.0.9
+ ignore: 7.0.11
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+ '@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.70.0
- '@typescript-eslint/types': 8.70.0
- '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.70.0
+ '@typescript-eslint/scope-manager': 8.71.0
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.71.0
debug: 4.4.3(supports-color@7.2.0)
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.70.0(supports-color@7.2.0)(typescript@6.0.3)':
+ '@typescript-eslint/project-service@8.71.0(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
- '@typescript-eslint/types': 8.70.0
+ '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.71.0
debug: 4.4.3(supports-color@7.2.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.70.0':
+ '@typescript-eslint/scope-manager@8.71.0':
dependencies:
- '@typescript-eslint/types': 8.70.0
- '@typescript-eslint/visitor-keys': 8.70.0
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/visitor-keys': 8.71.0
- '@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)':
+ '@typescript-eslint/tsconfig-utils@8.71.0(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
- '@typescript-eslint/type-utils@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+ '@typescript-eslint/type-utils@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/types': 8.70.0
- '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/utils': 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
debug: 4.4.3(supports-color@7.2.0)
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
ts-api-utils: 2.5.0(typescript@6.0.3)
@@ -9765,14 +9676,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.70.0': {}
+ '@typescript-eslint/types@8.71.0': {}
- '@typescript-eslint/typescript-estree@8.70.0(supports-color@7.2.0)(typescript@6.0.3)':
+ '@typescript-eslint/typescript-estree@8.71.0(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/project-service': 8.70.0(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
- '@typescript-eslint/types': 8.70.0
- '@typescript-eslint/visitor-keys': 8.70.0
+ '@typescript-eslint/project-service': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/visitor-keys': 8.71.0
debug: 4.4.3(supports-color@7.2.0)
minimatch: 10.2.6
semver: 7.8.5
@@ -9782,20 +9693,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+ '@typescript-eslint/utils@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
- '@typescript-eslint/scope-manager': 8.70.0
- '@typescript-eslint/types': 8.70.0
- '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.71.0
+ '@typescript-eslint/types': 8.71.0
+ '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.70.0':
+ '@typescript-eslint/visitor-keys@8.71.0':
dependencies:
- '@typescript-eslint/types': 8.70.0
+ '@typescript-eslint/types': 8.71.0
eslint-visitor-keys: 5.0.1
'@unrs/resolver-binding-android-arm-eabi@1.12.2':
@@ -9945,76 +9856,40 @@ snapshots:
d3-time-format: 4.1.0
internmap: 2.0.3
- '@vitejs/plugin-react@6.1.1(vite@8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4))':
+ '@vitejs/plugin-react@6.1.2(vite@8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))':
dependencies:
'@rolldown/pluginutils': 1.0.1
- vite: 8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4)
+ vite: 8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)
- '@vitest/coverage-v8@4.1.5(vitest@4.1.11)':
+ '@vitest/coverage-v8@5.0.3(vitest@5.0.3)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
- '@vitest/utils': 4.1.5
- ast-v8-to-istanbul: 1.0.0
- istanbul-lib-coverage: 3.2.2
- istanbul-lib-report: 3.0.1
- istanbul-reports: 3.2.0
- magicast: 0.5.2
- obug: 2.1.1
- std-env: 4.1.0
- tinyrainbow: 3.1.0
- vitest: 4.1.11(@types/node@26.6.2)(@vitest/coverage-v8@4.1.5)(msw@2.14.6(@types/node@26.6.2)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4))
+ '@vitest/istanbul-lib-coverage': 1.0.2
+ '@vitest/istanbul-lib-report': 1.0.2
+ ast-v8-to-istanbul: 1.0.7
+ magicast: 0.5.5
+ obug: 2.2.1
+ std-env: 4.3.0
+ tinyrainbow: 3.2.0
+ vitest: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(msw@2.14.6(@types/node@26.6.4)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))
- '@vitest/expect@4.1.11':
- dependencies:
- '@standard-schema/spec': 1.1.0
- '@types/chai': 5.2.3
- '@vitest/spy': 4.1.11
- '@vitest/utils': 4.1.11
- chai: 6.2.2
- tinyrainbow: 3.1.0
+ '@vitest/istanbul-lib-coverage@1.0.2': {}
- '@vitest/mocker@4.1.11(msw@2.14.6(@types/node@26.6.2)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4))':
+ '@vitest/istanbul-lib-report@1.0.2':
dependencies:
- '@vitest/spy': 4.1.11
+ '@vitest/istanbul-lib-coverage': 1.0.2
+
+ '@vitest/mocker@5.0.3(msw@2.14.6(@types/node@26.6.4)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))':
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ '@vitest/spy': 5.0.3
estree-walker: 3.0.3
- magic-string: 0.30.21
+ magic-string: 1.4.3
optionalDependencies:
- msw: 2.14.6(@types/node@26.6.2)(typescript@6.0.3)
- vite: 8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4)
+ msw: 2.14.6(@types/node@26.6.4)(typescript@6.0.3)
+ vite: 8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)
- '@vitest/pretty-format@4.1.11':
- dependencies:
- tinyrainbow: 3.1.0
-
- '@vitest/pretty-format@4.1.5':
- dependencies:
- tinyrainbow: 3.1.0
-
- '@vitest/runner@4.1.11':
- dependencies:
- '@vitest/utils': 4.1.11
- pathe: 2.0.3
-
- '@vitest/snapshot@4.1.11':
- dependencies:
- '@vitest/pretty-format': 4.1.11
- '@vitest/utils': 4.1.11
- magic-string: 0.30.21
- pathe: 2.0.3
-
- '@vitest/spy@4.1.11': {}
-
- '@vitest/utils@4.1.11':
- dependencies:
- '@vitest/pretty-format': 4.1.11
- convert-source-map: 2.0.0
- tinyrainbow: 3.1.0
-
- '@vitest/utils@4.1.5':
- dependencies:
- '@vitest/pretty-format': 4.1.5
- convert-source-map: 2.0.0
- tinyrainbow: 3.1.0
+ '@vitest/spy@5.0.3': {}
abort-controller@3.0.0:
dependencies:
@@ -10031,11 +9906,11 @@ snapshots:
acorn-walk@8.3.5:
dependencies:
- acorn: 8.18.0
+ acorn: 8.19.0
acorn@8.16.0: {}
- acorn@8.18.0: {}
+ acorn@8.19.0: {}
agent-base@7.1.4: {}
@@ -10065,7 +9940,7 @@ snapshots:
ansi-regex@5.0.1: {}
- ansi-regex@6.2.2: {}
+ ansi-regex@6.4.0: {}
ansi-styles@4.3.0:
dependencies:
@@ -10090,7 +9965,7 @@ snapshots:
buffer-crc32: 1.0.0
readable-stream: 4.7.0
readdir-glob: 1.1.3
- tar-stream: 3.2.0
+ tar-stream: 3.2.1
zip-stream: 6.0.1
transitivePeerDependencies:
- bare-abort-controller
@@ -10195,11 +10070,11 @@ snapshots:
ast-types-flow@0.0.8: {}
- ast-types@0.16.1:
+ ast-types@0.16.3:
dependencies:
tslib: 2.8.1
- ast-v8-to-istanbul@1.0.0:
+ ast-v8-to-istanbul@1.0.7:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
@@ -10223,47 +10098,49 @@ snapshots:
axobject-query@4.1.0: {}
- b4a@1.8.1: {}
+ b4a@1.9.0: {}
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
- bare-events@2.9.1: {}
+ bare-events@2.9.2: {}
- bare-fs@4.8.0:
+ bare-fs@4.8.2:
dependencies:
- bare-events: 2.9.1
- bare-path: 3.1.1
- bare-stream: 2.13.3(bare-events@2.9.1)
- bare-url: 2.5.2
+ bare-events: 2.9.2
+ bare-path: 3.1.2
+ bare-stream: 2.13.4(bare-events@2.9.2)
+ bare-url: 2.5.4
fast-fifo: 1.3.2
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
- bare-path@3.1.1: {}
+ bare-path@3.1.2: {}
- bare-stream@2.13.3(bare-events@2.9.1):
+ bare-stream@2.13.4(bare-events@2.9.2):
dependencies:
- b4a: 1.8.1
- streamx: 2.28.0
+ b4a: 1.9.0
+ streamx: 2.28.1
teex: 1.0.1
optionalDependencies:
- bare-events: 2.9.1
+ bare-events: 2.9.2
transitivePeerDependencies:
- react-native-b4a
- bare-url@2.5.2:
+ bare-url@2.5.4:
dependencies:
- bare-path: 3.1.1
+ bare-path: 3.1.2
base64-arraybuffer@1.0.2:
optional: true
base64-js@1.5.1: {}
- baseline-browser-mapping@2.11.11: {}
+ baseline-browser-mapping@2.11.26: {}
+
+ baseline-browser-mapping@2.11.27: {}
bcrypt-pbkdf@1.0.2:
dependencies:
@@ -10314,13 +10191,13 @@ snapshots:
dependencies:
fill-range: 7.1.1
- browserslist@4.28.7:
+ browserslist@4.29.3:
dependencies:
- baseline-browser-mapping: 2.11.11
- caniuse-lite: 1.0.30001806
- electron-to-chromium: 1.5.399
- node-releases: 2.0.51
- update-browserslist-db: 1.2.3(browserslist@4.28.7)
+ baseline-browser-mapping: 2.11.27
+ caniuse-lite: 1.0.30001815
+ electron-to-chromium: 1.5.451
+ node-releases: 2.0.58
+ update-browserslist-db: 1.3.4(browserslist@4.29.3)
buffer-crc32@1.0.0: {}
@@ -10344,7 +10221,7 @@ snapshots:
buildcheck@0.0.7:
optional: true
- bundle-name@4.1.0:
+ bundle-name@4.1.1:
dependencies:
run-applescript: 7.1.0
@@ -10352,7 +10229,7 @@ snapshots:
bytes@3.1.2: {}
- c12@3.3.4(magicast@0.5.2):
+ c12@3.3.4(magicast@0.5.5):
dependencies:
chokidar: 5.0.0
confbox: 0.2.4
@@ -10367,7 +10244,7 @@ snapshots:
pkg-types: 2.3.3
rc9: 3.1.0
optionalDependencies:
- magicast: 0.5.2
+ magicast: 0.5.5
call-bind-apply-helpers@1.0.2:
dependencies:
@@ -10390,7 +10267,9 @@ snapshots:
camelcase@5.3.1: {}
- caniuse-lite@1.0.30001806: {}
+ caniuse-lite@1.0.30001814: {}
+
+ caniuse-lite@1.0.30001815: {}
canvg@3.0.11:
dependencies:
@@ -10450,6 +10329,8 @@ snapshots:
clsx@2.1.1: {}
+ cn@0.2.6: {}
+
code-block-writer@13.0.3: {}
color-convert@2.0.1:
@@ -10518,19 +10399,10 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
- cosmiconfig@9.0.2(typescript@6.0.3):
- dependencies:
- env-paths: 2.2.1
- import-fresh: 3.3.1
- js-yaml: 4.3.2
- parse-json: 5.2.0
- optionalDependencies:
- typescript: 6.0.3
-
cpu-features@0.0.10:
dependencies:
buildcheck: 0.0.7
- nan: 2.28.0
+ nan: 2.29.0
optional: true
crc-32@1.2.2: {}
@@ -10665,9 +10537,9 @@ snapshots:
default-browser-id@5.0.1: {}
- default-browser@5.5.0:
+ default-browser@5.5.1:
dependencies:
- bundle-name: 4.1.0
+ bundle-name: 4.1.1
default-browser-id: 5.0.1
define-data-property@1.1.4:
@@ -10704,9 +10576,9 @@ snapshots:
dijkstrajs@1.0.3: {}
- docker-compose@1.4.2:
+ docker-compose@1.5.0:
dependencies:
- yaml: 2.9.0
+ yaml: 2.9.1
docker-modem@5.0.7(supports-color@7.2.0):
dependencies:
@@ -10723,7 +10595,7 @@ snapshots:
'@grpc/grpc-js': 1.14.5
'@grpc/proto-loader': 0.7.15
docker-modem: 5.0.7(supports-color@7.2.0)
- protobufjs: 7.6.5
+ protobufjs: 7.6.6
tar-fs: 2.1.5
transitivePeerDependencies:
- supports-color
@@ -10743,6 +10615,8 @@ snapshots:
dotenv@17.4.2: {}
+ dotenv@18.0.6: {}
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -10764,7 +10638,7 @@ snapshots:
'@standard-schema/spec': 1.1.0
fast-check: 3.23.2
- electron-to-chromium@1.5.399: {}
+ electron-to-chromium@1.5.451: {}
elkjs@0.11.1: {}
@@ -10796,10 +10670,6 @@ snapshots:
env-paths@3.0.0: {}
- error-ex@1.3.4:
- dependencies:
- is-arrayish: 0.2.1
-
es-abstract-get@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -10862,7 +10732,7 @@ snapshots:
typed-array-byte-offset: 1.0.5
typed-array-length: 1.0.8
unbox-primitive: 1.1.0
- which-typed-array: 1.1.23
+ which-typed-array: 1.1.24
es-define-property@1.0.1: {}
@@ -10887,7 +10757,7 @@ snapshots:
iterator.prototype: 1.1.5
math-intrinsics: 1.1.0
- es-module-lexer@2.1.0: {}
+ es-module-lexer@2.3.2: {}
es-object-atoms@1.1.2:
dependencies:
@@ -10921,18 +10791,18 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-config-next@16.3.6(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
+ eslint-config-next@16.3.8(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
dependencies:
- '@next/eslint-plugin-next': 16.3.6(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
+ '@next/eslint-plugin-next': 16.3.8(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))
eslint-plugin-react-hooks: 7.1.1(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
globals: 16.4.0
- typescript-eslint: 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ typescript-eslint: 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
optionalDependencies:
typescript: 6.0.3
transitivePeerDependencies:
@@ -10960,22 +10830,22 @@ snapshots:
tinyglobby: 0.2.17
unrs-resolver: 1.12.2
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.14.0(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
+ eslint-module-utils@2.14.0(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
debug: 3.2.7(supports-color@7.2.0)
optionalDependencies:
- '@typescript-eslint/parser': 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.2.0
@@ -10986,7 +10856,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
- eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
hasown: 2.0.4
is-core-module: 2.17.0
is-glob: 4.0.3
@@ -10998,7 +10868,7 @@ snapshots:
string.prototype.trimend: 1.0.10
tsconfig-paths: 3.15.0
optionalDependencies:
- '@typescript-eslint/parser': 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -11026,7 +10896,7 @@ snapshots:
eslint-plugin-react-hooks@7.1.1(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
'@babel/core': 7.29.7(supports-color@7.2.0)
- '@babel/parser': 7.29.8
+ '@babel/parser': 7.29.9
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
hermes-parser: 0.25.1
zod: 4.6.5
@@ -11140,7 +11010,7 @@ snapshots:
events-universal@1.0.1:
dependencies:
- bare-events: 2.9.1
+ bare-events: 2.9.2
transitivePeerDependencies:
- bare-abort-controller
@@ -11174,12 +11044,12 @@ snapshots:
is-plain-obj: 4.1.0
is-stream: 4.0.1
npm-run-path: 6.0.0
- pretty-ms: 9.3.0
+ pretty-ms: 9.3.1
signal-exit: 4.1.0
strip-final-newline: 4.0.0
yoctocolors: 2.2.0
- expect-type@1.3.0: {}
+ expect-type@1.4.0: {}
express-rate-limit@8.6.2(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
@@ -11210,7 +11080,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
- proxy-addr: 2.0.7
+ proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.3.0
router: 2.2.0(supports-color@7.2.0)
@@ -11281,7 +11151,7 @@ snapshots:
fast-string-width: 3.0.2
optional: true
- fastq@1.20.1:
+ fastq@1.20.3:
dependencies:
reusify: 1.1.0
@@ -11289,10 +11159,6 @@ snapshots:
dependencies:
walk-up-path: 4.0.0
- fdir@6.5.0(picomatch@4.0.5):
- optionalDependencies:
- picomatch: 4.0.5
-
fdir@6.5.0(picomatch@4.0.7):
optionalDependencies:
picomatch: 4.0.7
@@ -11358,9 +11224,10 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
- formatly@0.3.0:
+ formatly@0.7.1:
dependencies:
fd-package-json: 2.0.0
+ package-manager-detector: 1.9.0
forwarded@0.2.0: {}
@@ -11368,7 +11235,7 @@ snapshots:
fs-constants@1.0.0: {}
- fs-extra@11.4.0:
+ fs-extra@11.4.1:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.1
@@ -11405,7 +11272,7 @@ snapshots:
get-caller-file@2.0.5: {}
- get-east-asian-width@1.6.0: {}
+ get-east-asian-width@1.7.0: {}
get-intrinsic@1.3.0:
dependencies:
@@ -11446,10 +11313,6 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
- get-tsconfig@4.14.1:
- dependencies:
- resolve-pkg-maps: 1.0.0
-
get-tsconfig@4.14.3:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -11572,7 +11435,7 @@ snapshots:
ignore@5.3.2: {}
- ignore@7.0.9: {}
+ ignore@7.0.11: {}
immer@11.1.15: {}
@@ -11599,6 +11462,8 @@ snapshots:
ip-address@10.7.2: {}
+ ip-address@10.7.3: {}
+
ipaddr.js@1.9.1: {}
is-array-buffer@3.0.5:
@@ -11607,8 +11472,6 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
- is-arrayish@0.2.1: {}
-
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@@ -11741,7 +11604,7 @@ snapshots:
is-typed-array@1.1.15:
dependencies:
- which-typed-array: 1.1.23
+ which-typed-array: 1.1.24
is-unicode-supported@1.3.0: {}
@@ -11776,19 +11639,6 @@ snapshots:
isexe@3.1.5: {}
- istanbul-lib-coverage@3.2.2: {}
-
- istanbul-lib-report@3.0.1:
- dependencies:
- istanbul-lib-coverage: 3.2.2
- make-dir: 4.0.0
- supports-color: 7.2.0
-
- istanbul-reports@3.2.0:
- dependencies:
- html-escaper: 2.0.2
- istanbul-lib-report: 3.0.1
-
iterator.prototype@1.1.5:
dependencies:
define-data-property: 1.1.4
@@ -11820,8 +11670,6 @@ snapshots:
json-buffer@3.0.1: {}
- json-parse-even-better-errors@2.3.1: {}
-
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
@@ -11898,21 +11746,21 @@ snapshots:
kleur@4.1.5: {}
- knip@6.32.3:
+ knip@6.40.0:
dependencies:
fdir: 6.5.0(picomatch@4.0.7)
- formatly: 0.3.0
- get-tsconfig: 4.14.1
+ formatly: 0.7.1
+ get-tsconfig: 4.14.3
jiti: 2.7.0
- oxc-parser: 0.143.0
+ oxc-parser: 0.150.0
oxc-resolver: 11.24.2
picomatch: 4.0.7
- smol-toml: 1.8.0
+ smol-toml: 1.9.0
strip-json-comments: 5.0.3
tinyglobby: 0.2.17
- unbash: 4.0.10
- yaml: 2.9.0
- zod: 4.4.3
+ unbash: 4.0.12
+ yaml: 2.9.1
+ zod: 4.6.5
language-subtag-registry@0.3.23: {}
@@ -11924,48 +11772,48 @@ snapshots:
dependencies:
readable-stream: 2.3.8
- lefthook-darwin-arm64@2.1.12:
+ lefthook-darwin-arm64@2.2.1:
optional: true
- lefthook-darwin-x64@2.1.12:
+ lefthook-darwin-x64@2.2.1:
optional: true
- lefthook-freebsd-arm64@2.1.12:
+ lefthook-freebsd-arm64@2.2.1:
optional: true
- lefthook-freebsd-x64@2.1.12:
+ lefthook-freebsd-x64@2.2.1:
optional: true
- lefthook-linux-arm64@2.1.12:
+ lefthook-linux-arm64@2.2.1:
optional: true
- lefthook-linux-x64@2.1.12:
+ lefthook-linux-x64@2.2.1:
optional: true
- lefthook-openbsd-arm64@2.1.12:
+ lefthook-openbsd-arm64@2.2.1:
optional: true
- lefthook-openbsd-x64@2.1.12:
+ lefthook-openbsd-x64@2.2.1:
optional: true
- lefthook-windows-arm64@2.1.12:
+ lefthook-windows-arm64@2.2.1:
optional: true
- lefthook-windows-x64@2.1.12:
+ lefthook-windows-x64@2.2.1:
optional: true
- lefthook@2.1.12:
+ lefthook@2.2.1:
optionalDependencies:
- lefthook-darwin-arm64: 2.1.12
- lefthook-darwin-x64: 2.1.12
- lefthook-freebsd-arm64: 2.1.12
- lefthook-freebsd-x64: 2.1.12
- lefthook-linux-arm64: 2.1.12
- lefthook-linux-x64: 2.1.12
- lefthook-openbsd-arm64: 2.1.12
- lefthook-openbsd-x64: 2.1.12
- lefthook-windows-arm64: 2.1.12
- lefthook-windows-x64: 2.1.12
+ lefthook-darwin-arm64: 2.2.1
+ lefthook-darwin-x64: 2.2.1
+ lefthook-freebsd-arm64: 2.2.1
+ lefthook-freebsd-x64: 2.2.1
+ lefthook-linux-arm64: 2.2.1
+ lefthook-linux-x64: 2.2.1
+ lefthook-openbsd-arm64: 2.2.1
+ lefthook-openbsd-x64: 2.2.1
+ lefthook-windows-arm64: 2.2.1
+ lefthook-windows-x64: 2.2.1
levn@0.4.1:
dependencies:
@@ -12021,8 +11869,6 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
- lines-and-columns@1.2.4: {}
-
locate-path@3.0.0:
dependencies:
p-locate: 3.0.0
@@ -12075,9 +11921,11 @@ snapshots:
lru.min@1.1.5: {}
- lucide-react@1.44.0(react@19.3.0):
+ lucide-react@1.53.0(@types/react@19.3.0)(react@19.3.0):
dependencies:
react: 19.3.0
+ optionalDependencies:
+ '@types/react': 19.3.0
luxon@3.7.2: {}
@@ -12085,15 +11933,15 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
- magicast@0.5.2:
+ magic-string@1.4.3:
dependencies:
- '@babel/parser': 7.29.8
- '@babel/types': 7.29.8
- source-map-js: 1.2.1
+ '@jridgewell/sourcemap-codec': 1.6.0
- make-dir@4.0.0:
+ magicast@0.5.5:
dependencies:
- semver: 7.8.5
+ '@babel/parser': 7.29.9
+ '@babel/types': 7.29.8
+ source-map-js: 1.2.2
math-intrinsics@1.1.0: {}
@@ -12148,15 +11996,15 @@ snapshots:
mkdirp@3.0.1: {}
- mmdb-lib@3.0.2: {}
+ mmdb-lib@3.0.3: {}
mrmime@2.0.1: {}
ms@2.1.3: {}
- msw@2.14.6(@types/node@26.6.2)(typescript@6.0.3):
+ msw@2.14.6(@types/node@26.6.4)(typescript@6.0.3):
dependencies:
- '@inquirer/confirm': 6.1.1(@types/node@26.6.2)
+ '@inquirer/confirm': 6.1.1(@types/node@26.6.4)
'@mswjs/interceptors': 0.41.9
'@open-draft/deferred-promise': 3.0.0
'@types/statuses': 2.0.6
@@ -12183,9 +12031,9 @@ snapshots:
mute-stream@3.0.0:
optional: true
- mysql2@3.24.4(@types/node@26.6.2):
+ mysql2@3.24.4(@types/node@26.6.4):
dependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
aws-ssl-profiles: 1.1.2
generate-function: 2.3.1
iconv-lite: 0.7.3
@@ -12198,11 +12046,15 @@ snapshots:
dependencies:
lru.min: 1.1.5
- nan@2.28.0:
+ nan@2.29.0:
optional: true
nanoid@3.3.18: {}
+ nanoid@3.3.19: {}
+
+ nanoid@3.3.20: {}
+
napi-postinstall@0.3.4: {}
natural-compare@1.4.0: {}
@@ -12214,27 +12066,28 @@ snapshots:
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
- next@16.3.6(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.63.0)(@types/node@26.6.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
+ next@16.3.8(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.64.0)(@types/node@26.6.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
- '@next/env': 16.3.6
+ '@next/env': 16.3.8
'@swc/helpers': 0.5.23
- baseline-browser-mapping: 2.11.11
- caniuse-lite: 1.0.30001806
+ baseline-browser-mapping: 2.11.26
+ caniuse-lite: 1.0.30001814
postcss: 8.5.23
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.3.0)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.3.6
- '@next/swc-darwin-x64': 16.3.6
- '@next/swc-linux-arm64-gnu': 16.3.6
- '@next/swc-linux-arm64-musl': 16.3.6
- '@next/swc-linux-x64-gnu': 16.3.6
- '@next/swc-linux-x64-musl': 16.3.6
- '@next/swc-win32-arm64-msvc': 16.3.6
- '@next/swc-win32-x64-msvc': 16.3.6
- '@playwright/test': 1.63.0
- sharp: 0.35.4(@types/node@26.6.2)
+ '@next/swc-darwin-arm64': 16.3.8
+ '@next/swc-darwin-x64': 16.3.8
+ '@next/swc-linux-arm64-gnu': 16.3.8
+ '@next/swc-linux-arm64-musl': 16.3.8
+ '@next/swc-linux-x64-gnu': 16.3.8
+ '@next/swc-linux-x64-musl': 16.3.8
+ '@next/swc-win32-arm64-msvc': 16.3.8
+ '@next/swc-win32-x64-msvc': 16.3.8
+ '@opentelemetry/api': 1.9.1
+ '@playwright/test': 1.64.0
+ sharp: 0.35.5(@types/node@26.6.4)
transitivePeerDependencies:
- '@babel/core'
- '@types/node'
@@ -12253,9 +12106,9 @@ snapshots:
node-forge@1.4.0: {}
- node-releases@2.0.51: {}
+ node-releases@2.0.58: {}
- nodemailer@10.0.9: {}
+ nodemailer@10.0.16: {}
non-error@0.1.0: {}
@@ -12314,7 +12167,7 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.2
- obug@2.1.1: {}
+ obug@2.2.1: {}
ohash@2.0.12: {}
@@ -12334,14 +12187,14 @@ snapshots:
dependencies:
mimic-function: 5.0.1
- open@11.0.0:
+ open@11.0.4:
dependencies:
- default-browser: 5.5.0
+ default-browser: 5.5.1
define-lazy-prop: 3.0.0
is-in-ssh: 1.0.0
is-inside-container: 1.0.0
- powershell-utils: 0.1.0
- wsl-utils: 0.3.1
+ powershell-utils: 0.2.1
+ wsl-utils: 1.0.1
open@8.4.2:
dependencies:
@@ -12374,9 +12227,9 @@ snapshots:
string-width: 7.2.0
strip-ansi: 7.2.0
- otpauth@9.5.1:
+ otpauth@9.5.2:
dependencies:
- '@noble/hashes': 2.2.0
+ '@noble/hashes': 2.4.0
outvariant@1.4.3:
optional: true
@@ -12388,29 +12241,29 @@ snapshots:
object-keys: 1.1.1
safe-push-apply: 1.0.0
- oxc-parser@0.143.0:
+ oxc-parser@0.150.0:
dependencies:
- '@oxc-project/types': 0.143.0
+ '@oxc-project/types': 0.150.0
optionalDependencies:
- '@oxc-parser/binding-android-arm-eabi': 0.143.0
- '@oxc-parser/binding-android-arm64': 0.143.0
- '@oxc-parser/binding-darwin-arm64': 0.143.0
- '@oxc-parser/binding-darwin-x64': 0.143.0
- '@oxc-parser/binding-freebsd-x64': 0.143.0
- '@oxc-parser/binding-linux-arm-gnueabihf': 0.143.0
- '@oxc-parser/binding-linux-arm-musleabihf': 0.143.0
- '@oxc-parser/binding-linux-arm64-gnu': 0.143.0
- '@oxc-parser/binding-linux-arm64-musl': 0.143.0
- '@oxc-parser/binding-linux-ppc64-gnu': 0.143.0
- '@oxc-parser/binding-linux-riscv64-gnu': 0.143.0
- '@oxc-parser/binding-linux-riscv64-musl': 0.143.0
- '@oxc-parser/binding-linux-s390x-gnu': 0.143.0
- '@oxc-parser/binding-linux-x64-gnu': 0.143.0
- '@oxc-parser/binding-linux-x64-musl': 0.143.0
- '@oxc-parser/binding-openharmony-arm64': 0.143.0
- '@oxc-parser/binding-win32-arm64-msvc': 0.143.0
- '@oxc-parser/binding-win32-ia32-msvc': 0.143.0
- '@oxc-parser/binding-win32-x64-msvc': 0.143.0
+ '@oxc-parser/binding-android-arm-eabi': 0.150.0
+ '@oxc-parser/binding-android-arm64': 0.150.0
+ '@oxc-parser/binding-darwin-arm64': 0.150.0
+ '@oxc-parser/binding-darwin-x64': 0.150.0
+ '@oxc-parser/binding-freebsd-x64': 0.150.0
+ '@oxc-parser/binding-linux-arm-gnueabihf': 0.150.0
+ '@oxc-parser/binding-linux-arm-musleabihf': 0.150.0
+ '@oxc-parser/binding-linux-arm64-gnu': 0.150.0
+ '@oxc-parser/binding-linux-arm64-musl': 0.150.0
+ '@oxc-parser/binding-linux-ppc64-gnu': 0.150.0
+ '@oxc-parser/binding-linux-riscv64-gnu': 0.150.0
+ '@oxc-parser/binding-linux-riscv64-musl': 0.150.0
+ '@oxc-parser/binding-linux-s390x-gnu': 0.150.0
+ '@oxc-parser/binding-linux-x64-gnu': 0.150.0
+ '@oxc-parser/binding-linux-x64-musl': 0.150.0
+ '@oxc-parser/binding-openharmony-arm64': 0.150.0
+ '@oxc-parser/binding-win32-arm64-msvc': 0.150.0
+ '@oxc-parser/binding-win32-ia32-msvc': 0.150.0
+ '@oxc-parser/binding-win32-x64-msvc': 0.150.0
oxc-resolver@11.24.2:
optionalDependencies:
@@ -12442,7 +12295,7 @@ snapshots:
dependencies:
yocto-queue: 0.1.0
- p-limit@7.3.1:
+ p-limit@7.3.3:
dependencies:
yocto-queue: 1.2.2
@@ -12462,19 +12315,14 @@ snapshots:
package-json-from-dist@1.0.1: {}
+ package-manager-detector@1.9.0: {}
+
pako@2.1.0: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
- parse-json@5.2.0:
- dependencies:
- '@babel/code-frame': 7.29.7
- error-ex: 1.3.4
- json-parse-even-better-errors: 2.3.1
- lines-and-columns: 1.2.4
-
parse-ms@4.0.0: {}
parseurl@1.3.3: {}
@@ -12512,37 +12360,40 @@ snapshots:
optionalDependencies:
'@napi-rs/canvas': 0.1.80
- pdfjs-dist@6.3.289:
+ pdfjs-dist@6.4.299:
optionalDependencies:
- '@napi-rs/canvas': 1.0.9
+ '@napi-rs/canvas': 1.0.10
perfect-debounce@2.1.0: {}
performance-now@2.1.0:
optional: true
- pg-boss@12.34.0:
+ pg-boss@12.37.1(@opentelemetry/api@1.9.1):
dependencies:
+ '@opentelemetry/api': 1.9.1
cron-parser: 5.10.1
- pg: 8.23.0
- rrule-temporal: 2.2.6
- serialize-error: 13.0.1
+ pg: 8.23.1
+ rrule-temporal: 2.2.8
+ serialize-error: 13.0.2
transitivePeerDependencies:
- pg-native
- pg-cloudflare@1.4.0:
+ pg-cloudflare@1.4.1:
optional: true
- pg-connection-string@2.14.0: {}
+ pg-connection-string@2.14.1: {}
pg-int8@1.0.1: {}
- pg-pool@3.14.0(pg@8.23.0):
+ pg-pool@3.14.0(pg@8.23.1):
dependencies:
- pg: 8.23.0
+ pg: 8.23.1
pg-protocol@1.16.0: {}
+ pg-protocol@1.16.1: {}
+
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
@@ -12551,15 +12402,15 @@ snapshots:
postgres-date: 1.0.7
postgres-interval: 1.2.0
- pg@8.23.0:
+ pg@8.23.1:
dependencies:
- pg-connection-string: 2.14.0
- pg-pool: 3.14.0(pg@8.23.0)
- pg-protocol: 1.16.0
+ pg-connection-string: 2.14.1
+ pg-pool: 3.14.0(pg@8.23.1)
+ pg-protocol: 1.16.1
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
- pg-cloudflare: 1.4.0
+ pg-cloudflare: 1.4.1
pgpass@1.0.5:
dependencies:
@@ -12567,8 +12418,6 @@ snapshots:
picocolors@1.1.1: {}
- picomatch@4.0.5: {}
-
picomatch@4.0.7: {}
pkce-challenge@5.0.1: {}
@@ -12583,38 +12432,38 @@ snapshots:
dependencies:
find-up: 3.0.0
- playwright-core@1.63.0: {}
+ playwright-core@1.64.0: {}
- playwright@1.63.0:
+ playwright@1.64.0:
dependencies:
- playwright-core: 1.63.0
+ playwright-core: 1.64.0
pngjs@5.0.0: {}
possible-typed-array-names@1.1.0: {}
- postcss-selector-parser@7.1.4:
+ postcss-selector-parser@7.1.6:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.5.23:
dependencies:
- nanoid: 3.3.18
+ nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
-
- postcss@8.5.25:
- dependencies:
- nanoid: 3.3.18
- picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
postcss@8.5.26:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
+
+ postcss@8.5.29:
+ dependencies:
+ nanoid: 3.3.20
+ picocolors: 1.1.1
+ source-map-js: 1.2.2
postgres-array@2.0.0: {}
@@ -12630,7 +12479,7 @@ snapshots:
postgres@3.4.7: {}
- powershell-utils@0.1.0: {}
+ powershell-utils@0.2.1: {}
prelude-ls@1.2.1: {}
@@ -12640,17 +12489,17 @@ snapshots:
prettier@3.9.9: {}
- pretty-ms@9.3.0:
+ pretty-ms@9.3.1:
dependencies:
parse-ms: 4.0.0
- prisma@7.10.0(@types/node@26.6.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3):
+ prisma@7.10.0(@types/node@26.6.4)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(magicast@0.5.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3):
dependencies:
- '@prisma/config': 7.10.0(magicast@0.5.2)
+ '@prisma/config': 7.10.0(magicast@0.5.5)
'@prisma/dev': 0.24.17(typescript@6.0.3)
'@prisma/engines': 7.10.0
'@prisma/studio-core': 0.33.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- mysql2: 3.24.4(@types/node@26.6.2)
+ mysql2: 3.24.4(@types/node@26.6.4)
postgres: 3.4.7
optionalDependencies:
typescript: 6.0.3
@@ -12690,7 +12539,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- protobufjs@7.6.5:
+ protobufjs@7.6.6:
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2
@@ -12701,10 +12550,10 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.2
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
long: 5.3.2
- proxy-addr@2.0.7:
+ proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -12737,63 +12586,63 @@ snapshots:
queue-microtask@1.2.3: {}
- radix-ui@1.6.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
+ radix-ui@1.7.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
'@radix-ui/primitive': 1.1.7
- '@radix-ui/react-accessible-icon': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-alert-dialog': 1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-aspect-ratio': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-avatar': 1.2.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-checkbox': 1.3.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-accessible-icon': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-accordion': 1.2.21(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-alert-dialog': 1.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-arrow': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-aspect-ratio': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-avatar': 1.2.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-checkbox': 1.3.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collapsible': 1.1.21(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-collection': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-context': 1.2.2(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-context-menu': 2.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-direction': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-dropdown-menu': 2.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-context-menu': 2.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-dialog': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-direction': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-dismissable-layer': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-dropdown-menu': 2.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-focus-guards': 1.1.6(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-form': 0.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-hover-card': 1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-label': 2.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-menu': 2.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-menubar': 1.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-navigation-menu': 1.2.22(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-one-time-password-field': 0.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-password-toggle-field': 0.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-popover': 1.1.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-progress': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-radio-group': 1.4.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-scroll-area': 1.2.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-select': 2.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-separator': 1.1.15(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-slider': 1.4.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-slot': 1.3.3(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-switch': 1.3.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-toast': 1.2.23(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-toggle': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-toolbar': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- '@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-focus-scope': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-form': 0.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-hover-card': 1.1.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-label': 2.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-menu': 2.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-menubar': 1.1.25(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-navigation-menu': 1.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-one-time-password-field': 0.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-password-toggle-field': 0.1.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-popover': 1.2.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-popper': 1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-portal': 1.1.18(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-presence': 1.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-primitive': 2.1.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-progress': 1.1.17(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-radio-group': 1.4.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-roving-focus': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-scroll-area': 1.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-select': 2.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-separator': 1.1.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slider': 1.5.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-slot': 1.4.0(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-switch': 1.3.8(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-tabs': 1.1.22(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-toast': 1.2.24(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-toggle': 1.1.19(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-toggle-group': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-toolbar': 1.1.20(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-tooltip': 1.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-escape-keydown': 1.1.5(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.3.0)(react@19.3.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-use-size': 1.1.4(@types/react@19.3.0)(react@19.3.0)
- '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-use-size': 1.1.5(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-visually-hidden': 1.2.12(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
@@ -12819,7 +12668,7 @@ snapshots:
defu: 6.1.7
destr: 2.0.5
- react-day-picker@10.0.1(@types/react@19.3.0)(react@19.3.0):
+ react-day-picker@10.0.2(@types/react@19.3.0)(react@19.3.0):
dependencies:
'@date-fns/tz': 1.5.0
date-fns: 4.4.0
@@ -12832,9 +12681,11 @@ snapshots:
react: 19.3.0
scheduler: 0.28.0
- react-hook-form@7.83.0(react@19.3.0):
+ react-hook-form@7.89.0(@types/react@19.3.0)(react@19.3.0):
dependencies:
react: 19.3.0
+ optionalDependencies:
+ '@types/react': 19.3.0
react-is@16.13.1: {}
@@ -12906,9 +12757,9 @@ snapshots:
readdirp@5.1.1: {}
- recast@0.23.19:
+ recast@0.23.21:
dependencies:
- ast-types: 0.16.1
+ ast-types: 0.16.3
esprima: 4.0.1
source-map: 0.6.1
tiny-invariant: 1.3.3
@@ -13039,7 +12890,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- rrule-temporal@2.2.6:
+ rrule-temporal@2.2.8:
dependencies:
temporal-spec: 1.0.1
@@ -13108,7 +12959,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- serialize-error@13.0.1:
+ serialize-error@13.0.2:
dependencies:
non-error: 0.1.0
type-fest: 5.10.0
@@ -13151,34 +13002,35 @@ snapshots:
setprototypeof@1.2.0: {}
- shadcn@4.16.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(typescript@6.0.3):
+ shadcn@4.21.4(@cfworker/json-schema@4.1.1)(supports-color@7.2.0):
dependencies:
'@babel/core': 7.29.7(supports-color@7.2.0)
- '@babel/parser': 7.29.8
- '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
+ '@babel/parser': 7.29.9
+ '@babel/plugin-transform-typescript': 7.29.9(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
'@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
'@dotenvx/dotenvx': 1.75.1
- '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@3.25.76)
+ '@modelcontextprotocol/sdk': 1.32.1(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@3.25.76)
+ '@shadcn/registry': 0.1.3(supports-color@7.2.0)
'@types/validate-npm-package-name': 4.0.2
- browserslist: 4.28.7
+ browserslist: 4.29.3
+ cn: 0.2.6
commander: 14.0.3
- cosmiconfig: 9.0.2(typescript@6.0.3)
dedent: 1.7.2
deepmerge: 4.3.1
diff: 8.0.4
execa: 9.6.1
fast-glob: 3.3.3
- fs-extra: 11.4.0
+ fs-extra: 11.4.1
fuzzysort: 3.1.0
kleur: 4.1.5
- open: 11.0.0
+ open: 11.0.4
ora: 8.2.0
- postcss: 8.5.25
- postcss-selector-parser: 7.1.4
+ postcss: 8.5.29
+ postcss-selector-parser: 7.1.6
prompts: 2.4.2
- recast: 0.23.19
+ recast: 0.23.21
+ socks: 2.8.10
stringify-object: 5.0.0
- tailwind-merge: 3.6.0
ts-morph: 26.0.0
tsconfig-paths: 4.2.0
undici: 7.30.0
@@ -13189,40 +13041,39 @@ snapshots:
- '@cfworker/json-schema'
- babel-plugin-macros
- supports-color
- - typescript
- sharp@0.35.4(@types/node@26.6.2):
+ sharp@0.35.5(@types/node@26.6.4):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
- '@img/sharp-darwin-arm64': 0.35.4
- '@img/sharp-darwin-x64': 0.35.4
- '@img/sharp-freebsd-wasm32': 0.35.4
- '@img/sharp-libvips-darwin-arm64': 1.3.3
- '@img/sharp-libvips-darwin-x64': 1.3.3
- '@img/sharp-libvips-linux-arm': 1.3.3
- '@img/sharp-libvips-linux-arm64': 1.3.3
- '@img/sharp-libvips-linux-ppc64': 1.3.3
- '@img/sharp-libvips-linux-riscv64': 1.3.3
- '@img/sharp-libvips-linux-s390x': 1.3.3
- '@img/sharp-libvips-linux-x64': 1.3.3
- '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
- '@img/sharp-libvips-linuxmusl-x64': 1.3.3
- '@img/sharp-linux-arm': 0.35.4
- '@img/sharp-linux-arm64': 0.35.4
- '@img/sharp-linux-ppc64': 0.35.4
- '@img/sharp-linux-riscv64': 0.35.4
- '@img/sharp-linux-s390x': 0.35.4
- '@img/sharp-linux-x64': 0.35.4
- '@img/sharp-linuxmusl-arm64': 0.35.4
- '@img/sharp-linuxmusl-x64': 0.35.4
- '@img/sharp-webcontainers-wasm32': 0.35.4
- '@img/sharp-win32-arm64': 0.35.4
- '@img/sharp-win32-ia32': 0.35.4
- '@img/sharp-win32-x64': 0.35.4
- '@types/node': 26.6.2
+ '@img/sharp-darwin-arm64': 0.35.5
+ '@img/sharp-darwin-x64': 0.35.5
+ '@img/sharp-freebsd-wasm32': 0.35.5
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
+ '@img/sharp-libvips-darwin-x64': 1.3.4
+ '@img/sharp-libvips-linux-arm': 1.3.4
+ '@img/sharp-libvips-linux-arm64': 1.3.4
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
+ '@img/sharp-libvips-linux-s390x': 1.3.4
+ '@img/sharp-libvips-linux-x64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
+ '@img/sharp-linux-arm': 0.35.5
+ '@img/sharp-linux-arm64': 0.35.5
+ '@img/sharp-linux-ppc64': 0.35.5
+ '@img/sharp-linux-riscv64': 0.35.5
+ '@img/sharp-linux-s390x': 0.35.5
+ '@img/sharp-linux-x64': 0.35.5
+ '@img/sharp-linuxmusl-arm64': 0.35.5
+ '@img/sharp-linuxmusl-x64': 0.35.5
+ '@img/sharp-webcontainers-wasm32': 0.35.5
+ '@img/sharp-win32-arm64': 0.35.5
+ '@img/sharp-win32-ia32': 0.35.5
+ '@img/sharp-win32-x64': 0.35.5
+ '@types/node': 26.6.4
optional: true
shebang-command@2.0.0:
@@ -13259,8 +13110,6 @@ snapshots:
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
- siginfo@2.0.0: {}
-
signal-exit@3.0.7: {}
signal-exit@4.1.0: {}
@@ -13273,7 +13122,14 @@ snapshots:
sisteransi@1.0.5: {}
- smol-toml@1.8.0: {}
+ smart-buffer@4.2.0: {}
+
+ smol-toml@1.9.0: {}
+
+ socks@2.8.10:
+ dependencies:
+ ip-address: 10.7.3
+ smart-buffer: 4.2.0
sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
@@ -13282,7 +13138,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.3.0
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
source-map@0.6.1: {}
@@ -13303,12 +13159,10 @@ snapshots:
bcrypt-pbkdf: 1.0.2
optionalDependencies:
cpu-features: 0.0.10
- nan: 2.28.0
+ nan: 2.29.0
stable-hash@0.0.5: {}
- stackback@0.0.2: {}
-
stackblur-canvas@2.7.0:
optional: true
@@ -13316,7 +13170,7 @@ snapshots:
std-env@3.10.0: {}
- std-env@4.1.0: {}
+ std-env@4.3.0: {}
stdin-discarder@0.2.2: {}
@@ -13330,7 +13184,7 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
- streamx@2.28.0:
+ streamx@2.28.1:
dependencies:
events-universal: 1.0.1
fast-fifo: 1.3.2
@@ -13357,7 +13211,7 @@ snapshots:
string-width@7.2.0:
dependencies:
emoji-regex: 10.6.0
- get-east-asian-width: 1.6.0
+ get-east-asian-width: 1.7.0
strip-ansi: 7.2.0
string.prototype.includes@2.0.1:
@@ -13431,7 +13285,7 @@ snapshots:
strip-ansi@7.2.0:
dependencies:
- ansi-regex: 6.2.2
+ ansi-regex: 6.4.0
strip-bom@3.0.0: {}
@@ -13459,11 +13313,11 @@ snapshots:
svg-pathdata@6.0.3:
optional: true
- systeminformation@5.33.1: {}
+ systeminformation@5.33.15: {}
tagged-tag@1.0.0: {}
- tailwind-merge@3.6.0: {}
+ tailwind-merge@3.7.0: {}
tailwindcss@4.3.3: {}
@@ -13479,10 +13333,10 @@ snapshots:
tar-fs@3.1.3:
dependencies:
pump: 3.0.4
- tar-stream: 3.2.0
+ tar-stream: 3.2.1
optionalDependencies:
- bare-fs: 4.8.0
- bare-path: 3.1.1
+ bare-fs: 4.8.2
+ bare-path: 3.1.2
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
@@ -13496,12 +13350,12 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
- tar-stream@3.2.0:
+ tar-stream@3.2.1:
dependencies:
- b4a: 1.8.1
- bare-fs: 4.8.0
+ b4a: 1.9.0
+ bare-fs: 4.8.2
fast-fifo: 1.3.2
- streamx: 2.28.0
+ streamx: 2.28.1
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
@@ -13509,7 +13363,7 @@ snapshots:
teex@1.0.1:
dependencies:
- streamx: 2.28.0
+ streamx: 2.28.1
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
@@ -13532,7 +13386,7 @@ snapshots:
transitivePeerDependencies:
- encoding
- testcontainers@12.1.0(supports-color@7.2.0):
+ testcontainers@12.2.0(supports-color@7.2.0):
dependencies:
'@balena/dockerignore': 1.0.2
'@types/dockerode': 4.0.1
@@ -13540,7 +13394,7 @@ snapshots:
async-lock: 1.4.1
byline: 5.0.0
debug: 4.4.3(supports-color@7.2.0)
- docker-compose: 1.4.2
+ docker-compose: 1.5.0
dockerode: 5.0.1(supports-color@7.2.0)
get-port: 5.1.1
proper-lockfile: 4.1.2
@@ -13557,7 +13411,7 @@ snapshots:
text-decoder@1.2.7:
dependencies:
- b4a: 1.8.1
+ b4a: 1.9.0
transitivePeerDependencies:
- react-native-b4a
@@ -13568,16 +13422,16 @@ snapshots:
tiny-invariant@1.3.3: {}
- tinybench@2.9.0: {}
+ tinybench@6.2.1: {}
- tinyexec@1.1.1: {}
+ tinyexec@1.3.1: {}
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.7)
picomatch: 4.0.7
- tinyrainbow@3.1.0: {}
+ tinyrainbow@3.2.0: {}
tldts-core@7.4.9:
optional: true
@@ -13642,6 +13496,8 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
+ type-fest@4.41.0: {}
+
type-fest@5.10.0:
dependencies:
tagged-tag: 1.0.0
@@ -13684,12 +13540,12 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
- typescript-eslint@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
+ typescript-eslint@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/parser': 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3)
- '@typescript-eslint/utils': 8.70.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.71.0(eslint@9.39.3(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
eslint: 9.39.3(jiti@2.7.0)(supports-color@7.2.0)
typescript: 6.0.3
transitivePeerDependencies:
@@ -13697,7 +13553,7 @@ snapshots:
typescript@6.0.3: {}
- unbash@4.0.10: {}
+ unbash@4.0.12: {}
unbox-primitive@1.1.0:
dependencies:
@@ -13750,9 +13606,9 @@ snapshots:
until-async@3.0.2:
optional: true
- update-browserslist-db@1.2.3(browserslist@4.28.7):
+ update-browserslist-db@1.3.4(browserslist@4.29.3):
dependencies:
- browserslist: 4.28.7
+ browserslist: 4.29.3
escalade: 3.2.0
picocolors: 1.1.1
@@ -13817,44 +13673,39 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
- vite@8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4):
+ vite@8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.7
- postcss: 8.5.26
+ postcss: 8.5.29
rolldown: 1.1.5
tinyglobby: 0.2.17
optionalDependencies:
- '@types/node': 26.6.2
+ '@types/node': 26.6.4
fsevents: 2.3.3
jiti: 2.7.0
- yaml: 2.8.4
+ yaml: 2.9.1
- vitest@4.1.11(@types/node@26.6.2)(@vitest/coverage-v8@4.1.5)(msw@2.14.6(@types/node@26.6.2)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4)):
+ vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(msw@2.14.6(@types/node@26.6.4)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)):
dependencies:
- '@vitest/expect': 4.1.11
- '@vitest/mocker': 4.1.11(msw@2.14.6(@types/node@26.6.2)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4))
- '@vitest/pretty-format': 4.1.11
- '@vitest/runner': 4.1.11
- '@vitest/snapshot': 4.1.11
- '@vitest/spy': 4.1.11
- '@vitest/utils': 4.1.11
- es-module-lexer: 2.1.0
- expect-type: 1.3.0
- magic-string: 0.30.21
- obug: 2.1.1
- pathe: 2.0.3
+ '@types/chai': 5.2.3
+ '@vitest/mocker': 5.0.3(msw@2.14.6(@types/node@26.6.4)(typescript@6.0.3))(vite@8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1))
+ chai: 6.2.2
+ es-module-lexer: 2.3.2
+ expect-type: 1.4.0
+ magic-string: 1.4.3
+ obug: 2.2.1
picomatch: 4.0.7
- std-env: 4.1.0
- tinybench: 2.9.0
- tinyexec: 1.1.1
+ std-env: 4.3.0
+ tinybench: 6.2.1
+ tinyexec: 1.3.1
tinyglobby: 0.2.17
- tinyrainbow: 3.1.0
- vite: 8.1.5(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.8.4)
- why-is-node-running: 2.3.0
+ vite: 8.1.5(@types/node@26.6.4)(jiti@2.7.0)(yaml@2.9.1)
+ why-is-node-running: 3.2.1
optionalDependencies:
- '@types/node': 26.6.2
- '@vitest/coverage-v8': 4.1.5(vitest@4.1.11)
+ '@opentelemetry/api': 1.9.1
+ '@types/node': 26.6.4
+ '@vitest/coverage-v8': 5.0.3(vitest@5.0.3)
transitivePeerDependencies:
- msw
@@ -13877,7 +13728,7 @@ snapshots:
webpack-bundle-analyzer@4.10.1:
dependencies:
'@discoveryjs/json-ext': 0.5.7
- acorn: 8.18.0
+ acorn: 8.19.0
acorn-walk: 8.3.5
commander: 7.2.0
debounce: 1.2.1
@@ -13920,7 +13771,7 @@ snapshots:
isarray: 2.0.5
which-boxed-primitive: 1.1.1
which-collection: 1.0.2
- which-typed-array: 1.1.23
+ which-typed-array: 1.1.24
which-collection@1.0.2:
dependencies:
@@ -13931,7 +13782,7 @@ snapshots:
which-module@2.0.1: {}
- which-typed-array@1.1.23:
+ which-typed-array@1.1.24:
dependencies:
available-typed-arrays: 1.0.7
call-bind: 1.0.9
@@ -13949,10 +13800,7 @@ snapshots:
dependencies:
isexe: 3.1.5
- why-is-node-running@2.3.0:
- dependencies:
- siginfo: 2.0.0
- stackback: 0.0.2
+ why-is-node-running@3.2.1: {}
word-wrap@1.2.5: {}
@@ -13978,10 +13826,10 @@ snapshots:
ws@7.5.13: {}
- wsl-utils@0.3.1:
+ wsl-utils@1.0.1:
dependencies:
is-wsl: 3.1.1
- powershell-utils: 0.1.0
+ powershell-utils: 0.2.1
xtend@4.0.2: {}
@@ -13991,9 +13839,7 @@ snapshots:
yallist@3.1.1: {}
- yaml@2.8.4: {}
-
- yaml@2.9.0: {}
+ yaml@2.9.1: {}
yargs-parser@18.1.3:
dependencies:
@@ -14049,7 +13895,7 @@ snapshots:
zlibjs@0.3.1: {}
- zod-openapi@6.0.1(zod@4.4.3):
+ zod-openapi@6.0.2(zod@4.4.3):
dependencies:
zod: 4.4.3
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index a1304d0c1..5e1ffece7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -27,6 +27,13 @@ overrides:
"@hono/node-server@<1.19.15": "^1.19.15"
"picomatch@<4.0.4": "^4.0.4"
"body-parser@<2.3.0": "^2.3.0"
+ # CVE-2026-93749 (source-map-js < 1.2.2, HIGH, under postcss and the
+ # Tailwind build), CVE-2026-104844 (postcss-selector-parser < 7.1.6, under
+ # shadcn) and GHSA-r4xh-jqrq-34v2 (smol-toml < 1.9.0, under knip). All three
+ # are quadratic-time parsers reached at build or dev time only.
+ "source-map-js@<1.2.2": "^1.2.2"
+ "postcss-selector-parser@<7.1.6": "^7.1.6"
+ "smol-toml@<1.9.0": "^1.9.0"
# Array-limit bypass through bracket-key comma parsing, and a denial of
# service through an attacker-controlled isBuffer. 6.16.0 covers both.
"qs@<6.16.0": "^6.16.0"
@@ -38,6 +45,10 @@ overrides:
# follow-up fixed in 3.1.8, raised the floor from 3.1.6.
"fast-uri@<3.1.8": "^3.1.8"
"flatted@<3.4.2": "^3.4.2"
+ # GHSA-jqcg-44mw-7w3h: IP spoofing through IPv4-mapped addresses. It
+ # arrives under express in the MCP SDK; the app resolves client IPs itself,
+ # but the copy sits on a production path.
+ "proxy-addr@<2.0.8": "^2.0.8"
# GHSA-3f6p-5ww8-9rcr: auth-plugin downgrade leaking plaintext credentials.
# Reached only through prisma's MySQL driver, which this app never opens;
# pinned so the audit reads clean, and mirrored for /opt/prisma-cli in the
diff --git a/prisma.config.ts b/prisma.config.ts
index c1e7e5613..6100aac3c 100644
--- a/prisma.config.ts
+++ b/prisma.config.ts
@@ -3,8 +3,8 @@ import path from "node:path";
import dotenv from "dotenv";
import { defineConfig } from "prisma/config";
-dotenv.config({ path: path.join(__dirname, ".env") });
-dotenv.config({ path: path.join(__dirname, ".env.local") });
+dotenv.config({ path: path.join(__dirname, ".env"), quiet: true });
+dotenv.config({ path: path.join(__dirname, ".env.local"), quiet: true });
const inContainer = fs.existsSync("/.dockerenv");
const defaultDatabaseUrl = inContainer
diff --git a/prisma/migrations/0371_idempotency_request_fingerprint/migration.sql b/prisma/migrations/0371_idempotency_request_fingerprint/migration.sql
new file mode 100644
index 000000000..dcb360852
--- /dev/null
+++ b/prisma/migrations/0371_idempotency_request_fingerprint/migration.sql
@@ -0,0 +1,8 @@
+-- What request a replay-cache cell answered, as a SHA-256 of the canonical
+-- request body scoped by method and path. A request that reuses a key with a
+-- different body is executed instead of being handed the first body's cached
+-- response. The body itself is never stored. Nullable: rows written before
+-- this release carry none, and a null never matches, so they stop replaying
+-- for anything but their own retries already in flight (they expire within
+-- 24 hours either way).
+ALTER TABLE "idempotency_keys" ADD COLUMN IF NOT EXISTS "request_fingerprint" TEXT;
diff --git a/prisma/migrations/0372_pulse_hourly_mean_day/migration.sql b/prisma/migrations/0372_pulse_hourly_mean_day/migration.sql
new file mode 100644
index 000000000..4b7110762
--- /dev/null
+++ b/prisma/migrations/0372_pulse_hourly_mean_day/migration.sql
@@ -0,0 +1,113 @@
+-- Pulse: a day is the mean of its hours' means, a coarser bucket the mean of
+-- its days.
+--
+-- A watch samples pulse about every 5 to 10 minutes at rest and about every
+-- 5 seconds in a workout, so the plain mean over a day's readings let one
+-- workout hour outweigh the other twenty-three. The rollup writer now stores a
+-- PULSE DAY bucket's `mean` as the mean of its per-hour means, and a WEEK /
+-- MONTH / YEAR bucket's as the mean of its days' values (see
+-- `src/lib/measurements/day-statistic.ts`). Nothing rewrites a stored bucket
+-- on its own: the boot backfill and the freshness check fill only missing
+-- days, so rows written before this release would keep the reading mean
+-- until a write happened to touch their day. This migration recomputes them.
+--
+-- Scope, exactly:
+-- - only rows of type PULSE, in every granularity;
+-- - only the `mean` column. `count`, `min_value`, `max_value`, `sum_value`,
+-- `sd`, `slope`, `r2` and the regression sums stay over every reading, as
+-- the writer keeps them;
+-- - hours, days, weeks, months and years are `date_trunc` on `measured_at`
+-- in the session zone, exactly as the writer groups them, per user / source
+-- over non-deleted readings.
+--
+-- Recomputing from `measurements` makes the migration idempotent: a rerun
+-- derives the same means whatever the column held before. A stored row whose
+-- readings are all gone is left as it is; the writer prunes such rows on the
+-- next fold of their range.
+--
+-- The hour means are built once into a temporary table, so the readings are
+-- scanned once for all four granularities.
+--
+-- Reversibility (down): recompute `mean` as `AVG(value)` over the same groups,
+-- or refold the PULSE rollups on a release before this one. No column changes
+-- shape.
+
+CREATE TEMPORARY TABLE pulse_hour_means AS
+SELECT
+ m."user_id" AS user_id,
+ m."source" AS source,
+ date_trunc('hour', m."measured_at") AS hr,
+ AVG(m."value") AS hour_mean
+FROM measurements m
+WHERE m."type" = 'PULSE'
+ AND m."deleted_at" IS NULL
+GROUP BY m."user_id", m."source", date_trunc('hour', m."measured_at");
+
+CREATE TEMPORARY TABLE pulse_day_means AS
+SELECT
+ h.user_id,
+ h.source,
+ date_trunc('day', h.hr) AS day,
+ AVG(h.hour_mean) AS day_mean
+FROM pulse_hour_means h
+GROUP BY h.user_id, h.source, date_trunc('day', h.hr);
+
+CREATE INDEX pulse_day_means_key ON pulse_day_means (user_id, source, day);
+
+-- DAY buckets: the mean of the day's hour means.
+UPDATE "measurement_rollups" r
+SET "mean" = d.day_mean
+FROM pulse_day_means d
+WHERE r."granularity" = 'DAY'
+ AND r."type" = 'PULSE'
+ AND r."user_id" = d.user_id
+ AND r."source" = d.source
+ AND r."bucket_start" = d.day;
+
+-- WEEK buckets: the mean of the week's day values.
+UPDATE "measurement_rollups" r
+SET "mean" = s.mean
+FROM (
+ SELECT d.user_id, d.source, date_trunc('week', d.day) AS bucket_start,
+ AVG(d.day_mean) AS mean
+ FROM pulse_day_means d
+ GROUP BY d.user_id, d.source, date_trunc('week', d.day)
+) s
+WHERE r."granularity" = 'WEEK'
+ AND r."type" = 'PULSE'
+ AND r."user_id" = s.user_id
+ AND r."source" = s.source
+ AND r."bucket_start" = s.bucket_start;
+
+-- MONTH buckets.
+UPDATE "measurement_rollups" r
+SET "mean" = s.mean
+FROM (
+ SELECT d.user_id, d.source, date_trunc('month', d.day) AS bucket_start,
+ AVG(d.day_mean) AS mean
+ FROM pulse_day_means d
+ GROUP BY d.user_id, d.source, date_trunc('month', d.day)
+) s
+WHERE r."granularity" = 'MONTH'
+ AND r."type" = 'PULSE'
+ AND r."user_id" = s.user_id
+ AND r."source" = s.source
+ AND r."bucket_start" = s.bucket_start;
+
+-- YEAR buckets.
+UPDATE "measurement_rollups" r
+SET "mean" = s.mean
+FROM (
+ SELECT d.user_id, d.source, date_trunc('year', d.day) AS bucket_start,
+ AVG(d.day_mean) AS mean
+ FROM pulse_day_means d
+ GROUP BY d.user_id, d.source, date_trunc('year', d.day)
+) s
+WHERE r."granularity" = 'YEAR'
+ AND r."type" = 'PULSE'
+ AND r."user_id" = s.user_id
+ AND r."source" = s.source
+ AND r."bucket_start" = s.bucket_start;
+
+DROP TABLE pulse_day_means;
+DROP TABLE pulse_hour_means;
diff --git a/prisma/migrations/0373_backup_pass_attempts/migration.sql b/prisma/migrations/0373_backup_pass_attempts/migration.sql
new file mode 100644
index 000000000..f40e26b3f
--- /dev/null
+++ b/prisma/migrations/0373_backup_pass_attempts/migration.sql
@@ -0,0 +1,19 @@
+-- One row per account and backup pass, written when the pass starts the
+-- account and again when it gets past it. A start newer than the finish with
+-- no run active is an attempt the process died under; the pass takes that
+-- account last, and the admin backups page names it.
+CREATE TABLE "backup_pass_attempts" (
+ "user_id" TEXT NOT NULL,
+ -- The pass's queue name: data-backup or data-backup-offhost.
+ "pass" TEXT NOT NULL,
+ "started_at" TIMESTAMP(3) NOT NULL,
+ -- NULL until the pass has once got past this account.
+ "finished_at" TIMESTAMP(3),
+
+ CONSTRAINT "backup_pass_attempts_pkey" PRIMARY KEY ("user_id", "pass")
+);
+
+ALTER TABLE "backup_pass_attempts"
+ ADD CONSTRAINT "backup_pass_attempts_user_id_fkey"
+ FOREIGN KEY ("user_id") REFERENCES "users"("id")
+ ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/0374_document_thumbnail_failures/migration.sql b/prisma/migrations/0374_document_thumbnail_failures/migration.sql
new file mode 100644
index 000000000..d43029273
--- /dev/null
+++ b/prisma/migrations/0374_document_thumbnail_failures/migration.sql
@@ -0,0 +1,24 @@
+-- Why a document has no preview thumbnail. The boot-time backfill skips a
+-- document whose render failed for good, until the document changes.
+CREATE TABLE "document_thumbnail_failures" (
+ "document_id" TEXT NOT NULL,
+ "user_id" TEXT NOT NULL,
+ "reason" TEXT NOT NULL,
+ "attempts" INTEGER NOT NULL DEFAULT 1,
+ "failed_at" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "document_thumbnail_failures_pkey" PRIMARY KEY ("document_id")
+);
+
+CREATE INDEX "document_thumbnail_failures_user_id_idx"
+ ON "document_thumbnail_failures"("user_id");
+
+ALTER TABLE "document_thumbnail_failures"
+ ADD CONSTRAINT "document_thumbnail_failures_document_id_fkey"
+ FOREIGN KEY ("document_id") REFERENCES "inbound_documents"("id")
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE "document_thumbnail_failures"
+ ADD CONSTRAINT "document_thumbnail_failures_user_id_fkey"
+ FOREIGN KEY ("user_id") REFERENCES "users"("id")
+ ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/0375_coach_conversation_updated_at_repair/migration.sql b/prisma/migrations/0375_coach_conversation_updated_at_repair/migration.sql
new file mode 100644
index 000000000..f30a437e5
--- /dev/null
+++ b/prisma/migrations/0375_coach_conversation_updated_at_repair/migration.sql
@@ -0,0 +1,26 @@
+-- Move a Coach conversation's updated_at back to its newest message.
+--
+-- The free-text encryption backfill (v1.39.3) and the memory-summary refresh
+-- rewrote conversation rows through a Prisma update, which stamped updated_at
+-- with the run time. The Coach panel orders and groups by that column, so
+-- conversations nobody had touched in months showed up under "Today".
+--
+-- A turn stamps updated_at in the same transaction that creates its message,
+-- so the newest message's created_at is the conversation's real last
+-- activity. Only rows more than a minute later than that move back; a rename
+-- or a document attached shortly after the last turn keeps its stamp, and a
+-- conversation without messages is left alone. Running it again changes
+-- nothing: a repaired row is no longer later than its newest message.
+--
+-- One set-based statement. The per-conversation maximum is read from
+-- "coach_messages_conversation_id_created_at_idx" (conversation_id,
+-- created_at), so it does not sort the message table.
+UPDATE "coach_conversations" AS c
+SET "updated_at" = m."last_at"
+FROM (
+ SELECT "conversation_id", MAX("created_at") AS "last_at"
+ FROM "coach_messages"
+ GROUP BY "conversation_id"
+) AS m
+WHERE m."conversation_id" = c."id"
+ AND c."updated_at" > m."last_at" + INTERVAL '1 minute';
diff --git a/prisma/migrations/0376_ai_reasoning_controls/migration.sql b/prisma/migrations/0376_ai_reasoning_controls/migration.sql
new file mode 100644
index 000000000..54c6f36cc
--- /dev/null
+++ b/prisma/migrations/0376_ai_reasoning_controls/migration.sql
@@ -0,0 +1,6 @@
+-- The operator's reasoning controls on the instance row. Both columns carry a
+-- default, so the singleton row reads "reasoning allowed, no cap" the moment
+-- the migration lands and an upgrade needs no admin action.
+ALTER TABLE "app_settings"
+ ADD COLUMN "ai_reasoning_enabled" BOOLEAN NOT NULL DEFAULT true,
+ ADD COLUMN "ai_reasoning_max_effort" TEXT NOT NULL DEFAULT 'high';
diff --git a/prisma/migrations/0377_coach_message_trail/migration.sql b/prisma/migrations/0377_coach_message_trail/migration.sql
new file mode 100644
index 000000000..a3fe696d9
--- /dev/null
+++ b/prisma/migrations/0377_coach_message_trail/migration.sql
@@ -0,0 +1,4 @@
+-- The model-written text of a Coach turn's live trail, encrypted like the
+-- message itself. Nullable and without a default: every existing turn has no
+-- trail, and adding a nullable column rewrites no rows.
+ALTER TABLE "coach_messages" ADD COLUMN "trail_encrypted" BYTEA;
diff --git a/prisma/migrations/0378_coach_fact_source/migration.sql b/prisma/migrations/0378_coach_fact_source/migration.sql
new file mode 100644
index 000000000..f79dab9b8
--- /dev/null
+++ b/prisma/migrations/0378_coach_fact_source/migration.sql
@@ -0,0 +1,8 @@
+-- Where a Coach fact came from, when it was last used in a turn, and the
+-- message it came out of. Every row written before this migration came from
+-- the background extraction or the deterministic matcher, which could not be
+-- told apart, so they all read as `extracted`.
+ALTER TABLE "coach_facts"
+ ADD COLUMN "source" TEXT NOT NULL DEFAULT 'extracted',
+ ADD COLUMN "last_used_at" TIMESTAMP(3),
+ ADD COLUMN "source_message_id" TEXT;
diff --git a/prisma/migrations/0379_measurement_source_health_connect/migration.sql b/prisma/migrations/0379_measurement_source_health_connect/migration.sql
new file mode 100644
index 000000000..e427a3f98
--- /dev/null
+++ b/prisma/migrations/0379_measurement_source_health_connect/migration.sql
@@ -0,0 +1,14 @@
+-- Android Health Connect import provenance (#972).
+--
+-- Rows written by the Health Connect import worker carry their own
+-- `MeasurementSource` value, so they stay distinguishable from the Apple
+-- Health and generic IMPORT rows and get an isolated idempotency namespace
+-- `(userId, type, HEALTH_CONNECT, externalId)`.
+--
+-- Purely additive enum extension, alone in its migration: Postgres forbids
+-- USING a freshly added value in the same transaction, and nothing here uses
+-- it. `ADD VALUE IF NOT EXISTS` makes a rerun safe. Mirrors 0346 (EXTERNAL).
+--
+-- Reversibility: Postgres cannot remove an enum value, so `HEALTH_CONNECT`
+-- stays (inert with no rows).
+ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'HEALTH_CONNECT';
diff --git a/prisma/migrations/0380_import_job_kind/migration.sql b/prisma/migrations/0380_import_job_kind/migration.sql
new file mode 100644
index 000000000..ba7d31c2f
--- /dev/null
+++ b/prisma/migrations/0380_import_job_kind/migration.sql
@@ -0,0 +1,9 @@
+-- Which importer owns an `import_jobs` row (#972).
+--
+-- Until now every row was an Apple Health export; the Health Connect importer
+-- shares the table. The reconcile cron and the staged-file sweep scope by
+-- `kind` so one importer never fails or deletes the other's running job.
+-- Every existing row is an Apple Health job, which the default records.
+ALTER TABLE "import_jobs" ADD COLUMN "kind" TEXT NOT NULL DEFAULT 'apple_health';
+
+CREATE INDEX "import_jobs_user_id_kind_started_at_idx" ON "import_jobs"("user_id", "kind", "started_at");
diff --git a/prisma/migrations/0381_environment_air_quality/migration.sql b/prisma/migrations/0381_environment_air_quality/migration.sql
new file mode 100644
index 000000000..6c6ae407f
--- /dev/null
+++ b/prisma/migrations/0381_environment_air_quality/migration.sql
@@ -0,0 +1,40 @@
+-- Air quality, pollen and UV per environment day (#615).
+--
+-- Twenty-two nullable columns filled from the Open-Meteo air-quality feed
+-- (CAMS Europe / global). Nullable throughout: a partial feed stores what it
+-- has, and a day the feed never covered stays NULL rather than zero.
+-- `aq_fetched_at` NULL marks a day whose air-quality part has not been
+-- fetched yet; the partial gap index serves the nightly fill's lookup.
+--
+-- `users.environment_air_quality_enabled` is the per-user switch, on by
+-- default; the operator switch `ENVIRONMENT_AIR_QUALITY_DISABLED` wins.
+ALTER TABLE "environment_contexts"
+ ADD COLUMN "apparent_max" DOUBLE PRECISION,
+ ADD COLUMN "pm25_mean" DOUBLE PRECISION,
+ ADD COLUMN "pm25_max" DOUBLE PRECISION,
+ ADD COLUMN "pm10_mean" DOUBLE PRECISION,
+ ADD COLUMN "no2_mean" DOUBLE PRECISION,
+ ADD COLUMN "so2_mean" DOUBLE PRECISION,
+ ADD COLUMN "co_mean" DOUBLE PRECISION,
+ ADD COLUMN "o3_max_8h" DOUBLE PRECISION,
+ ADD COLUMN "eaqi_max" DOUBLE PRECISION,
+ ADD COLUMN "usaqi_max" DOUBLE PRECISION,
+ ADD COLUMN "uv_index_max" DOUBLE PRECISION,
+ ADD COLUMN "dust_max" DOUBLE PRECISION,
+ ADD COLUMN "aod_max" DOUBLE PRECISION,
+ ADD COLUMN "pollen_alder_max" DOUBLE PRECISION,
+ ADD COLUMN "pollen_birch_max" DOUBLE PRECISION,
+ ADD COLUMN "pollen_grass_max" DOUBLE PRECISION,
+ ADD COLUMN "pollen_mugwort_max" DOUBLE PRECISION,
+ ADD COLUMN "pollen_olive_max" DOUBLE PRECISION,
+ ADD COLUMN "pollen_ragweed_max" DOUBLE PRECISION,
+ ADD COLUMN "aq_domain" TEXT,
+ ADD COLUMN "aq_hours" INTEGER,
+ ADD COLUMN "aq_fetched_at" TIMESTAMP(3);
+
+CREATE INDEX "environment_contexts_aq_gap_idx"
+ ON "environment_contexts"("user_id", "date")
+ WHERE "aq_fetched_at" IS NULL;
+
+ALTER TABLE "users"
+ ADD COLUMN "environment_air_quality_enabled" BOOLEAN NOT NULL DEFAULT true;
diff --git a/prisma/migrations/0382_environment_location_encrypted/migration.sql b/prisma/migrations/0382_environment_location_encrypted/migration.sql
new file mode 100644
index 000000000..9be9f545f
--- /dev/null
+++ b/prisma/migrations/0382_environment_location_encrypted/migration.sql
@@ -0,0 +1,36 @@
+-- Environment locations sealed at rest (#615).
+--
+-- The home location, each dated location period and the per-day resolved
+-- location gain a sealed copy (lat, lon, label under AAD
+-- `healthlog/environment-location/v1`). The free-text encryption backfill
+-- fills it from the plaintext and then clears the plaintext; the plaintext
+-- columns therefore become nullable here and drop in a later release, once
+-- the backfill reports zero remaining rows everywhere.
+--
+-- The partial indexes serve the backfill's discovery ("rows still holding
+-- plaintext") and empty themselves as it runs.
+ALTER TABLE "users" ADD COLUMN "home_location_encrypted" BYTEA;
+
+ALTER TABLE "environment_travel_locations"
+ ADD COLUMN "location_encrypted" BYTEA,
+ ALTER COLUMN "lat" DROP NOT NULL,
+ ALTER COLUMN "lon" DROP NOT NULL,
+ ALTER COLUMN "label" DROP NOT NULL;
+
+ALTER TABLE "environment_contexts"
+ ADD COLUMN "location_encrypted" BYTEA,
+ ALTER COLUMN "lat" DROP NOT NULL,
+ ALTER COLUMN "lon" DROP NOT NULL,
+ ALTER COLUMN "location_label" DROP NOT NULL;
+
+CREATE INDEX "users_home_location_plain_idx"
+ ON "users"("id")
+ WHERE "home_lat" IS NOT NULL;
+
+CREATE INDEX "environment_travel_locations_plain_location_idx"
+ ON "environment_travel_locations"("id")
+ WHERE "lat" IS NOT NULL;
+
+CREATE INDEX "environment_contexts_plain_location_idx"
+ ON "environment_contexts"("id")
+ WHERE "lat" IS NOT NULL;
diff --git a/prisma/migrations/0383_managed_profile_handover/migration.sql b/prisma/migrations/0383_managed_profile_handover/migration.sql
new file mode 100644
index 000000000..6bdc836ce
--- /dev/null
+++ b/prisma/migrations/0383_managed_profile_handover/migration.sql
@@ -0,0 +1,32 @@
+-- Managed profile handover links (#959).
+--
+-- A guardian mints a one-time link that hands a managed profile over to the
+-- person it describes. Only the HMAC-SHA256 hash of the token is stored.
+-- `proposals_json` carries the guardian-side proposal per guardian (end,
+-- read, manage) that the new owner confirms when claiming. The partial
+-- unique index allows at most one open (unused, unrevoked) link per profile.
+CREATE TABLE "managed_profile_handovers" (
+ "id" TEXT NOT NULL,
+ "profile_id" TEXT NOT NULL,
+ "created_by_id" TEXT NOT NULL,
+ "token_hash" TEXT NOT NULL,
+ "proposals_json" JSONB NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "expires_at" TIMESTAMP(3) NOT NULL,
+ "used_at" TIMESTAMP(3),
+ "revoked_at" TIMESTAMP(3),
+
+ CONSTRAINT "managed_profile_handovers_pkey" PRIMARY KEY ("id")
+);
+
+CREATE UNIQUE INDEX "managed_profile_handovers_token_hash_key" ON "managed_profile_handovers"("token_hash");
+
+CREATE INDEX "managed_profile_handovers_profile_id_idx" ON "managed_profile_handovers"("profile_id");
+
+CREATE UNIQUE INDEX "managed_profile_handovers_open_idx"
+ ON "managed_profile_handovers"("profile_id")
+ WHERE "used_at" IS NULL AND "revoked_at" IS NULL;
+
+ALTER TABLE "managed_profile_handovers" ADD CONSTRAINT "managed_profile_handovers_profile_id_fkey" FOREIGN KEY ("profile_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE "managed_profile_handovers" ADD CONSTRAINT "managed_profile_handovers_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/0384_healthkit_type_sync/migration.sql b/prisma/migrations/0384_healthkit_type_sync/migration.sql
new file mode 100644
index 000000000..32ee27029
--- /dev/null
+++ b/prisma/migrations/0384_healthkit_type_sync/migration.sql
@@ -0,0 +1,16 @@
+-- Per-HealthKit-type arrival ledger (#1173).
+--
+-- One row per (user, type), upserted by the measurement batch route: when the
+-- type last arrived, under which `syncTrigger`, and when it last brought a new
+-- or updated sample. Diagnostic state the next sync rebuilds; not backed up.
+CREATE TABLE "healthkit_type_syncs" (
+ "user_id" TEXT NOT NULL,
+ "type" "measurement_type" NOT NULL,
+ "last_received_at" TIMESTAMP(3) NOT NULL,
+ "last_trigger" TEXT,
+ "last_new_sample_at" TIMESTAMP(3),
+
+ CONSTRAINT "healthkit_type_syncs_pkey" PRIMARY KEY ("user_id","type")
+);
+
+ALTER TABLE "healthkit_type_syncs" ADD CONSTRAINT "healthkit_type_syncs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/0385_lab_unit_conversion_source/migration.sql b/prisma/migrations/0385_lab_unit_conversion_source/migration.sql
new file mode 100644
index 000000000..b732f99c2
--- /dev/null
+++ b/prisma/migrations/0385_lab_unit_conversion_source/migration.sql
@@ -0,0 +1,12 @@
+-- Lab unit conversion provenance (#1095).
+--
+-- `biomarkers.analyte_key` names the catalogue analyte a marker resolves to,
+-- which selects its unit-conversion table. `lab_results.source_value` /
+-- `source_unit` keep the value and unit as printed on the report when a write
+-- converted the reading into the marker's canonical unit. All nullable; no
+-- existing row changes.
+ALTER TABLE "biomarkers" ADD COLUMN "analyte_key" TEXT;
+
+ALTER TABLE "lab_results"
+ ADD COLUMN "source_value" DOUBLE PRECISION,
+ ADD COLUMN "source_unit" TEXT;
diff --git a/prisma/migrations/0386_custom_vaccine/migration.sql b/prisma/migrations/0386_custom_vaccine/migration.sql
new file mode 100644
index 000000000..4b7d7c322
--- /dev/null
+++ b/prisma/migrations/0386_custom_vaccine/migration.sql
@@ -0,0 +1,29 @@
+-- User-defined vaccines (#1005).
+--
+-- A vaccine the user defines for a product the shipped catalogue does not
+-- list, in the same shape a catalogue entry resolves to. A dose links to it
+-- through `vaccination_records.custom_vaccine_id`; deleting the definition
+-- sets the link to NULL and the dose stays on its `vaccine_name`.
+CREATE TABLE "custom_vaccines" (
+ "id" TEXT NOT NULL,
+ "user_id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "components" TEXT[],
+ "typical_series_doses" INTEGER,
+ "booster_interval_months" INTEGER,
+ "deleted_at" TIMESTAMP(3),
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "custom_vaccines_pkey" PRIMARY KEY ("id")
+);
+
+CREATE UNIQUE INDEX "custom_vaccines_user_id_name_key" ON "custom_vaccines"("user_id", "name");
+
+ALTER TABLE "custom_vaccines" ADD CONSTRAINT "custom_vaccines_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE "vaccination_records" ADD COLUMN "custom_vaccine_id" TEXT;
+
+CREATE INDEX "vaccination_records_custom_vaccine_id_idx" ON "vaccination_records"("custom_vaccine_id");
+
+ALTER TABLE "vaccination_records" ADD CONSTRAINT "vaccination_records_custom_vaccine_id_fkey" FOREIGN KEY ("custom_vaccine_id") REFERENCES "custom_vaccines"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/0387_life_event/migration.sql b/prisma/migrations/0387_life_event/migration.sql
new file mode 100644
index 000000000..785afe466
--- /dev/null
+++ b/prisma/migrations/0387_life_event/migration.sql
@@ -0,0 +1,31 @@
+-- Life events for the timeline (#613).
+--
+-- A dated anchor in the person's life: a move, a birth, a new job, a loss.
+-- Dates are calendar strings (`YYYY-MM-DD`) without a time zone, and
+-- `precision` says how much of the date is known; at MONTH or YEAR the stored
+-- date is the first day of that month or year. Title and note are encrypted
+-- at rest. Soft-deleted like the other record tables; the index serves the
+-- per-user live list in date order.
+CREATE TYPE "life_event_category" AS ENUM ('FAMILY', 'HOME', 'WORK', 'LOSS', 'OTHER');
+
+CREATE TYPE "life_event_precision" AS ENUM ('DAY', 'MONTH', 'YEAR');
+
+CREATE TABLE "life_events" (
+ "id" TEXT NOT NULL,
+ "user_id" TEXT NOT NULL,
+ "category" "life_event_category" NOT NULL,
+ "start_date" TEXT NOT NULL,
+ "end_date" TEXT,
+ "precision" "life_event_precision" NOT NULL,
+ "title_encrypted" BYTEA NOT NULL,
+ "note_encrypted" BYTEA,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "deleted_at" TIMESTAMP(3),
+
+ CONSTRAINT "life_events_pkey" PRIMARY KEY ("id")
+);
+
+CREATE INDEX "life_events_user_id_deleted_at_start_date_idx" ON "life_events"("user_id", "deleted_at", "start_date");
+
+ALTER TABLE "life_events" ADD CONSTRAINT "life_events_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/0388_documents_layout/migration.sql b/prisma/migrations/0388_documents_layout/migration.sql
new file mode 100644
index 000000000..b7b5122c7
--- /dev/null
+++ b/prisma/migrations/0388_documents_layout/migration.sql
@@ -0,0 +1,3 @@
+-- Per-user document vault presentation (view + month arrangement).
+-- Nullable, display-only; null reads as the defaults.
+ALTER TABLE "users" ADD COLUMN "documents_layout_json" JSONB;
diff --git a/prisma/migrations/0389_measurement_fold_repair/migration.sql b/prisma/migrations/0389_measurement_fold_repair/migration.sql
new file mode 100644
index 000000000..c53691304
--- /dev/null
+++ b/prisma/migrations/0389_measurement_fold_repair/migration.sql
@@ -0,0 +1,12 @@
+-- One row per account once the fold repair has recomputed the daily and
+-- hourly means an older release folded in two runs. The compaction-tombstone
+-- purge waits for it, because the tombstones it deletes are what the repair
+-- reads. Holds no health data; the account's deletion takes it along.
+CREATE TABLE "measurement_fold_repairs" (
+ "user_id" TEXT NOT NULL,
+ "completed_at" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "measurement_fold_repairs_pkey" PRIMARY KEY ("user_id")
+);
+
+ALTER TABLE "measurement_fold_repairs" ADD CONSTRAINT "measurement_fold_repairs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/0390_handover_creator_set_null/migration.sql b/prisma/migrations/0390_handover_creator_set_null/migration.sql
new file mode 100644
index 000000000..22525bd7d
--- /dev/null
+++ b/prisma/migrations/0390_handover_creator_set_null/migration.sql
@@ -0,0 +1,14 @@
+-- v1.42 (#959): a handover outlives the Guardian who minted it.
+--
+-- `created_by_id` cascaded, so a Guardian deleting their own account deleted
+-- every handover they had issued, including a claimed one whose decision the
+-- new owner had not made yet. The claim had already applied the proposal
+-- (possibly MANAGE for a co-Guardian), and the owner lost the one place that
+-- asks them to confirm it. The creator is now cleared instead; an unclaimed
+-- link without a creator is refused by the claim like any link whose creator
+-- lost access.
+ALTER TABLE "managed_profile_handovers" ALTER COLUMN "created_by_id" DROP NOT NULL;
+
+ALTER TABLE "managed_profile_handovers" DROP CONSTRAINT "managed_profile_handovers_created_by_id_fkey";
+
+ALTER TABLE "managed_profile_handovers" ADD CONSTRAINT "managed_profile_handovers_created_by_id_fkey" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/prisma/migrations/0391_environment_aq_history/migration.sql b/prisma/migrations/0391_environment_aq_history/migration.sql
new file mode 100644
index 000000000..67b503977
--- /dev/null
+++ b/prisma/migrations/0391_environment_aq_history/migration.sql
@@ -0,0 +1,4 @@
+-- Progress of the air-quality history backfill (days covered of days the
+-- source can serve). Written by the background job only; null until its
+-- first run.
+ALTER TABLE "users" ADD COLUMN "environment_aq_history_json" JSONB;
diff --git a/prisma/migrations/0392_correlation_seasonal_engine_since/migration.sql b/prisma/migrations/0392_correlation_seasonal_engine_since/migration.sql
new file mode 100644
index 000000000..4a9b66e3c
--- /dev/null
+++ b/prisma/migrations/0392_correlation_seasonal_engine_since/migration.sql
@@ -0,0 +1,4 @@
+-- When the instance first stored a discovery finding under the seasonally
+-- adjusted correlation engine. Stamped once by the engine itself; null until
+-- its first run, which counts every existing pattern as an earlier finding.
+ALTER TABLE "app_settings" ADD COLUMN "correlation_seasonal_engine_since" TIMESTAMP(3);
diff --git a/prisma/migrations/0393_google_health_backfill_progress/migration.sql b/prisma/migrations/0393_google_health_backfill_progress/migration.sql
new file mode 100644
index 000000000..f061f5961
--- /dev/null
+++ b/prisma/migrations/0393_google_health_backfill_progress/migration.sql
@@ -0,0 +1,4 @@
+-- The collections an in-flight Google Health history backfill has already
+-- walked to the end, so a retried attempt resumes with the collections it did
+-- not reach instead of starting over. Null when no backfill is in flight.
+ALTER TABLE "google_health_connections" ADD COLUMN "backfill_progress" JSONB;
diff --git a/prisma/migrations/0395_medication_auto_mark_taken/migration.sql b/prisma/migrations/0395_medication_auto_mark_taken/migration.sql
new file mode 100644
index 000000000..ddabd35c9
--- /dev/null
+++ b/prisma/migrations/0395_medication_auto_mark_taken/migration.sql
@@ -0,0 +1,7 @@
+-- Per-medication option to record each scheduled dose as taken without a
+-- tap, the instant it was last switched on (no slot before it is filled),
+-- and the marker on an intake the auto-take job recorded.
+ALTER TABLE "medications" ADD COLUMN "auto_mark_taken" BOOLEAN NOT NULL DEFAULT false;
+ALTER TABLE "medications" ADD COLUMN "auto_mark_taken_since" TIMESTAMPTZ(6);
+ALTER TABLE "medication_intake_events" ADD COLUMN "auto_recorded" BOOLEAN NOT NULL DEFAULT false;
+
diff --git a/prisma/migrations/0397_measurements_noted_live_idx/migration.sql b/prisma/migrations/0397_measurements_noted_live_idx/migration.sql
new file mode 100644
index 000000000..de777e276
--- /dev/null
+++ b/prisma/migrations/0397_measurements_noted_live_idx/migration.sql
@@ -0,0 +1,21 @@
+-- v1.42.2 — the record search reads the notes on readings newest first.
+--
+-- A record can hold millions of readings and a handful of notes. Without an
+-- index that holds only the noted rows, the search walks the account's whole
+-- `(user_id, measured_at)` range to find them. This partial index holds the
+-- live rows that carry a note (the encrypted column, or the legacy plaintext
+-- one the boot backfill has not migrated yet), so the read is the handful.
+--
+-- Additive only, no table rewrite, no data change. Non-concurrent for the
+-- reason migration 0243 gives: Prisma Migrate runs a migration file in a
+-- transaction. The build is not short on a large instance: the predicate
+-- only decides which rows land in the index, so Postgres still scans the
+-- whole `measurements` table to find them, and the non-concurrent build
+-- blocks writes to the table while it runs. That is acceptable because the
+-- entrypoint applies migrations before the app serves traffic, so no request
+-- is waiting on those writes; an upgrade on a table with many millions of
+-- readings simply takes longer to come up.
+CREATE INDEX IF NOT EXISTS "measurements_noted_live_idx"
+ ON "measurements" ("user_id", "measured_at" DESC, "id" DESC)
+ WHERE "deleted_at" IS NULL
+ AND ("notes_encrypted" IS NOT NULL OR "notes" IS NOT NULL);
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6a05a5659..20b178b71 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -11,7 +11,8 @@
// round-trip the same instant. The invariant every reader, writer, and
// raw-SQL query MUST hold is:
//
-// * The application server and the Postgres session run in UTC.
+// * The application server and the Postgres session run in UTC (every
+// connection pins TimeZone=UTC, see src/lib/db-session.ts).
// * Wall-clock / calendar-local anchoring lives in an explicit `tz`
// string column (e.g. `MoodEntry.tz`) or the user's `timezone`, NOT
// in the column type.
@@ -64,9 +65,11 @@ model User {
///
/// A timestamp rather than a boolean: it says since when, and preserves a
/// future-compatible null state without adding a transition surface here.
- /// This release does not expose a way to clear the marker or attach
- /// credentials; the grants and activity trail remain intact if a later,
- /// separately designed handover ever needs that state.
+ /// Cleared by the handover flow (`ManagedProfileHandover`): when the
+ /// person the profile describes claims it with credentials of their own,
+ /// the marker goes back to null in the same transaction that settles the
+ /// guardians' grants, so the account becomes an ordinary adult account.
+ /// The grants and activity trail stay intact across that transition.
///
/// Consumers land in this release: the guardian resolver in
/// `src/lib/api-handler.ts` reads it on every guardian request, and the
@@ -91,16 +94,32 @@ model User {
// without a home set) simply has no environment rows. `homeTimezone` anchors
// the day-key an observation is stored under (it can differ from the display
// `timezone` above for an account that tracks weather for another locale).
- homeLat Float? @map("home_lat")
- homeLon Float? @map("home_lon")
- homeLabel String? @map("home_label")
- homeTimezone String? @map("home_timezone")
+ homeLat Float? @map("home_lat")
+ homeLon Float? @map("home_lon")
+ homeLabel String? @map("home_label")
+ homeTimezone String? @map("home_timezone")
// v1.25 — effective-from instant for the home location. Stamped the moment a
// home is set/updated. Day resolution attributes a past day to home ONLY when
// the day is on/after this instant; earlier days are skipped (never fabricated
// from the current home). To fill the deep past the user adds an explicit
// dated location period instead. Backfill defaults its start to this instant.
- homeSince DateTime? @map("home_since")
+ homeSince DateTime? @map("home_since")
+ /// v1.42 — sealed home location (lat, lon, label)
+ /// under AAD `healthlog/environment-location/v1`. Replaces the plaintext
+ /// `homeLat` / `homeLon` / `homeLabel` once the free-text encryption backfill
+ /// reports zero remaining rows; the plaintext columns drop in v1.43.
+ homeLocationEncrypted Bytes? @map("home_location_encrypted")
+ /// v1.42 — per-user switch for the air-quality and
+ /// pollen part of the environment module. On by default; off means the
+ /// nightly fetch skips the air-quality feed and the surfaces leave the
+ /// fields out. The operator switch `ENVIRONMENT_AIR_QUALITY_DISABLED` wins.
+ environmentAirQualityEnabled Boolean @default(true) @map("environment_air_quality_enabled")
+ /// v1.42 — progress of the air-quality history backfill
+ /// (`src/lib/environment/air-quality-history.ts`): how many past days with
+ /// entries the source can cover, how many carry air quality, when it was
+ /// last checked and whether it is through. Written by the job only; read by
+ /// `GET /api/environment` for the settings card. Null until the first run.
+ environmentAqHistoryJson Json? @map("environment_aq_history_json")
// Codex OAuth (ChatGPT subscription)
codexAccessTokenEncrypted String? @map("codex_access_token_encrypted")
@@ -298,6 +317,12 @@ model User {
// once the user has explicitly toggled the view or saved an order.
// Shape: { "version": 1, "view": "table", "order": ["", ...] }
medicationListLayoutJson Json? @map("medication_list_layout_json")
+ // v1.42 — per-user document vault presentation: the card/list view and
+ // the stacked/flowing month arrangement. Display-only, preserve-when-absent
+ // PUT on `/api/documents/inbound/layout`, resolved field by field on read
+ // (`src/lib/documents/documents-layout.ts`). Null = defaults (cards, stacked).
+ // Shape: { "version": 1, "view": "list", "arrangement": "flow" }
+ documentsLayoutJson Json? @map("documents_layout_json")
// v1.17.0 — per-user mood-tag presentation: group order + per-group tag
// placement (catalogue tags "moved" into a group live here only — their
// `categoryId` never changes). Display-only, preserve-when-absent PUT,
@@ -765,6 +790,8 @@ model User {
dataBackups DataBackup[]
backupRestoreJobs BackupRestoreJob[]
offhostBackupState OffhostBackupState?
+ backupPassAttempts BackupPassAttempt[]
+ documentThumbnailFailures DocumentThumbnailFailure[]
achievements UserAchievement[]
recommendationFeedback RecommendationFeedback[]
idempotencyKeys IdempotencyKey[]
@@ -969,6 +996,20 @@ model User {
// `vaccinations` module key, which governs the surfaces and not the store.
vaccinationRecords VaccinationRecord[]
vaccinationDocumentLinks VaccinationDocumentLink[]
+ // v1.42 — user-defined vaccines for products the shipped catalogue does
+ // not list (#1005).
+ customVaccines CustomVaccine[]
+ // v1.42 — per-HealthKit-type arrival ledger (#1173).
+ healthKitTypeSyncs HealthKitTypeSync[]
+ // v1.42 — managed-profile handover links (#959): the profile being handed
+ // over, and the guardian who minted the link.
+ handoversOfProfile ManagedProfileHandover[] @relation("HandoverProfile")
+ handoversCreated ManagedProfileHandover[] @relation("HandoverCreator")
+ // v1.42 — dated anchors in the person's life for the timeline (#613): a
+ // move, a birth, a new job. Shared under the `profile` domain; never sent to
+ // a model.
+ lifeEvents LifeEvent[]
+ measurementFoldRepair MeasurementFoldRepair?
// v1.7.0 profile — optional patient-identity fields surfaced on the
// health-record export cover (PDF) and the FHIR `Patient` resource.
@@ -1072,16 +1113,21 @@ model EnvironmentContext {
date String
/// Coarse resolved location for the day (lat/lon rounded to 1 decimal, about
- /// 11 km, + label + the source that won precedence).
- lat Float
- lon Float
+ /// 11 km, + label + the source that won precedence). Nullable since v1.42:
+ /// the sealed copy in `locationEncrypted` replaces the plaintext once the
+ /// encryption backfill has run; the plaintext columns drop in v1.43.
+ lat Float?
+ lon Float?
/// @internal: human-readable label of the resolved location, stored as
/// operator/debug provenance beside the coordinates; surfaces select the
/// numeric and weather columns only, never this label. The backup selects it
/// (`src/lib/export/environment-backup.ts`) with the rest of the row: a
/// restored reading has to keep saying where it was read.
- locationLabel String @map("location_label")
- source EnvironmentLocationSource
+ locationLabel String? @map("location_label")
+ /// v1.42 — sealed (lat, lon, label) for the day,
+ /// AAD `healthlog/environment-location/v1`.
+ locationEncrypted Bytes? @map("location_encrypted")
+ source EnvironmentLocationSource
/// Daily temperature (°C). Min doubles as the overnight-low proxy.
tempMin Float? @map("temp_min")
@@ -1109,6 +1155,54 @@ model EnvironmentContext {
/// WMO weather code for the day (descriptive condition lookup).
weatherCode Int? @map("weather_code")
+ // v1.42 (#615) — air quality, pollen and UV from the Open-Meteo
+ // air-quality feed (CAMS). All nullable: a partial or absent feed stores
+ // what it has, and a day the feed never covered stays NULL, never zero.
+ /// Daily apparent ("feels-like") maximum (°C).
+ apparentMax Float? @map("apparent_max")
+ /// PM2.5 daily mean (µg/m³).
+ pm25Mean Float? @map("pm25_mean")
+ /// PM2.5 hourly maximum over the local day (µg/m³).
+ pm25Max Float? @map("pm25_max")
+ /// PM10 daily mean (µg/m³).
+ pm10Mean Float? @map("pm10_mean")
+ /// Nitrogen dioxide daily mean (µg/m³).
+ no2Mean Float? @map("no2_mean")
+ /// Sulphur dioxide daily mean (µg/m³).
+ so2Mean Float? @map("so2_mean")
+ /// Carbon monoxide daily mean (µg/m³).
+ coMean Float? @map("co_mean")
+ /// Ozone, maximum 8-hour running mean of the day (µg/m³).
+ o3Max8h Float? @map("o3_max_8h")
+ /// European AQI, daily maximum.
+ eaqiMax Float? @map("eaqi_max")
+ /// US AQI, daily maximum.
+ usaqiMax Float? @map("usaqi_max")
+ /// UV index, daily maximum.
+ uvIndexMax Float? @map("uv_index_max")
+ /// Saharan dust, daily maximum (µg/m³).
+ dustMax Float? @map("dust_max")
+ /// Aerosol optical depth, daily maximum.
+ aodMax Float? @map("aod_max")
+ /// Alder pollen, daily maximum (grains/m³, Europe only).
+ pollenAlderMax Float? @map("pollen_alder_max")
+ /// Birch pollen, daily maximum (grains/m³, Europe only).
+ pollenBirchMax Float? @map("pollen_birch_max")
+ /// Grass pollen, daily maximum (grains/m³, Europe only).
+ pollenGrassMax Float? @map("pollen_grass_max")
+ /// Mugwort pollen, daily maximum (grains/m³, Europe only).
+ pollenMugwortMax Float? @map("pollen_mugwort_max")
+ /// Olive pollen, daily maximum (grains/m³, Europe only).
+ pollenOliveMax Float? @map("pollen_olive_max")
+ /// Ragweed pollen, daily maximum (grains/m³, Europe only).
+ pollenRagweedMax Float? @map("pollen_ragweed_max")
+ /// Which air-quality model domain served the day (`cams_europe` / `cams_global`).
+ aqDomain String? @map("aq_domain")
+ /// Hourly air-quality samples the day was aggregated from (75 % rule).
+ aqHours Int? @map("aq_hours")
+ /// When the air-quality part was last fetched; NULL = not yet fetched (gap index).
+ aqFetchedAt DateTime? @map("aq_fetched_at")
+
/// When the row was last fetched/refreshed from the upstream feed.
fetchedAt DateTime @default(now()) @map("fetched_at")
createdAt DateTime @default(now()) @map("created_at")
@@ -1116,6 +1210,9 @@ model EnvironmentContext {
@@unique([userId, date])
@@index([userId, date])
+ // `environment_contexts_aq_gap_idx` (user_id, date) WHERE aq_fetched_at IS
+ // NULL and `environment_contexts_plain_location_idx` (id) WHERE lat IS NOT
+ // NULL are partial indexes from migrations 0381 / 0382 (raw SQL only).
@@map("environment_contexts")
}
@@ -1136,14 +1233,22 @@ model EnvironmentTravelLocation {
startDate String @map("start_date")
endDate String @map("end_date")
- lat Float
- lon Float
- label String
+ /// Plaintext coarse location. Nullable since v1.42: the sealed copy in
+ /// `locationEncrypted` replaces it once the encryption backfill has run;
+ /// the plaintext columns drop in v1.43.
+ lat Float?
+ lon Float?
+ label String?
+ /// v1.42 — sealed (lat, lon, label) for the
+ /// period, AAD `healthlog/environment-location/v1`.
+ locationEncrypted Bytes? @map("location_encrypted")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([userId, startDate, endDate])
+ // `environment_travel_locations_plain_location_idx` (id) WHERE lat IS NOT
+ // NULL is a partial index from migration 0382 (raw SQL only).
@@map("environment_travel_locations")
}
@@ -1789,6 +1894,12 @@ enum MeasurementSource {
// user's own. iOS DTO must mirror this exact spelling so the
// /api/sync/changes delta feed decodes the rows.
EXTERNAL
+ // v1.42 — rows imported from an Android Health Connect export (#972).
+ // Server-owned: written only by the Health Connect import worker, never
+ // by a client, so the batch + single-POST surfaces reject it like IMPORT.
+ // iOS DTO must mirror this spelling so the /api/sync/changes delta feed
+ // decodes the rows.
+ HEALTH_CONNECT
@@map("measurement_source")
}
@@ -1985,6 +2096,12 @@ model Measurement {
// without this one the purge planned a sequential scan and could not finish
// inside statement_timeout. Raw SQL only (partial predicate not expressible
// in Prisma).
+ // `measurements_noted_live_idx` is a partial index over `(user_id,
+ // measured_at DESC, id DESC) WHERE deleted_at IS NULL AND (notes_encrypted
+ // IS NOT NULL OR notes IS NOT NULL)` — see migration 0397 (v1.42.2). The
+ // record search reads the notes on readings newest first; without it a
+ // record of millions of readings with a few notes walks every reading.
+ // Raw SQL only (partial predicate not expressible in Prisma).
@@map("measurements")
}
@@ -2683,6 +2800,20 @@ model Medication {
/// (the stretch in between archives with an empty payload), so the rate
/// paths never count the untracked stretch as missed.
trackIntake Boolean @default(true) @map("track_intake")
+ /// v1.42.2 — record every scheduled dose as taken without a tap. For a
+ /// person who takes a fixed dose reliably (evening supplements) and does not
+ /// want to confirm it each day. The `intake-auto-take` job writes a taken
+ /// intake for each slot of a tracked, active, unpaused medication once the
+ /// slot's time has passed, marked `autoRecorded`; a slot the person already
+ /// took, skipped or removed is never touched, and no slot before
+ /// `autoMarkTakenSince` is ever filled. While on, no medication reminder is
+ /// sent for it on any channel, and the hourly auto-miss pass leaves the
+ /// slots this job owns alone.
+ autoMarkTaken Boolean @default(false) @map("auto_mark_taken")
+ /// v1.42.2 — when `autoMarkTaken` was last switched on. The auto-take job
+ /// never fills a slot anchored before it, so switching the option on does
+ /// not rewrite the history before that moment. NULL while the option is off.
+ autoMarkTakenSince DateTime? @map("auto_mark_taken_since") @db.Timestamptz(6)
/// v1.6.0 — route of administration. Decoupled from `treatmentClass`
/// so the injection-site picker can surface for any INJECTION dose.
/// A one-time injection is `oneShot = true` + `deliveryForm =
@@ -3188,6 +3319,12 @@ model MedicationIntakeEvent {
/// legacy row defaults to false (a user skip or a live pending row,
/// neither of which is an auto-miss).
autoMissed Boolean @default(false) @map("auto_missed")
+ /// v1.42.2 — true when the `intake-auto-take` job recorded this dose
+ /// because the medication has `autoMarkTaken` on, not the person. It counts
+ /// as taken everywhere (compliance, history, Coach); history lists label it
+ /// "recorded automatically". Any explicit take or skip of the slot by the
+ /// person clears it.
+ autoRecorded Boolean @default(false) @map("auto_recorded")
/// v1.15.20 — how this row's `scheduledFor` slot binding came to be.
/// `AUTO` (default) = the write path attributed the take by window-band
/// membership (or the row is a skip / pending mint anchored on its slot).
@@ -3812,6 +3949,71 @@ model InviteToken {
@@map("invite_tokens")
}
+/// v1.42 (#959) — a one-time link that hands a managed profile over to the
+/// person it describes. Minted by a guardian with MANAGE on the profile; the
+/// token (`hlp_` + 64 hex) is shown once and only its HMAC-SHA256 hash is
+/// stored, the `InviteToken` scheme. Claiming sets credentials on the
+/// profile's own `User` row (no data moves), clears `managedProfileAt` and
+/// settles each guardian's grant by the proposal recorded here. At most one
+/// open link per profile (partial unique index in migration 0383).
+model ManagedProfileHandover {
+ id String @id @default(cuid())
+ /// The managed profile (`User` row with `managedProfileAt` set).
+ profileId String @map("profile_id")
+ /// The guardian who minted the link. Cleared when that account is deleted
+ /// (0390): a claimed handover keeps the new owner's pending decision, and
+ /// an unclaimed link without a creator is refused like any link whose
+ /// creator lost access.
+ createdById String? @map("created_by_id")
+ /// HMAC-SHA256 of the token, unique; the claim and its preview look the
+ /// link up by it (`src/lib/managed-profiles/handover.ts`).
+ tokenHash String @unique @map("token_hash")
+ /// The handover's state, versioned and read fail-closed by
+ /// `parseHandoverState`: the issuing guardian's proposal per guardian
+ /// (end / read / manage), and after the claim what each guardian was given
+ /// and the new owner's final decision. The pending-decision read on the new
+ /// owner's account is answered from here.
+ proposalsJson Json @map("proposals_json")
+ createdAt DateTime @default(now()) @map("created_at")
+ /// Link expiry (1, 7 or 14 days after minting).
+ expiresAt DateTime @map("expires_at")
+ /// Set when the profile was claimed through the link; the token is spent.
+ usedAt DateTime? @map("used_at")
+ /// Set when a guardian withdrew the link, a newer link replaced it, or its
+ /// creator lost access.
+ revokedAt DateTime? @map("revoked_at")
+
+ profile User @relation("HandoverProfile", fields: [profileId], references: [id], onDelete: Cascade)
+ createdBy User? @relation("HandoverCreator", fields: [createdById], references: [id], onDelete: SetNull)
+
+ @@index([profileId])
+ // `managed_profile_handovers_open_idx` UNIQUE (profile_id) WHERE used_at IS
+ // NULL AND revoked_at IS NULL is a partial index from migration 0383.
+ @@map("managed_profile_handovers")
+}
+
+/// v1.42 (#1173) — when each HealthKit type last arrived through the batch
+/// ingest, and by which trigger. One row per (user, type), upserted by the
+/// batch route; read by the Apple Health card's per-type freshness list.
+/// Diagnostic state: rebuilt by the next sync, so it is not backed up.
+model HealthKitTypeSync {
+ userId String @map("user_id")
+ type MeasurementType
+ /// Last batch that carried this type.
+ lastReceivedAt DateTime @map("last_received_at")
+ /// `syncTrigger` of that batch
+ /// (`foreground` / `background` / `push` / `manual`), NULL when absent.
+ lastTrigger String? @map("last_trigger")
+ /// Last batch that inserted or updated a sample of
+ /// this type (as opposed to sending only duplicates).
+ lastNewSampleAt DateTime? @map("last_new_sample_at")
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@id([userId, type])
+ @@map("healthkit_type_syncs")
+}
+
// v1.16.0 — one row per signup an invite admitted. `InviteToken.usedBy`
// keeps only the LAST consumer (it overwrites on multi-use invites);
// this ledger carries the full history so the admin table can show
@@ -4194,6 +4396,11 @@ model GoogleHealthConnection {
/// pre-fix externalId (a re-scored night minted parallel copies). Null =
/// repair pending (or the connection predates the fix).
sleepRepairedAt DateTime? @map("sleep_repaired_at")
+ /// The collections the in-flight history backfill has already walked to
+ /// the end (`{ done: string[] }`), so a retry resumes with the rest
+ /// instead of starting over. Null once the backfill completes, and reset
+ /// on every reconnect.
+ backfillProgress Json? @map("backfill_progress")
/// One bounded, owner-scoped current sync run. This is replaced at the start
/// of every run and never retains provider payloads or historical runs.
syncProgress Json? @map("sync_progress")
@@ -4948,6 +5155,17 @@ model AppSettings {
assistantInsightStatusEnabled Boolean @default(true) @map("assistant_insight_status_enabled")
assistantDocumentAiEnabled Boolean @default(true) @map("assistant_document_ai_enabled")
+ // v1.41 — the operator's reasoning controls. `aiReasoningEnabled` switches
+ // provider-side reasoning off for every surface (the Coach and the three
+ // background jobs that use it); `aiReasoningMaxEffort` caps the level a
+ // person may pick (`low` | `medium` | `high`, an app-side closed enum in
+ // `src/lib/ai/reasoning/levels.ts`). Resolved once per turn or job by
+ // `src/lib/ai/reasoning/resolve.ts` and published per record on
+ // `GET /api/auth/me` as `coachReasoning`. Defaults keep reasoning on with no
+ // cap, so an upgrade needs no admin action.
+ aiReasoningEnabled Boolean @default(true) @map("ai_reasoning_enabled")
+ aiReasoningMaxEffort String @default("high") @map("ai_reasoning_max_effort")
+
// v1.18.0 — operator-level server-wide module availability. A DISABLED
// allowlist mirroring the per-user `User.modulePreferencesJson`: absent /
// empty / `true` ⇒ available, an explicit `false` ⇒ disabled for EVERY
@@ -4985,6 +5203,14 @@ model AppSettings {
/// step names who confirmed when the account still exists.
encryptionKeyBackupConfirmedByUserId String? @map("encryption_key_backup_confirmed_by_user_id")
+ /// When this instance first stored a discovery finding under the seasonally
+ /// adjusted correlation engine (v1.42). Stamped once, by the first
+ /// `syncAcceptedPatterns` run of a discovery family, before it writes; null
+ /// until then. Consumer: `hadFindingsBeforeSeasonalAdjustment`, which counts
+ /// a discovery pattern created before it (or any, while it is null) as a
+ /// finding of the earlier engine and so earns the one-time note.
+ correlationSeasonalEngineSince DateTime? @map("correlation_seasonal_engine_since")
+
@@map("app_settings")
}
@@ -5160,7 +5386,7 @@ model DataBackup {
/// What the restore preview shows for this copy, worked out while the copy
/// was written (`src/lib/export/backup-preview.ts`): the section counts, the
/// schema version, and the inner key ids with the sections they sit in and
- /// one sample value each, so the key verdict is taken again against this
+ /// the shortest value of each section, so the key verdict is taken again against this
/// server's keys every time it is read. It names the copy it describes and
/// is ignored once the copy changes. Plain JSON on purpose: counts and dates
/// only, and the samples are ciphertext already, which a key rotation must
@@ -5335,6 +5561,32 @@ model OffhostBackupState {
@@map("offhost_backup_state")
}
+/// Where one account stands in a backup pass that walks every account (the
+/// weekly copy, `data-backup`, and the nightly off-host upload,
+/// `data-backup-offhost`): when the pass last started the account and when it
+/// last got past it, stored or failed in a way the pass caught. A start newer
+/// than the finish, with no run of that pass active, is an attempt the
+/// process died under: a restart, or a container killed for memory by a
+/// record too large for it. The pass takes such an account last, so one
+/// account cannot keep every account after it from a copy, and the admin
+/// backups page names it.
+/// Consumers: `orderInterruptedLast()` in both passes, and
+/// `readBackupPassActivity()` behind `GET /api/admin/backups`.
+model BackupPassAttempt {
+ userId String @map("user_id")
+ /// The pass's queue name: `data-backup` or `data-backup-offhost`.
+ pass String
+ /// When the pass last started on this account.
+ startedAt DateTime @map("started_at")
+ /// When the pass last got past it. NULL until it has once.
+ finishedAt DateTime? @map("finished_at")
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@id([userId, pass])
+ @@map("backup_pass_attempts")
+}
+
/// Per application key id, when the nightly off-host run last put an object
/// in the bucket whose inner ciphertext needed that key. The bucket's
/// lifecycle rule keeps each object for its retention, so a retired key is
@@ -5400,6 +5652,13 @@ model ImportJob {
pgBossJobId String? @unique @map("pg_boss_job_id")
/// `queued | unpacking | parsing | upserting | done | failed`.
status String @default("queued")
+ /// v1.42 (#972) — which importer owns the row: `apple_health` (every row
+ /// before v1.42) or `health_connect`. The reconcile cron and the
+ /// staged-file sweep look each row up on its own kind's queue
+ /// (`src/lib/jobs/apple-health-import-worker.ts`), so one importer never
+ /// fails or deletes the other's running job; the upload dedupe and the
+ /// Health Connect status route scope by it.
+ kind String @default("apple_health")
/// Free-text reason on `failed`; null otherwise.
failureReason String? @map("failure_reason")
/// Bytes uploaded — surfaced to the operator audit trail.
@@ -5447,6 +5706,7 @@ model ImportJob {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, startedAt])
+ @@index([userId, kind, startedAt])
@@index([status])
@@map("import_jobs")
}
@@ -5545,15 +5805,19 @@ model ProviderHealth {
/// (userId, key, method, path) so retries within the TTL replay the
/// original outcome instead of producing a duplicate side-effect.
model IdempotencyKey {
- id String @id @default(cuid())
- userId String @map("user_id")
- key String
- method String
- path String
- responseStatus Int @map("response_status")
- responseBody String @map("response_body") // serialized JSON envelope
- expiresAt DateTime @map("expires_at")
- createdAt DateTime @default(now()) @map("created_at")
+ id String @id @default(cuid())
+ userId String @map("user_id")
+ key String
+ method String
+ path String
+ responseStatus Int @map("response_status")
+ responseBody String @map("response_body") // serialized JSON envelope
+ /// SHA-256 of the canonical request body, scoped by method and path. A
+ /// lookup replays only when it matches; a different body under the same key
+ /// runs normally. Null on rows written before it existed, which never match.
+ requestFingerprint String? @map("request_fingerprint")
+ expiresAt DateTime @map("expires_at")
+ createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -5886,6 +6150,13 @@ model LabResult {
sourceReferenceLow Float? @map("source_reference_low")
sourceReferenceHigh Float? @map("source_reference_high")
sourceReferenceText String? @map("source_reference_text")
+ /// @pending-consumer(#1095) v1.42 — the value as printed on the source
+ /// report, kept when a write converted it into the biomarker's canonical
+ /// unit. NULL when no conversion happened. A later edit of `value` clears it.
+ sourceValue Float? @map("source_value")
+ /// @pending-consumer(#1095) v1.42 — the unit the source report printed,
+ /// paired with `sourceValue`.
+ sourceUnit String? @map("source_unit")
/// When the sample was taken (UTC instant; wall-clock anchoring is the
/// reader's concern per the schema's UTC-only convention).
takenAt DateTime @map("taken_at")
@@ -5954,6 +6225,10 @@ model Biomarker {
/// and from the lab-entry pickers, but never deleted — its readings and
/// canonical unit/range definition survive.
hidden Boolean @default(false) @map("hidden")
+ /// @pending-consumer(#1095) v1.42 — the catalogue analyte this marker
+ /// resolves to (alias list in `src/lib/labs/biomarker-catalog.ts`), which
+ /// selects the unit-conversion table. NULL = not resolved; no conversion.
+ analyteKey String? @map("analyte_key")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -6403,6 +6678,13 @@ model CoachMessage {
// only the tables' metadata. Read through `readMessageResults` by the
// owner-narrowed results endpoint; NULL on every turn without a table.
resultsEncrypted Bytes? @map("results_encrypted")
+ // v1.41 — the model-written text of the turn's live trail (JSON
+ // `CoachTrail`, at most 16 KB): reasoning titles and texts per round, the
+ // facts recalled into the turn, the text of a fact proposal. AES-256-GCM
+ // like `encryptedContent`. The trail's plaintext metadata (phase, status,
+ // round, catalog label, duration, count, stop reason) rides
+ // `metricSourceJson.activity`. Owner-only; NULL on every turn without one.
+ trailEncrypted Bytes? @map("trail_encrypted")
createdAt DateTime @default(now()) @map("created_at")
conversation CoachConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@ -6445,6 +6727,18 @@ model CoachFact {
updatedAt DateTime @updatedAt @map("updated_at")
// Soft-delete: hides the fact from injection but keeps the row for audit.
deletedAt DateTime? @map("deleted_at")
+ // v1.41 — who put the fact here, an app-side closed enum
+ // (`COACH_FACT_SOURCES` in `src/lib/ai/coach/memory/contract.ts`): `user`
+ // (the remember button, or a proposal the person confirmed), `coach` (saved
+ // during a turn), `extracted` (the background extraction, and every row
+ // written before the column existed), `pattern` (the deterministic matcher).
+ source String @default("extracted")
+ // v1.41 — when the fact last went into a Coach turn; NULL until it has.
+ lastUsedAt DateTime? @map("last_used_at")
+ // v1.41 — the message the fact came out of, for "undo" and "from this
+ // conversation". A bare id like `sourceConversationId`, not a relation, so
+ // deleting a conversation does not take the fact with it.
+ sourceMessageId String? @map("source_message_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -8365,28 +8659,29 @@ model InboundDocument {
/// explicitly reads it with AI or generates its summary.
aiReadDeferred Boolean @default(false) @map("ai_read_deferred")
- facts ExtractedFact[]
- conditionLinks DocumentConditionLink[]
+ facts ExtractedFact[]
+ conditionLinks DocumentConditionLink[]
/// 1:1 blind content-search index (encrypted extracted text + HMAC token
/// array). Absent until the document is indexed; cascades on delete.
- contentIndex DocumentContentIndex?
+ contentIndex DocumentContentIndex?
/// 1:1 encrypted preview thumbnail (small JPEG, AES-256-GCM at rest).
/// Absent until the background thumbnail job runs; a side table (not a
/// column) so the list/detail `omit: { contentEncrypted }` queries never
/// drag the blob into the SELECT. Cascades on delete.
- thumbnail DocumentThumbnail?
+ thumbnail DocumentThumbnail?
+ thumbnailFailure DocumentThumbnailFailure?
/// v1.29.x (S7) — coach-conversation attachments (m:n via
/// `CoachConversationDocument`). Deleting this document cascades its join rows
/// away (its text leaves every conversation's context) but never the
/// conversation itself — a fenced thread survives via its sticky flag.
- chatAttachments CoachConversationDocument[]
+ chatAttachments CoachConversationDocument[]
/// share-link memberships (m:n via `ClinicianShareLinkDocument`).
/// Cascades: deleting this document removes it from every share it was on.
- shareLinks ClinicianShareLinkDocument[]
+ shareLinks ClinicianShareLinkDocument[]
/// Further import source keys that resolved to this document by content.
- sourceAliases DocumentSourceAlias[]
+ sourceAliases DocumentSourceAlias[]
/// Background AI reads of this document (v1.40). Transient; cascades.
- aiRuns DocumentAiRun[]
+ aiRuns DocumentAiRun[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -8678,6 +8973,34 @@ model DocumentThumbnail {
@@map("document_thumbnails")
}
+/// Why a document has no preview thumbnail, kept so the boot-time backfill
+/// stops queueing a document whose render cannot succeed. A PDF whose first
+/// page renders blank was queued, rendered, refused and queued again at
+/// every boot, without end. A deterministic refusal (`raster-failed`,
+/// `empty-render`, `pixel-cap`) is final after one attempt; any other reason
+/// gets three. Either way the document is tried again once it changes
+/// (`updatedAt` past `failedAt`), and a direct enqueue (an upload) still
+/// renders it. A preview that is made deletes the row.
+/// Consumers: `runThumbnailBackfillForUser()` and
+/// `enqueueBootTimeThumbnailBackfill()` in
+/// `src/lib/jobs/document-thumbnail-backfill.ts`.
+model DocumentThumbnailFailure {
+ documentId String @id @map("document_id")
+ userId String @map("user_id")
+ /// The renderer's reason (`ThumbnailSkipReason`), or `decrypt-failed`.
+ reason String
+ /// Renders that failed since the last success or change.
+ attempts Int @default(1)
+ /// When the newest of them failed.
+ failedAt DateTime @map("failed_at")
+
+ document InboundDocument @relation(fields: [documentId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@index([userId])
+ @@map("document_thumbnail_failures")
+}
+
// ─── Nutrient intake (v1.28) ───────────────────────────────────────
//
// Micronutrient daily totals synced from Apple Health — vitamins,
@@ -9029,13 +9352,13 @@ model EncounterConditionLink {
/// module is off; these data routes stay exempt so a restore or an import
/// keeps working and re-enabling finds every row intact.
model VaccinationRecord {
- id String @id @default(cuid())
- userId String @map("user_id")
+ id String @id @default(cuid())
+ userId String @map("user_id")
/// The day the dose was given, at UTC midnight. An Impfpass carries dates,
/// never times, so the time component is not meaningful and is not
/// collected. The API refuses future instants: a vaccination is logged
/// after the fact, and a planned booster is a reminder, not a record.
- occurredAt DateTime @map("occurred_at")
+ occurredAt DateTime @map("occurred_at")
/// Catalogue identity. NULL = a free-text-only row, which is the arm that
/// makes a 1987 Pass line loggable at all.
///
@@ -9045,48 +9368,86 @@ model VaccinationRecord {
/// resolve, but a restore writes whatever the backup held. A slug that
/// stops resolving degrades to the free-text arm — a tested behaviour, not
/// an accident (`src/lib/vaccinations/__tests__/catalog-integrity.test.ts`).
- antigenSlug String? @map("antigen_slug")
+ antigenSlug String? @map("antigen_slug")
/// The Pass's own wording, verbatim. May be a trade name; that is user
/// data. Plaintext and bounded so the list can render and search it.
- vaccineName String? @map("vaccine_name")
+ vaccineName String? @map("vaccine_name")
/// The Pass's claim about which dose this is. Optional — the series
/// derivation falls back to the chronological index per component antigen.
- doseNumber Int? @map("dose_number")
+ doseNumber Int? @map("dose_number")
/// Per-record override of the catalogue's typical series length, for the
/// schedule a person was actually given.
- seriesDoses Int? @map("series_doses")
+ seriesDoses Int? @map("series_doses")
/// Chargenbezeichnung (IfSG §22). A batch code, not a secret: plaintext and
/// bounded so the list and the report can render it without a decrypt.
- lotNumber String? @map("lot_number")
- site VaccinationSite?
- practitionerId String? @map("practitioner_id")
- encounterId String? @map("encounter_id")
+ lotNumber String? @map("lot_number")
+ site VaccinationSite?
+ practitionerId String? @map("practitioner_id")
+ encounterId String? @map("encounter_id")
/// The booster reminder this dose satisfied, when one matched. The act runs
/// through `satisfyReminder`; this column is the record of it, not the act.
/// One id for possibly several satisfied rows — the engine keeps
/// `lastSatisfiedAt` per reminder, this keeps the first match in catalogue
/// component order.
- reminderId String? @map("reminder_id")
+ reminderId String? @map("reminder_id")
+ /// v1.42 (#1005) — the user's own vaccine definition when the dose is for a
+ /// product the catalogue does not list, resolved through
+ /// `src/lib/vaccinations/resolve-vaccine-entry.ts`. Catalogue wins when both
+ /// resolve. Removing the definition unlinks the dose and leaves it a name
+ /// (`removeCustomVaccine`); SetNull covers a hard delete the same way.
+ customVaccineId String? @map("custom_vaccine_id")
/// The one free-text field (AES-256-GCM at rest, the `*Encrypted` Bytes
/// convention). Impfreaktionen live here in v1 rather than in a structured
/// reaction model nobody has asked for yet.
- noteEncrypted Bytes? @map("note_encrypted")
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @updatedAt @map("updated_at")
+ noteEncrypted Bytes? @map("note_encrypted")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
/// Soft-delete tombstone. NULL = live; non-NULL = deleted at the instant.
- deletedAt DateTime? @map("deleted_at")
+ deletedAt DateTime? @map("deleted_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
practitioner Practitioner? @relation(fields: [practitionerId], references: [id], onDelete: SetNull)
encounter Encounter? @relation(fields: [encounterId], references: [id], onDelete: SetNull)
reminder MeasurementReminder? @relation(fields: [reminderId], references: [id], onDelete: SetNull)
+ customVaccine CustomVaccine? @relation(fields: [customVaccineId], references: [id], onDelete: SetNull)
documentLinks VaccinationDocumentLink[]
@@index([userId, deletedAt, occurredAt])
@@index([userId, antigenSlug, occurredAt])
+ @@index([customVaccineId])
@@map("vaccination_records")
}
+/// v1.42 (#1005) — a vaccine the user defines for a product the shipped
+/// catalogue does not list. Same shape the resolver hands out for a
+/// catalogue entry (`components` = antigen slugs, typical series length,
+/// booster interval), so series derivation and booster matching treat both
+/// alike. Soft-deleted; a deleted definition leaves its doses standing on
+/// their `vaccineName`.
+model CustomVaccine {
+ id String @id @default(cuid())
+ userId String @map("user_id")
+ /// Display name, unique per user (the API also refuses a case-only twin).
+ name String
+ /// Antigen slugs the product covers, from `ANTIGEN_SLUGS`; storage is
+ /// tolerant, the resolver skips a slug it does not know.
+ components String[]
+ /// Typical primary-series length; the series total for its doses.
+ typicalSeriesDoses Int? @map("typical_series_doses")
+ /// Booster interval in months, the booster offer's prefill; NULL = none.
+ boosterIntervalMonths Int? @map("booster_interval_months")
+ /// Soft-delete tombstone. NULL = live.
+ deletedAt DateTime? @map("deleted_at")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ records VaccinationRecord[]
+
+ @@unique([userId, name])
+ @@map("custom_vaccines")
+}
+
/// Where the dose went. Small and closed, because the Pass's own vocabulary
/// is small and closed; OTHER absorbs the rest rather than growing the enum.
enum VaccinationSite {
@@ -9265,3 +9626,89 @@ model DocumentAiRun {
@@index([status, expiresAt])
@@map("document_ai_runs")
}
+
+/// A dated anchor in the person's life for the timeline (v1.42, #613): a move,
+/// a birth, a new job, a loss. Not a health reading and not a symptom; it gives
+/// the years around the readings something to hang on.
+///
+/// Shared under the `profile` domain, beside allergies, visits and the
+/// immunization history. Never sent to a model in any form: the Coach, the
+/// MCP endpoint and every AI job leave it out, and the day loader's model-facing
+/// projection drops it.
+///
+/// Dates are calendar dates without a time zone (`YYYY-MM-DD`), the
+/// `src/lib/tz/date-only.ts` convention, because "we moved in September 2023"
+/// names a day on a calendar, not an instant. `precision` says how much of the
+/// date the person actually knows; at MONTH or YEAR the stored date is the
+/// first day of that month or year, and only a DAY event appears in a day view.
+model LifeEvent {
+ id String @id @default(cuid())
+ userId String @map("user_id")
+ /// @pending-consumer(#613) The kind of event, from a deliberately short list.
+ category LifeEventCategory
+ /// @pending-consumer(#613) First day of the event, `YYYY-MM-DD`. At MONTH or
+ /// YEAR precision the first day of that month or year.
+ startDate String @map("start_date")
+ /// @pending-consumer(#613) Last day, `YYYY-MM-DD`, for an event that spans a
+ /// period (parental leave, a year abroad). NULL = a single date.
+ endDate String? @map("end_date")
+ /// @pending-consumer(#613) How much of the date is known. Applies to both
+ /// ends.
+ precision LifeEventPrecision
+ /// @pending-consumer(#613) What happened, in the person's own words,
+ /// AES-256-GCM encrypted at rest.
+ titleEncrypted Bytes @map("title_encrypted")
+ /// @pending-consumer(#613) An optional note, AES-256-GCM encrypted at rest.
+ noteEncrypted Bytes? @map("note_encrypted")
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+ /// Soft-delete tombstone. NULL = live.
+ deletedAt DateTime? @map("deleted_at")
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@index([userId, deletedAt, startDate])
+ @@map("life_events")
+}
+
+/// What kind of life event. Few on purpose: travel already comes from the
+/// environment module, and a long list turns an anchor into a diary.
+enum LifeEventCategory {
+ FAMILY
+ HOME
+ WORK
+ LOSS
+ OTHER
+
+ @@map("life_event_category")
+}
+
+/// How much of a life event's date the person knows.
+enum LifeEventPrecision {
+ DAY
+ MONTH
+ YEAR
+
+ @@map("life_event_precision")
+}
+
+/// v1.42 — the fold repair has been through this account.
+///
+/// Before v1.42 the daily-mean consolidation and the dense hourly fold could
+/// fold a local day in two runs, the second overwriting the stored mean with
+/// the mean of the later part alone. The one-time repair
+/// (`src/lib/jobs/measurement-fold-repair.ts`) recomputes every such mean from
+/// the samples still in the table, the fold's own tombstones among them. The
+/// compaction-tombstone purge deletes exactly those tombstones, so it leaves an
+/// account alone until this row exists, and a restore that writes back a file
+/// older than `completedAt`'s run removes the row so the repair goes again.
+model MeasurementFoldRepair {
+ userId String @id @map("user_id")
+ /// When the repair finished the account. A restore compares it with the
+ /// file's export instant; the purge only asks whether the row exists.
+ completedAt DateTime @map("completed_at")
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@map("measurement_fold_repairs")
+}
diff --git a/public/sw.js b/public/sw.js
index 12e07572b..02420a92e 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -36,7 +36,7 @@ try {
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
- /* @sw-version-fallback */ "v1.39.9";
+ /* @sw-version-fallback */ "v1.42.2";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`
@@ -230,8 +230,9 @@ self.addEventListener("fetch", (event) => {
return;
}
- // Locale-catalog boot script — cache-first. The URL is versioned
- // (`/i18n/?v=`) and served immutable, so cache-first is
+ // Locale-catalog boot script — cache-first. The URL is versioned by the
+ // catalogs' content hash (`/i18n/?v=`, `next.config.ts`),
+ // so a changed catalog is a new URL and cache-first is
// correct and keeps the catalog available on an offline relaunch (the
// cached shell HTML references the same versioned URL it was rendered
// with). Carries no user data — same public strings as the repository's
@@ -341,6 +342,14 @@ async function networkFirstApi(request) {
* scores straight into the HTML and emits `no-store`; the explicit
* path skip is belt-and-braces so a revoked share can never linger in
* CacheStorage and render back offline.
+ *
+ * In practice rule 1 covers every signed-in page: `src/proxy.ts` pins
+ * `private, no-store` on all of them, because several server-prefetch one
+ * account's record into the HTML. Only the public pages (sign-in, legal)
+ * land in this cache, so a navigation to an app page without a connection
+ * gets the offline page below, never somebody's cached record. A page that
+ * is already open keeps working offline from the cached chunks and the
+ * allowlisted data reads.
*/
function isCacheableNavigation(request, response) {
const cacheControl = response.headers.get("Cache-Control") || "";
@@ -568,16 +577,28 @@ async function networkFirst(event, cacheName) {
// "Offline" token + a generic retry hint expressed as an icon
// (the round-trip arrow), so the page does the right thing in any
// locale without shipping a translation bundle into the worker.
- return new Response(
- 'HealthLog — Offline
HealthLog
Offline
',
- {
- status: 503,
- headers: { "Content-Type": "text/html; charset=utf-8" },
- },
- );
+ //
+ // v1.42 — the arrow is a real control now (a link to the same URL,
+ // 64 px), the page reloads by itself as soon as the connection is
+ // back, it follows the device's light or dark scheme, and it keeps
+ // clear of the notch and home indicator of an installed app.
+ return new Response(OFFLINE_PAGE, {
+ status: 503,
+ headers: { "Content-Type": "text/html; charset=utf-8" },
+ });
}
}
+const OFFLINE_PAGE =
+ 'HealthLog — Offline
HealthLog
Offline
' +
+ '';
+
/**
* Cache a successful network response without making CacheStorage
* availability part of the fetch contract. Quota, eviction, or trim failures
diff --git a/scripts/__tests__/release-workflows.test.ts b/scripts/__tests__/release-workflows.test.ts
index 3686f9bca..79e27408a 100644
--- a/scripts/__tests__/release-workflows.test.ts
+++ b/scripts/__tests__/release-workflows.test.ts
@@ -249,7 +249,7 @@ describe("release container inputs", () => {
expect(dockerfile).toContain(
"ln -sfn /opt/prisma-cli/node_modules/.bin/tsx /usr/local/bin/healthlog-tsx",
);
- expect(dockerfile).toContain("dotenv@17.4.2");
+ expect(dockerfile).toContain("dotenv@18.0.6");
expect(dockerfile).toContain(
"ln -sfn /opt/prisma-cli/node_modules/dotenv /app/node_modules/dotenv",
);
diff --git a/scripts/backfill-mood-note-column.ts b/scripts/backfill-mood-note-column.ts
index 093d8d2d8..ebdeb514d 100644
--- a/scripts/backfill-mood-note-column.ts
+++ b/scripts/backfill-mood-note-column.ts
@@ -52,6 +52,7 @@ interface RowSnapshot {
id: string;
tags: string | null;
note: string | null;
+ updatedAt: Date;
}
async function main(): Promise {
@@ -74,7 +75,7 @@ async function main(): Promise {
tags: { not: null },
note: null,
},
- select: { id: true, tags: true, note: true },
+ select: { id: true, tags: true, note: true, updatedAt: true },
});
console.log(`[backfill-note] scanned ${candidates.length} candidate rows`);
@@ -87,9 +88,11 @@ async function main(): Promise {
updated += 1;
if (!dryRun) {
+ // The entry did not change for its reader; it keeps its `updatedAt`
+ // (the sync cursor) rather than taking the script's run time.
await prisma.moodEntry.update({
where: { id: row.id },
- data: { note, tags: newTags },
+ data: { note, tags: newTags, updatedAt: row.updatedAt },
});
}
}
diff --git a/scripts/compare-correlation-findings.ts b/scripts/compare-correlation-findings.ts
new file mode 100644
index 000000000..886e5e0f4
--- /dev/null
+++ b/scripts/compare-correlation-findings.ts
@@ -0,0 +1,338 @@
+/**
+ * Read-only before/after count of correlation findings per account, for the
+ * release-candidate dry run of the v1.42 statistics change.
+ *
+ * v1.42 changes what the discovery engine accepts: trend and season are
+ * removed from both series before correlating, p-values use the effective
+ * sample size, shrinkage and the phrasing tier read that effective size, and
+ * the weather channels are tested once over the averaged day-before-and-same-
+ * day exposure instead of a next-day lag. Every one of those can make an
+ * existing finding disappear. This script answers "how many, and where",
+ * on a copy of production, before the release ships.
+ *
+ * For each account it builds the matrix exactly as the correlations route
+ * does (same assembler, same 180-day window, same module map, same tiered
+ * read) and scans it twice:
+ *
+ * - BEFORE: the pre-v1.42 rules, restated here from the engine's own
+ * exported pieces (lag join at 1 day, Pearson with df = n − 2, BH-FDR,
+ * shrinkage and tier by n), over the raw daily weather values;
+ * - AFTER: the engine as it now ships.
+ *
+ * It prints counts only. No correlation, p-value, metric value or account
+ * identifier leaves the database: accounts are listed by a short hash of
+ * their id, channels by their stable key family. Nothing is written — the
+ * pattern store is not synced, no cache is touched.
+ *
+ * Run (inside the app container, which has DATABASE_URL):
+ * pnpm dlx tsx scripts/compare-correlation-findings.ts [--limit N]
+ */
+import "dotenv/config";
+
+import { createHash } from "node:crypto";
+
+import { prisma } from "@/lib/db";
+import { resolveModuleMap } from "@/lib/modules/gate";
+import { assembleDiscoveryMatrix } from "@/lib/insights/discovery-matrix";
+import { MIN_PAIRED_N, pearson } from "@/lib/insights/correlations";
+import {
+ benjaminiHochberg,
+ confidenceTier,
+ discoverCorrelations,
+ discoverEmergingCorrelations,
+ EARLY_FDR_Q,
+ EARLY_MIN_PAIRS,
+ EARLY_WINDOW_DAYS,
+ FDR_Q,
+ filterSeriesToWindow,
+ lagJoin,
+ metricFamily,
+ shrinkEstimate,
+ type DailySeriesPoint,
+ type NamedSeries,
+} from "@/lib/insights/correlation-discovery";
+import { ENVIRONMENT_FIELDS } from "@/lib/environment/fields";
+import {
+ isSurfaceVisible,
+ correlationChannelSurfaceId,
+} from "@/lib/modules/surface";
+import { wallClockInTz } from "@/lib/tz/wall-clock";
+import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
+
+const MS_PER_DAY = 86_400_000;
+/** The correlations route's window. */
+const WINDOW_DAYS = 180;
+
+interface Finding {
+ behaviour: string;
+ outcome: string;
+}
+
+interface LegacyFinding extends Finding {
+ tier: ReturnType;
+}
+
+/** The pre-v1.42 discovery rules, restated from the engine's parts. */
+function legacyDiscover(
+ series: NamedSeries[],
+ opts: { minPairs: number; fdrQ: number },
+): { found: LegacyFinding[]; pairsTested: number } {
+ const behaviours = series.filter((s) => s.role === "behaviour");
+ const outcomes = series.filter((s) => s.role === "outcome");
+ const tested: Array = [];
+ for (const b of behaviours) {
+ for (const o of outcomes) {
+ if (metricFamily(b.key) === metricFamily(o.key)) continue;
+ const { xs, ys } = lagJoin(b.points, o.points, 1);
+ if (xs.length < opts.minPairs) continue;
+ const result = pearson({ xs, ys, minPairs: opts.minPairs });
+ if (result.status !== "ok") continue;
+ tested.push({
+ behaviour: b.key,
+ outcome: o.key,
+ n: result.n,
+ r: result.r,
+ pValue: result.pValue,
+ });
+ }
+ }
+ const q = benjaminiHochberg(tested.map((t) => t.pValue));
+ const found = tested
+ .filter((t, i) => t.pValue < 0.05 && q[i] <= opts.fdrQ)
+ .map((t) => ({
+ behaviour: t.behaviour,
+ outcome: t.outcome,
+ tier: confidenceTier(shrinkEstimate(t.r, t.n), t.n),
+ }))
+ .filter((t) => t.tier !== null);
+ return { found, pairsTested: tested.length };
+}
+
+/** The pre-v1.42 emerging pass: same rules, trailing window, no faint tier. */
+function legacyEmerging(
+ series: NamedSeries[],
+ retrospective: Finding[],
+ recentFromDayKey: string,
+): Finding[] {
+ const recent = legacyDiscover(
+ filterSeriesToWindow(series, recentFromDayKey),
+ { minPairs: EARLY_MIN_PAIRS, fdrQ: EARLY_FDR_Q },
+ );
+ const established = new Set(retrospective.map(key));
+ return recent.found
+ .filter((f) => f.tier !== "faint")
+ .filter((f) => !established.has(key(f)));
+}
+
+function key(f: Finding): string {
+ return `${f.behaviour}␟${f.outcome}`;
+}
+
+function tzDayKey(at: Date, tz: string): string {
+ const { year, month, day } = wallClockInTz(at, tz);
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
+}
+
+/**
+ * The weather channels as the engine read them before v1.42: one raw daily
+ * value per field, no averaging. Read here directly because the shipped
+ * series now carries the averaged window.
+ */
+async function legacyEnvironmentSeries(
+ userId: string,
+ since: Date,
+): Promise