diff --git a/.planning/round-v1434-closure-report.md b/.planning/round-v1434-closure-report.md new file mode 100644 index 000000000..90ca9b710 --- /dev/null +++ b/.planning/round-v1434-closure-report.md @@ -0,0 +1,480 @@ +--- +file: .planning/round-v1434-closure-report.md +purpose: v1.4.34 release closure — apple-health import + reliability + web freeze +created: 2026-05-17 +tag: v1.4.34 +--- + +# v1.4.34 — release closure + +Shipped 2026-05-17. The final functional web release before the iOS +native client lands. Headline work: a streaming Apple Health +`export.zip` importer, a server-side aggregation cache that collapses +the three hottest dashboard reads onto in-process LRU slots with +single-flight coalescing and per-user invalidation, a broader +compliance classifier with a dedicated `early` bucket, the Coach +launch surface hoisted onto every authed page with a dashboard-hero +CTA, a trimmer Settings shelf that folds Sources into Targets, two +e2e flake-window tightenings, and the web-freeze marker pinned across +CHANGELOG + Prisma schema head comment + the v1.5 strategic plan +decision log. + +## Outcome + +- GitHub Release: + . +- Tag `v1.4.34` → + `affede31a408974d016f4f6b47a9cb0cd703d8d0` on `main` + (PR #179 squash, merged at 2026-05-16T19:25:26Z). +- Sister-repos: + - `healthlog-docs@5391448` — image-pin bump in + `src/content/docs/self-hosting/updates.mdx` from `:1.4.33` + to `:1.4.34`. + - `healthlog-landing@33e6039` — already on the expected HEAD + from the v1.4.33 AASA-rollout round; no drift, no commit + needed. +- GHCR build conclusion: SUCCESS — single multi-arch + (linux/amd64 + linux/arm64) build on tag push + (`25970776818`), 4 m 47 s linux/amd64 + 4 m 16 s linux/arm64, + manifest-merge step green; image digest + `sha256:3f268a9e390cfccb22795bb1755c8786ea8cb276573f02744ee58954722b3166`. + IW-A's `docker-publish.yml` workflow gate held — exactly one + build fired per tag (vs the double-build pattern noted in + the v1.4.33 iceberg). +- Coolify auto-deploy: **fallback used on both hosts.** + - apps-01: Coolify-API redeploy (MCP `deploy --force`) + queued + finished at 2026-05-16T19:32:36Z but did not pull + the new image because the compose file pins `:latest` and + the local `:latest` was stale. Host-side retag fallback + applied (`docker pull :1.4.34` + `docker tag :latest` + + `docker compose up -d --force-recreate app`). + - edge-01: Coolify MCP still unreachable (matches v1.4.33 + iceberg note). SSH host-side compose-pin bump applied: + `sed -i 's|:1.4.33|:1.4.34|g' docker-compose.yaml` + + `docker pull` + `docker compose up -d --force-recreate`. + - Pre-deploy compose backups created at + `docker-compose.yaml.pre-v1434.bak` on both hosts. + +## Commits on develop since v1.4.33 closure + +Forty-two commits between `b8f6b13a` (v1.4.33 release commit on +develop) and the v1.4.34 release tag, including the trailing +v1.4.33-finalisation tail (AASA + planning), the post-merge +reconciliation merge commit, and the v1.4.34 work proper. The +v1.4.34 work proper (after `72a72266 docs(planning): v1.4.33 +closure`) groups as: + +### IW-A — Infrastructure carryovers + +- `8e05f922` feat(http): centralise authed Cache-Control with + bfcache-friendly preset +- `42432832` chore(build): silence NFT-trace warnings and ship + the bfcache header +- `24b8d52f` ci(docker): fire the release build once per tag +- `5daa78da` docs(planning): document apps-01 env-var duplicate + sections +- `aac2ab68` docs(planning): v1.4.34 IW-A close-out report + +### IW-XML — Apple Health `export.zip` import + +- `2a3fb0e9` chore(deps): add sax for streaming Apple Health + XML import +- `3d309bb9` feat(import): add ImportJob model for Apple Health + export ingest +- `3e2d2c46` feat(import): streaming Apple Health export.xml + parser + mapper +- `9c227894` feat(jobs): apple-health-import worker queue +- `73cd44e0` feat(import): Apple Health export.zip endpoints + + multipart streamer +- `c6528b93` test(import): cover Apple Health export.zip parser + + endpoints +- `cc3aae97` docs(changelog): note the Apple Health + `export.zip` import for v1.4.34 +- `1413bc39` docs(planning): IW-XML close-out report + +### IW-B — Dashboard + Coach + Cache-Control + +- `9a8a9e9a` feat(analytics): expose per-type lastSeenByType + + bfcache-friendly Cache-Control +- `721e88df` feat(i18n): add stale-hint week/month plurals and + dashboard coach CTA +- `0729c202` feat(dashboard): hoist Coach launch provider and + add hero CTA +- `4d404542` docs(planning): v1.4.34 IW-B close-out report + +### IW-C — Compliance classifier + +- `36d5398c` fix(compliance): widen grace window and add + `early` bucket +- `4984bce5` fix(compliance): route `early` intakes through + the compliant bucket +- `30c8555c` refactor(charts): drop the v1.4.33 heatmap + classifier-bug fallback + +### IW-D — Settings + Insights UX + +- `fd81d3b8` refactor(settings): merge Sources into Thresholds + as "Targets & Sources" +- `bcca7664` refactor(insights): collapse five vital pills under + a Vitals parent +- `43502185` i18n: add Vitals parent + Targets & Sources keys + across six locales + +### IW-F-Perf — Consumer collapse + +- `ca7ca0e7` feat(queries): shared achievements query hook +- `3b77b6a0` refactor(gamification): collapse achievement + consumers onto shared hook +- `c6345b71` test(queries): pin achievements hook collapse + + audit analytics consumers +- `6529073c` docs(planning): IW-F-Perf close-out report + +### IW-G — Server-cache primitive + 3 hot routes + 13 invalidation hooks + +- `a5dd8e1d` feat(cache): server-side LRU primitive + per-user + invalidation helpers (also carries the IW-E web-freeze marker + edits, see §"Pool-bump hot-fix + observability" below) +- `68c4bb73` perf(api): wire 3 hot read routes through the + server cache +- `757387ca` feat(cache): wire 13 write endpoints to per-user + invalidation helpers +- `bcdab48e` test(cache): integration coverage + CHANGELOG entry + for server cache + +### IW-E — Web-Freeze marker + e2e flake fixes + +- `fef24a89` test(e2e): stabilise onboarding-flicker + + mobile-viewport probes +- `13b451a5` docs(planning): v1.4.34 IW-E close-out report +- (web-freeze edits on `CHANGELOG.md` + `prisma/schema.prisma` + + `.planning/v15-strategic-plan.md` rode along with `a5dd8e1d` + through a concurrent-worker staging race — content is correct, + attribution is the IW-G commit) + +### Release commits + +- `72b2506a` docs(changelog): clean v1.4.34 entry +- `1ae4adfd` chore(release): v1.4.34 +- `fd1a4366` Merge remote-tracking branch 'origin/main' into + develop (post-v1.4.33 reconcile) + +## Verification + +All checks performed at 2026-05-17T00:30 UTC after the host-side +retag fallback completed on both hosts. + +### apps-01 (`healthlog.bombeck.io`) + +``` +$ curl -s https://healthlog.bombeck.io/api/version +{"data":{"version":"1.4.34","buildSha":null,"builtAt":null,"license":"AGPL-3.0", +"repository":"https://github.com/MBombeck/HealthLog", +"changelog":"https://github.com/MBombeck/HealthLog/blob/main/CHANGELOG.md", +"docs":"https://docs.healthlog.dev","offlineGeoEnabled":false},"error":null} + +$ curl -sI https://healthlog.bombeck.io/privacy +HTTP/2 200 + +$ curl -s https://healthlog.bombeck.io/.well-known/apple-app-site-association +{"applinks":{"apps":[],"details":[]},"webcredentials":{"apps":["S8WDX4W5KX.dev.healthlog.app"]}} +``` + +### edge-01 (`demo.healthlog.dev`) + +``` +$ curl -s https://demo.healthlog.dev/api/version +{"data":{"version":"1.4.34","buildSha":null,"builtAt":null,"license":"AGPL-3.0", +"repository":"https://github.com/MBombeck/HealthLog", +"changelog":"https://github.com/MBombeck/HealthLog/blob/main/CHANGELOG.md", +"docs":"https://docs.healthlog.dev","offlineGeoEnabled":false},"error":null} + +$ curl -sI https://demo.healthlog.dev/privacy +HTTP/2 200 + +$ curl -s https://demo.healthlog.dev/.well-known/apple-app-site-association +{"applinks":{"apps":[],"details":[]},"webcredentials":{"apps":["S8WDX4W5KX.dev.healthlog.app"]}} +``` + +### Landing (`healthlog.dev`) + +``` +$ curl -sI https://healthlog.dev/privacy +HTTP/2 200 +$ curl -sI https://healthlog.dev/support +HTTP/2 200 +$ curl -s https://healthlog.dev/.well-known/apple-app-site-association +{"applinks":{"apps":[],"details":[]},"webcredentials":{"apps":["S8WDX4W5KX.dev.healthlog.app"]}} +``` + +### Apple CDN re-probe + +- `app-site-association.cdn-apple.com/a/v1/healthlog.dev` → + 200, matches origin byte-for-byte. +- `app-site-association.cdn-apple.com/a/v1/demo.healthlog.dev` + → 200, matches origin byte-for-byte. +- `app-site-association.cdn-apple.com/a/v1/healthlog.bombeck.io` + → 200, matches origin byte-for-byte. **Ingested since + v1.4.33** (was 404 in the v1.4.33 closure-report; Apple's + CDN walked the new origin after the first authenticated + fetch). + +### CI on PR #179 + +- 6/7 green: Build linux/amd64, Build linux/arm64, integration, + Lint/Typecheck/Test, dependency audit, secret scanning. +- e2e: 3 pre-existing v1.4.32-era flakes (onboarding-flicker + desktop + mobile, mobile-viewport touch-targets) — IW-E's + tightening landed on the spec selectors but the CI run still + caught the same three windows. Accepted ship-with-flakes per + the maintainer's standing decision. + +## Pool-bump hot-fix + observability + +### apps-01 `DATABASE_URL` pool-bump + +Mid-session env-only change (no code, no release) per +`.planning/round-v1434-prod-slowness-investigation.md` §2: +`DATABASE_URL` was edited on apps-01 to append +`?connection_limit=20&pool_timeout=10` so the prisma + pg-boss +pools share a deterministic ceiling instead of starving each +other under load. Followed by a Coolify redeploy to pick up the +new env. **edge-01 still has the bare `DATABASE_URL`** because +the edge-01 Coolify MCP daemon was unreachable during the +session — operator action pending the MCP daemon restart. + +Once edge-01 is reachable, the same edit applies: +- env var: `DATABASE_URL` +- append: `?connection_limit=20&pool_timeout=10` +- redeploy (or container restart) to flush the prisma + pg-boss + pool instances and pick up the new connection-string. + +### Server-cache observability + +`ServerCache` from IW-G ships an `annotate` hook that lands +two keys on every wide-event for a cached request: + +- `cache..outcome` ∈ `{ hit | miss | stampede }` +- `cache..key_hash` — non-reversible djb2 32-bit hash of + the cache key (`userId|scope`) + +Prod log grep recipes for hit-ratio: + +``` +# All analytics outcomes in the last hour +... | grep -E 'cache\.analytics\.outcome' | jq -r '.meta."cache.analytics.outcome"' | sort | uniq -c + +# Achievements + intake + analytics hit-ratio in one pass +... | grep -E 'cache\.(analytics|achievements|medicationsIntake)\.outcome' | jq -r '"\(.meta | to_entries | map(select(.key | endswith(".outcome"))) | .[0].key) \(.meta | to_entries | map(select(.key | endswith(".outcome"))) | .[0].value)"' | sort | uniq -c +``` + +Eight registered caches sit behind the registry; capacity caps +land each instance below 500-1000 entries × ~5 KB so worst-case +memory across the eight is ~40 MB — comfortably inside the +512 MB Coolify container budget. Process-local cache only; +multi-instance Coolify deploys would diverge per container +until the v1.5.x Redis migration. + +## Apple Health import perf + +The IW-XML synthetic 10 000-record case stresses the +cumulative-fold path (every row collapses into one +`stats:HKQuantityTypeIdentifierStepCount:YYYY-MM-DD` daily +bucket) and lands at 312 ms wall-clock with a heap-delta ceiling +under 100 MB. A bounded 1 GB synthetic-fixture test was +deliberately deferred — the in-process heap-delta assertion +already exercises the streaming property under test +(per-token SAX, bounded buffers), and the integration-tier +1 GB fixture would require a `testcontainers` Postgres + several +minutes per run. Adding the 1 GB-fixture path to a nightly +perf suite tracks under v1.4.35 backlog. + +## Resource-limits Coolify resize + +Per the v1.4.33 prod-slowness investigation §3 (and noted in +the iceberg), the apps-01 HealthLog application still has +`limits_cpus: "0"` + `limits_memory: "0"` in the Coolify +control-plane config — i.e. no Docker resource limits at all, +so the host's 25-app inventory shares the same kernel scheduler +budget. The recommended UI-action is to bind HealthLog to +`limits_cpus: "2"` + `limits_memory: "1g"` in the Coolify +Application → Limits panel so the container has a deterministic +floor regardless of neighbour load. This is operator-action +only and pending the maintainer's UI access. + +## Carry-overs to v1.4.x / v1.5.x + +### Behind the web-freeze gate + +The web-freeze marker now pins additive-only changes for the +v1.4.x tail. Concrete items still on the runway: + +- **Apple Health import: 1 GB synthetic-fixture perf test** + under the nightly suite (IW-XML defer). +- **Apple Health import: HKClinicalRecord / FHIR ingest** + (IW-XML §13, post v1.5). +- **Apple Health import: HKElectrocardiogram waveforms** + (no HealthLog ECG model yet, deferred indefinitely). +- **Apple Health import: workout routes (`workout-routes/*.gpx`)** + — schema field exists, parser write-back deferred to a + post-v1.4.34 follow-up. +- **`getting-started-checklist.tsx` thick-slice swap** — drop + the dashboard's duplicate slim-call once IW-B's + `lastSeenByType` contract stabilises (IW-F-Perf §3). +- **Per-metric interleaved Settings blocks** — IW-D's + Targets+Sources merger stacks both card bodies vertically; + the deeper "metric-by-metric, threshold on top, source list + below" merge needs a ~400 LoC `` and + cross-namespace metric mapping. Deferred. +- **Coolify env-var duplicate prune** — IW-A's audit pinned + the section-1 / section-2 duplicate pairs on apps-01 (28 + keys); operator-action deletes pending. Edge-01 audit still + blocked on MCP unreachability. +- **GHCR docs `:latest` rotation policy** — IW-A's + workflow-trigger change collapses to one build per release + tag and keeps `:latest` on stable tags only. Confirmed + working this round. + +### v1.5.x scope (after iOS clears review) + +- **Redis-backed shared cache** to replace IW-G's process-local + `ServerCache` once multi-instance deploys arrive. +- **iOS Universal Links** — populate `applinks.details` once the + iOS side opts in to deep-linking; current AASA body has empty + `applinks` arrays. +- **`/.well-known/security.txt`** (RFC 9116) and + **`/.well-known/change-password`** (W3C) — admitted by the + current proxy bypass already. +- **iOS bundle-rename window** — Apple's AASA CDN caches for an + hour; document the implication on the iOS handoff if the + bundle prefix ever rotates. + +## Iceberg notes for the next round + +- **Coolify registry-digest auto-deploy still off.** Both hosts + ignored the GHCR push; apps-01 used MCP API redeploy + host + retag, edge-01 used SSH compose-pin + recreate. The "watch + image registry for new digests" toggle in the Coolify UI is + the load-bearing fix per + `.planning/coolify-auto-deploy-howto.md`; without it the + host-side fallback is the only path. Recipe: + ``` + # apps-01 (compose pins :latest) + ssh apps-01 + docker pull ghcr.io/mbombeck/healthlog:1.4.34 + docker tag ghcr.io/mbombeck/healthlog:1.4.34 \ + ghcr.io/mbombeck/healthlog:latest + cd /data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss + docker compose up -d --force-recreate app + ``` + ``` + # edge-01 (compose pins explicit tag) + ssh edge-01 + sed -i 's|healthlog:1.4.33|healthlog:1.4.34|g' \ + /data/coolify/applications/ck8cs4osswg8w440gskw08w8/docker-compose.yaml + docker pull ghcr.io/mbombeck/healthlog:1.4.34 + cd /data/coolify/applications/ck8cs4osswg8w440gskw08w8 + docker compose up -d --force-recreate + ``` + Pre-deploy compose backups created at + `docker-compose.yaml.pre-v1434.bak` on both hosts. +- **GHCR double-build fixed.** IW-A's + `docker-publish.yml` change collapsed the squash-merge + tag + push double-run to exactly one build per release tag this + round. The `:latest` raw-tag enable rule moved onto the tag + ref so each release refreshes both the semver tags and the + `:latest` alias in the same run. Pre-release tags + (`-rc.1`, `-beta.2`) stay off `:latest`. +- **edge-01 Coolify MCP unreachable.** The MCP integration for + edge-01 (`46.225.114.153:8000`) returned a connection error + again during the deploy pipeline; SSH itself worked fine so + the fallback path covered it. **The edge-01 pool-bump cannot + land via MCP** until the daemon is restarted — flagged as the + primary operator-action carry-over. +- **PR-merge conflict on every release.** Develop has the work + as atomic commits; main is updated by squash-merge so the + squash commit's tree diverges from develop's + atomic-commit-tree on the same files. Every release surfaces + ~20 add/add conflicts on the v1.4.34-touched paths — every + one resolves cleanly with `--ours` on develop and a + no-edit merge commit. Worth pinning in the release runbook + so the next round doesn't burn cycles re-discovering the + shape. +- **e2e flake band stable at 3.** IW-E's two-spec tightening + landed on develop but the CI run still caught the same three + windows (`onboarding-flicker` desktop + mobile, + `mobile-viewport`). The fixes are correct in spirit but the + underlying race is a Vercel-dev cold-mount artefact that + doesn't reproduce locally. Ship-with-flakes pattern holds. + +## Drain-script status + +Operator-action pending. `/api/admin/drain-per-sample-cumulative` +was not invoked from this round — same as v1.4.33, it's +cookie-auth gated and the admin cookie is not available +programmatically. The drain remains on the v1.5 iOS-TestFlight +cutover checklist as a manual step against the production host. + +## Blocked items + +None. The entire pipeline cleared the same session: tag pushed, +GHCR built both architectures, both hosts redeployed (via +fallback), GH Release published, sister-repos in sync, drain +script intentionally deferred to operator-action per briefing. + +## Operator-action carry-overs + +Five items waiting on the maintainer: + +1. **edge-01 Coolify MCP daemon restart** so the next release + can reach env vars + force redeploys via MCP instead of + falling through to SSH. +2. **edge-01 `DATABASE_URL` pool-bump** — apply the same + `?connection_limit=20&pool_timeout=10` suffix that landed + on apps-01 this session. +3. **Coolify resource-limits resize on apps-01** — bind + HealthLog to `limits_cpus: "2"` + + `limits_memory: "1g"` in the Coolify Application → Limits + panel. +4. **Coolify registry-digest auto-deploy toggle** — flip the + "watch image registry for new digests" switch in the + Coolify UI for both apps-01 and edge-01 so the GHCR push + produces green auto-deploys without the host-side + fallback. +5. **apps-01 env-var duplicate prune** — IW-A's audit pinned + 28 section-1 / section-2 duplicate pairs; deletes pending. + Highest-priority candidate is UUID + `d3r4k1lryj6n0z7dfj4hhc8t` (placeholder + `POSTGRES_PASSWORD` value `"POSTGRES_PASSWORD is required"`). + +## Final state of the v1.4.34 wins + +- **Apple Health `export.zip` import live.** Both hosts accept + multipart uploads at `POST /api/import/apple-health-export`; + worker queue registered; idempotent re-uploads short-circuit + on content hash; `ImportJob` row state machine surfaces + progress through the status endpoint. +- **Server-side aggregation cache live.** Three hot reads + cached, 13 write endpoints invalidate. First-warm hit ratio + expected to land in the 70-90 % band within the first few + minutes of production traffic (single warm dashboard request + per user, cached for 60 s, invalidates on the user's next + write). `cache..outcome` annotation on the wide-event + is the live signal — grep prod logs to confirm. +- **Compliance classifier widened.** Early intakes count as + compliant in heatmap + perfect-day surfaces; the v1.4.33 + `looksClassifierBug` defensive fallback is retired now that + the underlying root cause is gone. +- **Coach reachable from every authed route.** Drawer + launch + provider hoisted to the auth-shell; dashboard hero CTA wires + to `coachLaunch.askCoach(null)`. +- **Settings shelf trim** — section count 11 → 10 (Sources + folded into Targets); insights tab-strip 14 → 10 (Vitals + parent collapses HRV, Resting HR, Oxygen, Body Temperature, + Active Energy). +- **Bfcache eligibility restored.** Every authed HTML page now + ships `Cache-Control: private, max-age=0, must-revalidate` + (vs the framework's default hard-bfcache-breaker + `no-store, must-revalidate`). +- **Web freeze pinned.** v1.4.34 is the last functional web + release; the marker lives in CHANGELOG + Prisma schema head + comment + `.planning/v15-strategic-plan.md` §5 decision log.