mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
chore(release): v1.15.17 — large imports, demo chart toggles
This commit is contained in:
1 parent
42fcc22e39
commit
8efdac7c5a
4 files changed
+168
-2
No files matched your search
@@ -2,6 +2,16 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.15.17] — 2026-06-07 — large imports, demo chart toggles
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Large Apple Health exports import correctly.** A request-body size limit in the middleware layer truncated uploads over ~10 MB before they reached the importer, so a multi-megabyte `export.zip` lost its ZIP structure and failed with an "end-of-central-directory" error. The limit is raised so real exports arrive intact. (GitHub #281)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Chart display toggles work in demo mode.** On a demo instance, the comparison-baseline selector and per-chart overlay toggles above charts are no longer blocked — these are harmless display preferences.
|
||||
|
||||
## [1.15.16] — 2026-06-07 — wellness ring glow no longer clipped
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: HealthLog API
|
||||
version: 1.15.16
|
||||
version: 1.15.17
|
||||
description: >-
|
||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "healthlog",
|
||||
"version": "1.15.16",
|
||||
"version": "1.15.17",
|
||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"homepage": "https://healthlog.dev",
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# migrate-demo-edge01.sh
|
||||
|
||||
Copies **exactly one** user — the demo account
|
||||
`usr_demo_cf31025295714ece8d91f5af13afd76d` — from the production apps01
|
||||
Postgres to the **separate** edge01 demo Postgres. No other user's rows are
|
||||
transferred under any circumstance.
|
||||
|
||||
> Review the script first, then run it deliberately. It performs destructive,
|
||||
> demo-scoped deletes on the edge01 demo DB, then loads the apps01 demo slice.
|
||||
> It never writes to apps01 (source is read-only).
|
||||
|
||||
## How it connects
|
||||
|
||||
- **Source (apps01):** SSH alias `apps-01`, container `db-pg8wggwogo8c4gc4ks0kk4ss`
|
||||
(stable), psql `healthlog`/`healthlog`.
|
||||
- **Target (edge01):** SSH alias `edge-01`. The demo Postgres is a separate
|
||||
Coolify resource whose container name is **not hardcoded** — the script
|
||||
resolves it at runtime via `docker ps | grep -iE 'postgres|^db-'` and aborts
|
||||
if it cannot pick a single match (override with `DST_DB_CONTAINER=db-xxxx`).
|
||||
The demo **app** uuid is `ck8cs4osswg8w440gskw08w8`; match its linked
|
||||
Postgres resource, not the docs/landing DB.
|
||||
- **Edge demo user id is resolved at runtime** by `username='demo'` (it may
|
||||
differ from the apps01 id). The wipe phase is scoped to that resolved id; the
|
||||
load then brings the apps01 row in, so afterwards the edge01 demo user
|
||||
carries the apps01 canonical id.
|
||||
|
||||
Each table is transferred with a scoped, server-side `\copy ... TO STDOUT`
|
||||
piped host-to-host into `\copy ... FROM STDIN`, using **explicit column lists
|
||||
on both ends** so secret columns can be NULLed and column order is pinned.
|
||||
|
||||
## What moves (FK-parent-first, 20 tables)
|
||||
|
||||
| # | Table | Scope predicate |
|
||||
|---|---|---|
|
||||
| 1 | `users` | `id = demo` (the one row; secrets NULLed, `role='ADMIN'`) |
|
||||
| 2 | `measurements` | `user_id = demo AND deleted_at IS NULL` (incl. RECOVERY/STRAIN/STRESS_SCORE rows) |
|
||||
| 3 | `cycle_profiles` | `user_id = demo` |
|
||||
| 4 | `menstrual_cycles` | `user_id = demo` |
|
||||
| 5 | `cycle_day_logs` | `user_id = demo` (encrypted cols NULLed) |
|
||||
| 6 | `cycle_symptom_links` | `day_log_id IN (demo's day logs)` |
|
||||
| 7 | `mood_entries` | `user_id = demo AND deleted_at IS NULL` |
|
||||
| 8 | `mood_entry_tag_links` | `mood_entry_id IN (demo's entries)` — global tags only |
|
||||
| 9 | `medications` | `user_id = demo` |
|
||||
| 10 | `medication_schedules` | `medication_id IN (demo's meds)` |
|
||||
| 11 | `medication_intake_events` | `user_id = demo AND deleted_at IS NULL` |
|
||||
| 12 | `medication_dose_changes` | `medication_id IN (demo's meds)` |
|
||||
| 13 | `medication_inventory_items` | `user_id = demo` |
|
||||
| 14 | `medication_inventory_events` | `medication_id IN (demo's meds)` |
|
||||
| 15 | `medication_side_effects` | `user_id = demo` |
|
||||
| 16 | `reminder_phase_configs` | `medication_id IN (demo's meds)` |
|
||||
| 17 | `personal_records` | `user_id = demo` |
|
||||
| 18 | `user_achievements` | `user_id = demo` |
|
||||
| 19 | `consent_receipts` | `user_id = demo` (carries the `ai_full` consent) |
|
||||
| 20 | `audit_logs` | `user_id = demo AND action LIKE 'insights.%'` (plaintext AI assessments) |
|
||||
|
||||
## Secret / credential columns NULLed on the `users` row
|
||||
|
||||
`codex_access_token_encrypted`, `codex_refresh_token_encrypted`,
|
||||
`codex_token_expires_at`, `codex_connected_at` (and `codex_connection_status`
|
||||
reset to `'disconnected'`), `ai_anthropic_key_encrypted`,
|
||||
`ai_local_key_encrypted`, `ai_openai_key_encrypted`,
|
||||
`withings_client_id_encrypted`, `withings_client_secret_encrypted`,
|
||||
`whoop_client_id_encrypted`, `whoop_client_secret_encrypted`,
|
||||
`fitbit_client_id_encrypted`, `fitbit_client_secret_encrypted`,
|
||||
`telegram_bot_token`, `telegram_chat_id`, `mood_log_url_encrypted`,
|
||||
`mood_log_api_key_encrypted`, `mood_log_webhook_secret`,
|
||||
`insurance_number_encrypted`, `insurer_name`, `insurer_ik_number`.
|
||||
|
||||
Forced flags: `telegram_enabled=false`, `mood_log_enabled=false`,
|
||||
`role='ADMIN'`.
|
||||
|
||||
**Kept:** `password_hash` (demo login works), `avatar_bytes` /
|
||||
`avatar_content_type` / `avatar_updated_at` (profile photo), and all
|
||||
display / preference / layout JSON (`thresholds_json`,
|
||||
`dashboard_widgets_json`, `insights_layout_json`, `coach_prefs_json`,
|
||||
`source_priority_json`, `doctor_report_prefs_json`, `notification_prefs`,
|
||||
`healthkit_config_json`, unit prefs, `height_cm`, `gender`, `timezone`,
|
||||
`locale`, `date_of_birth`, `display_name`, onboarding state, research-mode
|
||||
acknowledgement).
|
||||
|
||||
## What is excluded — and why
|
||||
|
||||
- **Auth / device / push** (sessions, passkeys, auth_challenges, api_tokens,
|
||||
refresh_tokens, devices, push_subscriptions, push_attempts,
|
||||
notification_channels, notification_preferences) — per-environment, never
|
||||
portable.
|
||||
- **Integration / OAuth** (withings/whoop/fitbit connections, all
|
||||
`*_oauth_states` / `*_connect_tickets`, telegram scheduled-deletions +
|
||||
reminder messages, clinician_share_links, provider_health) — tied to live
|
||||
credentials.
|
||||
- **Encrypted-at-rest payloads we can't decrypt on edge01:**
|
||||
`insight_narratives` and all `coach_*` (coach_conversations, coach_messages,
|
||||
coach_facts, coach_usage). The edge01 instance does not hold the prod
|
||||
encryption key, so the AES-256-GCM ciphertext would be unreadable. The same
|
||||
reason drives NULLing `cycle_day_logs.sensitive_encrypted` /
|
||||
`notes_encrypted` while still carrying that table's plaintext cycle fields.
|
||||
The plaintext AI assessment text is preserved through the `insights.%`
|
||||
`audit_logs` rows (the text lives in the `details` JSON) instead.
|
||||
- **Transient / regenerable:** idempotency_keys, rate_limits, data_backups,
|
||||
import_jobs, host_metrics, feedback, recommendation_feedback, and all rollup
|
||||
tables (measurement_rollups, mood_entry_rollups,
|
||||
medication_compliance_rollups, strain_trimp_cache, cycle_predictions) — these
|
||||
rebuild on edge01.
|
||||
- **No rows:** workouts / workout_routes / workout_samples (demo has none).
|
||||
- **Instance / global:** app_settings; global catalogues (mood_tags,
|
||||
cycle_symptoms) — these pre-exist on edge01 and the carried link rows
|
||||
reference the global ids only.
|
||||
|
||||
## Encryption note
|
||||
|
||||
The edge01 demo instance is assumed **not** to share the apps01 encryption
|
||||
key. Every column whose value is AES-256-GCM ciphertext under the prod key is
|
||||
either NULLed (secrets on `users`, `cycle_day_logs.*_encrypted`) or skipped
|
||||
entirely (`insight_narratives`, `coach_messages.encryptedContent`). Carrying
|
||||
that ciphertext would produce undecryptable rows that the app's fail-closed
|
||||
loader would reject.
|
||||
|
||||
## role = ADMIN + DEMO_MODE expectations
|
||||
|
||||
The demo user is loaded with `role='ADMIN'` so the admin surfaces are
|
||||
viewable in the demo. This is **only safe because DEMO_MODE must stay ON** on
|
||||
edge01 — the proxy layer (`src/proxy.ts`) blocks every mutating request in
|
||||
demo mode, so an admin demo session can browse admin pages but cannot change
|
||||
anything. If DEMO_MODE were ever off on edge01, this admin demo account would
|
||||
be a write-capable admin. Keep DEMO_MODE enabled.
|
||||
|
||||
## Safety mechanisms built into the script
|
||||
|
||||
- `set -euo pipefail` and `ON_ERROR_STOP=1` on every psql call.
|
||||
- **Pre-flight guard (apps01):** for each carried set, a query that must
|
||||
return `0` foreign-owned rows — aborts on any non-zero. Includes a check
|
||||
that the demo owns **0 custom mood_tags** (so every carried
|
||||
`mood_entry_tag_link` points at a global tag that pre-exists on edge01) plus
|
||||
a defensive "non-global tag" link count.
|
||||
- **Scoped wipe-first (edge01):** FK-child-first deletes, every one scoped to
|
||||
the runtime-resolved edge01 demo id. No unscoped delete anywhere. Refuses to
|
||||
proceed if `username='demo'` on edge01 resolves to the operator id.
|
||||
- **Post-load verification (edge01):** per-table demo row counts; a hard
|
||||
assertion that the operator id `cmlupy4tn000001rpzx1pxvz7` appears in zero
|
||||
rows; a check that user_id-bearing carried tables hold only the demo id; and
|
||||
a confirmation the demo user exists with `role=ADMIN`.
|
||||
|
||||
## Caveats / things to confirm before running
|
||||
|
||||
- **`FOREIGN_HITS` assertion** counts *all* non-demo rows in the carried
|
||||
tables on edge01, not just what this run inserted. On a clean
|
||||
single-demo-user DB it is `0`. If edge01 legitimately hosts other demo-only
|
||||
accounts, treat a non-zero value as a warning to inspect, not an automatic
|
||||
failure (the script prints it as a WARNING, not an abort).
|
||||
- Confirm edge01 psql credentials are `healthlog`/`healthlog` (the script
|
||||
verifies connectivity before any write; adjust `PG_USER` / `PG_DB` if the
|
||||
demo Postgres resource uses different ones).
|
||||
- The script assumes edge01's schema is at the same (or a compatible) Prisma
|
||||
migration level as apps01. A column present on apps01 but missing on edge01
|
||||
fails the `\copy` loudly rather than silently corrupting data — run
|
||||
migrations on edge01 first if the two have drifted.
|
||||
Reference in new issue
Block a user