Merge branch 'fix/encrypt-free-text-and-local-ai' into release/v1.39.3

# Conflicts:
#	src/app/api/import/csv/route.ts
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 21:50:05 +02:00
commit 66b1aef40c
69 files changed
+3625 -367

No files matched your search

+21 -5
View File
@@ -3429,7 +3429,13 @@ paths:
"302":
description: Always a redirect. To the provider on success. On failure to `/auth/login?error=<reason>` (browser) or
`healthlog://oidc-callback?error=<reason>` (native), where reason is one of `oidc_disabled`,
`oidc_rate_limited`, `oidc_invalid_request`, `oidc_failed`.
`oidc_rate_limited`, `oidc_invalid_request`, `oidc_failed`. The provider then returns to the callback (not
listed here, because the identity provider drives it rather than a client), which ends the native leg at
`healthlog://oidc-callback` with `code=<hlh_…>`, with `mfa_ticket=<ticket>&methods=<list>`, or with
`error=<reason>`, where reason is one of `oidc_disabled`, `oidc_rate_limited`, `oidc_denied`,
`oidc_no_email`, `oidc_email_unverified`, `oidc_identity_conflict`, `oidc_registration_disabled`,
`oidc_failed`. The reasons are this exact snake_case vocabulary on both legs; a client maps a value it does
not know to a generic sign-in failure.
/api/auth/passkey/register-options:
post:
tags:
@@ -13215,9 +13221,12 @@ paths:
before 1900). Glucose accepts `mmol/L` (converted to canonical `mg/dL`); weight accepts `lb` (converted to
`kg`). `glucoseContext` is optional on a `BLOOD_GLUCOSE` row — a blank cell stores no context, which is the
normal shape of a continuous-sensor export; a non-empty value is still checked against the enum, and the column
is refused on any other type. An `externalId` column makes re-upload idempotent (upsert on `(userId, type,
source=IMPORT, externalId)`); without it a re-upload duplicates. `?dryRun=1` validates + previews without
writing. Shares the 5/hour `import:` rate bucket."
is refused on any other type. Re-uploading a file is idempotent. With an `externalId` column a row upserts on
`(userId, type, source=IMPORT, externalId)`: a changed row is `updated`, an unchanged one is `skipped` with
reason `duplicate` and not written. Without it a row is deduplicated on `(type, measuredAt)` and a repeat is
`skipped` / `duplicate`. Writes commit in chunks rather than as one transaction; a failure part-way leaves
earlier chunks written, and re-uploading the same file completes the rest. `?dryRun=1` validates + previews
without writing. Shares the 5/hour `import:` rate bucket."
parameters:
- name: dryRun
in: query
@@ -36407,6 +36416,11 @@ components:
anyOf:
- type: string
- type: "null"
noteEncrypted:
anyOf:
- type: string
minLength: 1
- type: "null"
createdAt:
type: string
minLength: 1
@@ -37084,6 +37098,9 @@ components:
minLength: 1
title:
type: string
titleEncrypted:
type: string
minLength: 1
documentScoped:
type: boolean
summaryEncrypted:
@@ -37175,7 +37192,6 @@ components:
additionalProperties: {}
required:
- id
- title
- documentScoped
- createdAt
- updatedAt
+38 -19
View File
@@ -140,16 +140,35 @@ Base URL `http://<vllm-host>:8000/v1`, API key `your-shared-secret`,
model `Qwen/Qwen2.5-14B-Instruct`. The local-client encrypts the
API key at rest like every other provider credential.
**SSRF guard.** Local endpoints route through the same validation
as every other outbound URL HealthLog hits. The guard rejects
private-IP-range targets by default. `ALLOW_LOCAL_AI_PRIVATE_HOSTS`
opens the escape hatch and accepts two forms: a comma-separated host
allowlist (e.g. `ALLOW_LOCAL_AI_PRIVATE_HOSTS=ollama.lan,10.0.0.5`),
which permits only those exact hostnames, or the legacy
`ALLOW_LOCAL_AI_PRIVATE_HOSTS=true`, which permits any private host —
including the cloud-metadata endpoint, so prefer the host list. Leave
it unset on public-facing instances unless you specifically run an
internal endpoint.
**Private endpoints.** Any user of the instance can type a base URL,
so HealthLog refuses one on a private address unless the operator has
listed it. List the endpoint's exact origin in `AI_PRIVATE_ORIGINS`,
comma-separated, the same way `NOTIFICATION_PRIVATE_ORIGINS` works for
notification relays:
```bash
AI_PRIVATE_ORIGINS="http://ollama.lan:11434,http://10.0.0.5:4000"
```
An origin is `scheme://host[:port]` and nothing else: no path, no
wildcard, no range. A listed endpoint is called through a pinned
connection that refuses to follow redirects, and the cloud-metadata
address, link-local addresses and the unspecified address stay
unreachable even when listed or when a listed name resolves to one.
`ALLOW_LOCAL_AI_PRIVATE_HOSTS`, the older setting, still works as a
host list (`ollama.lan,10.0.0.5`, any port), with the same pinning.
Its `true` form used to open every private host on the network to
every user. It is deprecated since v1.39.3 and will be removed in a
later release. Until then it still covers the operator's own AI
configurations, the server key in the admin panel and AI settings
saved on an admin account, over the same pinned connection, so the
metadata address and link-local addresses stay unreachable. Any other
account's base URL needs its origin in `AI_PRIVATE_ORIGINS`. The boot
summary names the setting while it is set, and the admin AI settings
page shows the exact `AI_PRIVATE_ORIGINS` line for the endpoints saved
on the instance.
**Model sizing.** Roughly: 7-8B for 8 GB GPU / Apple Silicon base
(briefings fine, deeper Coach reasoning gets terse), 14-24B for
@@ -186,9 +205,9 @@ therefore never be sent to a host you typed into a different form,
and that property is structural rather than a comment — the OPENAI
arm has no code path that reads the gateway's base URL.
**SSRF guard.** The gateway's base URL passes the same floor as the
Local provider's: a public host always, a private one only when the
operator allowlisted it via `ALLOW_LOCAL_AI_PRIVATE_HOSTS` (documented
**Private endpoints.** The gateway's base URL passes the same floor
as the Local provider's: a public host always, a private one only when
the operator listed its origin in `AI_PRIVATE_ORIGINS` (documented
under Local endpoints above). LAN gateways are the normal case for LiteLLM and vLLM, so
the escape hatch matters here as much as it does for Ollama.
@@ -439,12 +458,12 @@ upstream error.
- **"No AI provider configured."** Either set up at least one BYOK
provider in `/settings/ai`, or configure an admin-shared OpenAI
key in the admin panel.
- **Local endpoint rejected as "internal/private host".** Set
`ALLOW_LOCAL_AI_PRIVATE_HOSTS` to the endpoint's exact hostname
(e.g. `ollama.lan`; comma-separate several) in the container
environment, or to `true` to allow any private host. Prefer the host
list; leave it unset on internet-facing instances unless you
specifically want to allow private-range targets.
- **Local endpoint rejected as "internal/private host".** Add the
endpoint's exact origin to `AI_PRIVATE_ORIGINS` in the container
environment (e.g. `http://ollama.lan:11434`; comma-separate several)
and restart. With `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true` an admin
account's endpoint still works, but no other account's does; the
admin AI settings page lists the origins to add.
- **Codex disconnects after a few weeks of inactivity.** ChatGPT
refresh tokens lapse after extended idle periods. Reconnect via
the **Connect ChatGPT** button — HealthLog re-runs the device-code