mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
chore(release): v1.28.30 — the daily briefing stops silently skipping days
The recurring no-briefing day had a chain of causes, closed end to end: Every failure path in the nightly pregenerate + the comprehensive generator now annotates a queryable cause (several classes — features-error, payload-too-large, aborted, generator-failed — previously bumped a counter with zero trace). A failed nightly warm enqueues one bounded retry ~45 minutes later (singleton per user, existing budgets apply), so a transient provider hiccup heals before morning. A cached payload that carries NO briefing no longer satisfies the 24h POST short-circuit nor the unchanged-content hash gate — a once-stripped briefing could previously block its own regeneration indefinitely. The card surfaces the omission reason persistently (grounding strip, generation failure, not attempted) with a forcing retry. Documentation corrected against code truth: Withings credentials are per-user (the documented env path never existed; the dead example entries and manifest rows are gone, and WITHINGS_WEBHOOK_SECRET is now actually whitelisted in compose — stock deploys could never deliver it); the classic Fitbit portal no longer accepts new registrations; the backup-restore example no longer mints a fresh encryption key and uses pnpm dlx tsx; the TLS guide describes the real client model (two CA-level SPKI pins, any-match, leaf renewals need no build); scaling/admin references fixed; the operator-shared subscription provider is documented.
This commit is contained in:
1 parent
14a517387e
commit
a4feabec81
26 files changed
+1190
-148
No files matched your search
@@ -33,7 +33,9 @@ HealthLog supports two key-provisioning modes that coexist:
|
||||
`AppSettings.adminAiKeyEncrypted`. Users who have not configured
|
||||
their own key fall back to the admin key. This is the last entry
|
||||
in the default provider chain so it acts as a safety net rather
|
||||
than the default route.
|
||||
than the default route. Since v1.28.14 the operator can also share
|
||||
a subscription-based account that users opt into — see
|
||||
[Admin-shared subscription access](#admin-shared-subscription-access).
|
||||
|
||||
Single-user instances usually pick one mode and stop there.
|
||||
Multi-user instances tend to run BYOK with the admin-shared key as a
|
||||
@@ -209,6 +211,41 @@ documented in `docs/codex-protocol-spec.md` for anyone who wants to
|
||||
audit the request shape; users who just want the integration to
|
||||
work do not need to read it.
|
||||
|
||||
## Admin-shared subscription access
|
||||
|
||||
Besides the shared API key, the operator can connect a single
|
||||
subscription-based AI account for the whole instance — an
|
||||
operator-shared subscription provider that users without any
|
||||
personal AI setup can opt into.
|
||||
|
||||
- **Admin side.** Under **Admin → AI** the operator runs the same
|
||||
device-code OAuth flow as the per-user connection
|
||||
(`POST /api/admin/central-codex/device-start` +
|
||||
`device-poll`). The tokens land AES-256-GCM-encrypted in the
|
||||
`AppSettings` singleton and are never returned by any endpoint —
|
||||
status and disconnect go through `GET` / `DELETE
|
||||
/api/admin/central-codex`, which sit behind the cookie-only
|
||||
`requireAdmin()` boundary.
|
||||
- **User side.** Each user opts in individually in `/settings/ai`
|
||||
(`PATCH /api/auth/me/use-central-codex`, body
|
||||
`{ "useCentralCodex": boolean }`). The opt-in is off by default.
|
||||
- **Chain semantics.** The shared connection is the internal
|
||||
`admin-codex` chain entry. It is never part of the persisted chain
|
||||
and never offered in the chain editor — it is appended **last** to
|
||||
the resolved chain, and only when the user opted in AND the
|
||||
operator has connected the account
|
||||
(`src/lib/ai/provider-chain.ts`). A hand-crafted persisted entry
|
||||
resolves to nothing.
|
||||
- **Cost.** Generations through the shared connection run under the
|
||||
operator's account, so usage and rate limits land on the operator,
|
||||
not the user.
|
||||
|
||||
The settings UI states this plainly: the shared access is a single
|
||||
signed-in AI account the operator connected for everyone on the
|
||||
server — not the user's own key. Users who want their data to reach
|
||||
only a provider under their own contract should configure a personal
|
||||
provider instead; their own entries always resolve first.
|
||||
|
||||
## Privacy stance
|
||||
|
||||
- **OpenAI** and **Anthropic** see the prompt + structured snapshot
|
||||
|
||||
+20
-17
@@ -5,10 +5,10 @@
|
||||
|
||||
HealthLog reads Fitbit and Pixel Watch data through the **classic Fitbit Web
|
||||
API** (`api.fitbit.com`). The OAuth app is a **Fitbit developer app registered
|
||||
at [dev.fitbit.com](https://dev.fitbit.com/apps/new)** — **not** a Google Cloud
|
||||
at [dev.fitbit.com](https://dev.fitbit.com/)** — **not** a Google Cloud
|
||||
OAuth client. Like WHOOP, credentials are brought per user: each user (or the
|
||||
operator on their behalf) registers their own Fitbit app and pastes the client
|
||||
id/secret into Settings. The internal integration key is `fitbit`.
|
||||
operator on their behalf) supplies their own Fitbit app's client id/secret in
|
||||
Settings. The internal integration key is `fitbit`.
|
||||
|
||||
> **Common mistake:** the field wants a **Fitbit** Client ID (a short numeric
|
||||
> id from dev.fitbit.com), **not** a Google Cloud client id ending in
|
||||
@@ -16,30 +16,33 @@ id/secret into Settings. The internal integration key is `fitbit`.
|
||||
> consent screen with `unauthorized_client — Invalid client_id`, because Fitbit
|
||||
> and Google Cloud are separate client registries.
|
||||
|
||||
> **Heads-up — September 2026 sunset.** Google is retiring the classic Fitbit
|
||||
> Web API in **September 2026** and moving to the Google Health API behind Google
|
||||
> sign-in. That replacement currently requires Google Restricted-scope brand
|
||||
> verification plus an annual third-party CASA security assessment, which does
|
||||
> not fit a self-hosted bring-your-own-credentials model, so the path beyond the
|
||||
> sunset is still being worked out. Until then the setup below is the supported
|
||||
> way to connect Fitbit.
|
||||
> **Heads-up — new registrations are closed.** Fitbit has **discontinued
|
||||
> self-serve app registration**: the form at `dev.fitbit.com/apps/new` is
|
||||
> disabled for new applications, and the classic Fitbit Web API sunsets in
|
||||
> **September 2026**. If you already hold a registered Fitbit app, it keeps
|
||||
> working until the sunset and the setup below still applies. If you are
|
||||
> connecting Fitbit data for the first time, use the
|
||||
> [Google Health integration](google-health.md) instead — Fitbit devices sync
|
||||
> into Google's health stack, which HealthLog reads directly.
|
||||
|
||||
## 1. Register a Fitbit developer app
|
||||
## 1. Use an existing Fitbit developer app
|
||||
|
||||
1. Sign in at <https://dev.fitbit.com/apps/new> and register a new application.
|
||||
2. **OAuth 2.0 Application Type:**
|
||||
Registration of new apps is no longer possible (see the note above). For an
|
||||
app you registered before the shutdown, these are the settings that matter:
|
||||
|
||||
1. **OAuth 2.0 Application Type:**
|
||||
- **Personal** — the app only ever sees the **owner's own** Fitbit account,
|
||||
and gets intraday (per-minute) heart-rate/steps automatically. This is the
|
||||
right choice for a single-person self-host.
|
||||
- **Server** (or **Client**) — needed when the instance connects **other
|
||||
people's** accounts. Intraday/heart-rate access is then granted case by
|
||||
case via Fitbit's Intraday Data Access request form.
|
||||
3. **Callback URL:** `https://your-instance.example.com/api/fitbit/callback`
|
||||
2. **Callback URL:** `https://your-instance.example.com/api/fitbit/callback`
|
||||
— absolute, **HTTPS**, and matching the `redirect_uri` HealthLog sends (see
|
||||
the redirect-URI note below). One entry per environment you connect from.
|
||||
4. **Scopes:** tick the ones HealthLog requests (next section).
|
||||
5. Save. Fitbit issues a **Client ID** (short, numeric) and a **Client Secret**
|
||||
— keep the secret out of any chat log.
|
||||
3. **Scopes:** the ones HealthLog requests (next section).
|
||||
4. The app's settings page shows the **Client ID** (short, numeric) and the
|
||||
**Client Secret** — keep the secret out of any chat log.
|
||||
|
||||
## 2. Scopes
|
||||
|
||||
|
||||
@@ -50,8 +50,10 @@ Click **Connect WHOOP**. The flow:
|
||||
2. The user signs in on WHOOP and grants the requested scopes.
|
||||
3. WHOOP redirects back to `/api/whoop/callback` with an auth code.
|
||||
4. HealthLog exchanges the code for an access + refresh token, stores
|
||||
them encrypted, registers the webhook, and triggers an initial
|
||||
sync of recent history.
|
||||
them encrypted, and triggers an initial sync of recent history.
|
||||
(The webhook is **not** registered per user — it is configured once
|
||||
at app level in the WHOOP developer console; see the env-var
|
||||
section below.)
|
||||
5. Subsequent syncs run on every webhook delivery plus a safety-net
|
||||
cron pull.
|
||||
|
||||
|
||||
@@ -38,20 +38,25 @@ arrive via the sleep series endpoint.
|
||||
needed — `user.metrics` covers weight / BP / heart-list / sleep /
|
||||
SpO₂ / body comp / temperature / VO₂ max; `user.activity` is the
|
||||
workout-equivalent stream for steps / energy / distance / floors.
|
||||
See `src/lib/withings/client.ts:42` (`WITHINGS_OAUTH_SCOPE`).
|
||||
See `WITHINGS_OAUTH_SCOPE` in `src/lib/withings/client.ts`.
|
||||
4. Save the application. Withings will issue a **Client ID** and a
|
||||
**Client Secret** — keep the secret out of any chat log; it grants
|
||||
read access to every linked account's full health history.
|
||||
|
||||
## 2. Wire the credentials into HealthLog
|
||||
## 2. Wire the credentials in Settings → Integrations
|
||||
|
||||
Paste the client ID and secret into `.env`:
|
||||
Withings credentials are stored per user, not in `.env`. Sign in as
|
||||
the target user, open **Settings → Integrations → Withings**, and
|
||||
paste the client ID and secret. They are encrypted AES-256-GCM at
|
||||
rest (`src/lib/withings/credentials.ts`). On a shared instance every
|
||||
user brings their own Withings app credentials; there is no
|
||||
server-level client-id/secret fallback.
|
||||
|
||||
Two server-level env vars remain:
|
||||
|
||||
```env
|
||||
WITHINGS_CLIENT_ID="your-client-id"
|
||||
WITHINGS_CLIENT_SECRET="your-client-secret"
|
||||
WITHINGS_REDIRECT_URI="https://your-instance.example.com/api/withings/callback"
|
||||
WITHINGS_WEBHOOK_SECRET="$(openssl rand -hex 32)"
|
||||
WITHINGS_REDIRECT_URI="https://your-instance.example.com/api/withings/callback"
|
||||
```
|
||||
|
||||
`WITHINGS_WEBHOOK_SECRET` is the path-segment token the webhook
|
||||
@@ -60,10 +65,9 @@ that fits in a URL path segment works, but a 64-character hex string
|
||||
keeps it indistinguishable from random. Restart the `app` container
|
||||
after saving.
|
||||
|
||||
Per-user override is also possible: the Settings UI accepts a
|
||||
client-ID/secret pair scoped to one user, useful if you want a
|
||||
shared instance where every user brings their own Withings app
|
||||
credentials. Server-level env vars cover the default case.
|
||||
`WITHINGS_REDIRECT_URI` is optional — when unset the callback URL
|
||||
defaults to `${NEXT_PUBLIC_APP_URL}/api/withings/callback`. Set it
|
||||
only when the OAuth callback must differ from that default.
|
||||
|
||||
## 3. Set the webhook URL
|
||||
|
||||
@@ -90,9 +94,8 @@ callback. The portal only owns the OAuth callback URL.
|
||||
|
||||
## 4. Link from the Settings page
|
||||
|
||||
With env vars in place and the app restarted, sign in as the target
|
||||
user and open `/settings/integrations/withings`. Click **Connect
|
||||
Withings**. The flow:
|
||||
With the credentials saved in Settings, open
|
||||
`/settings/integrations/withings` and click **Connect Withings**. The flow:
|
||||
|
||||
1. The Connect button redirects to Withings' `oauth2/authorize`.
|
||||
2. The user signs in on Withings and grants the requested scopes.
|
||||
@@ -133,10 +136,10 @@ Override per-user via the Sources section of `/settings/thresholds`.
|
||||
|
||||
### The Connect button errors out
|
||||
|
||||
- **`Withings credentials not configured`** — `WITHINGS_CLIENT_ID`
|
||||
or `WITHINGS_CLIENT_SECRET` is missing from `.env`. Confirm both
|
||||
are set in the container's runtime environment (`docker compose
|
||||
exec app env | grep WITHINGS`).
|
||||
- **`Withings credentials not configured`** — the signed-in user has
|
||||
not saved a client ID + secret under **Settings → Integrations →
|
||||
Withings**. Credentials live per user in the database, not in
|
||||
`.env` — paste the pair from the developer portal and retry.
|
||||
- **`Callback URI mismatch`** — the URL HealthLog sends to Withings
|
||||
must match an entry in the developer-portal app's callback list.
|
||||
Add the public URL exactly, including the `https://` scheme and
|
||||
|
||||
Reference in new issue
Block a user