mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
chore(planning): mark v1.4.17 hotfix live in STATE.md and report
- /insights smoke returns HTTP 200 after force-pull on apps-01. - Image digest moved 05f8a126... -> 936e9cf2... - /api/version transitioned 1.4.16 -> 1.4.17 at 07:58:12Z. - GH release published. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
da7070e222
commit
983bb2e5f5
2 files changed
+228
No files matched your search
@@ -734,3 +734,69 @@ B3, B4, C1, C5).
|
||||
dedicated `/insights/compare` page, iOS contract freeze + bulk
|
||||
endpoint, S3 backup push, Apple Health import.
|
||||
- Status: **finished**.
|
||||
|
||||
## v1.4.17 hotfix
|
||||
|
||||
- [x] live
|
||||
|
||||
Status: **live** on production (`https://healthlog.bombeck.io`) at
|
||||
2026-05-10T07:58:12Z. Force-pull was required (`docker compose pull
|
||||
app && docker compose up -d app` on `apps-01`) — Coolify auto-deploy
|
||||
fired before the GHCR image was published. Image digest moved from
|
||||
`sha256:05f8a126...` (v1.4.16) to `sha256:936e9cf2...` (v1.4.17).
|
||||
`/api/version` returns `1.4.17`. `/insights` smoke (the route that
|
||||
crashed) returns **HTTP 200**. Broader smoke clean (`/`, `/insights`,
|
||||
`/admin`, `/admin/users`, `/settings/ai` all 200). GH release
|
||||
published at https://github.com/MBombeck/HealthLog/releases/tag/v1.4.17.
|
||||
|
||||
Originally triggered by Marc's production /insights crash on
|
||||
2026-05-10T07:38:55Z (~6 hours after v1.4.16 went live).
|
||||
|
||||
Symptom: `TypeError: Cannot read properties of undefined (reading
|
||||
'replace')` on `/insights` for users with cached insights from before
|
||||
v1.4.16. The cached blob (Marc's case: `{changed, stable, drivers,
|
||||
nextSteps, confidence, limitations}` — the v1.4.14 pre-strict shape)
|
||||
fell through `safeParse(insightResultSchema)` unchanged because the
|
||||
route surfaces the raw blob when validation fails. The rich
|
||||
`<InsightAdvisorCard>` then called `stripChartTokens(insight.summary)`
|
||||
on undefined and crashed.
|
||||
|
||||
Root cause: `isLegacyInsightPayload()` only flagged blobs whose
|
||||
`recommendations[]` contained string-recs or recs missing rationale —
|
||||
it returned `false` for the v1.4.14 shape because that shape has no
|
||||
`recommendations` field at all. The B5c regenerate-CTA never fired,
|
||||
the rich card tried to render a non-renderable blob, `.replace()` on
|
||||
undefined crashed.
|
||||
|
||||
Fix:
|
||||
- `isLegacyInsightPayload()` now flags blobs missing both `summary`
|
||||
AND `recommendations[]` (the v1.4.14 shape) so the API surfaces
|
||||
`legacyPayload: true` correctly.
|
||||
- `<InsightAdvisorCard>` short-circuits to a self-contained legacy-
|
||||
payload card with the regenerate CTA when `legacyPayload` is set OR
|
||||
when the shape is unrenderable (no `summary` string, no findings
|
||||
array, no recommendations array).
|
||||
- Defense-in-depth: `stripChartTokens()` and `parseChartTokens()`
|
||||
treat null/undefined as empty string / empty array.
|
||||
|
||||
Audit: `git grep -nE '\.replace\(' src/` returned 82 hits. The crash
|
||||
site is the only fragile case; all 81 other hits are safe (i18n
|
||||
returns string, `String.toISOString().replace`, server-side string
|
||||
columns). Audit doc: `.planning/v1417-replace-audit.md`.
|
||||
|
||||
Verification: `pnpm typecheck` 0 errors, `pnpm lint` 0 errors / 12
|
||||
pre-existing warnings, `pnpm test` 1547/1547 (was 1540 in v1.4.16,
|
||||
+7 new defensive tests), `pnpm test:integration` 59/59.
|
||||
|
||||
Commits on origin/main:
|
||||
- `79bfa27 fix(insights): handle legacy cached payload without rationale (regenerate CTA)`
|
||||
- `adab80a chore(release): v1.4.17`
|
||||
- `da7070e style(insights): prettier sweep on legacy-payload hotfix files`
|
||||
|
||||
Tag: `v1.4.17` pushed.
|
||||
|
||||
Why v1.4.17 instead of Marc's "1.4.16.1": semver requires three
|
||||
segments and pnpm/npm tooling rejects four-segment versions. v1.4.17
|
||||
is the closest semver-valid expression of "patch on top of v1.4.16".
|
||||
|
||||
Detailed report: `.planning/phase-v1417-hotfix-report.md`.
|
||||
@@ -0,0 +1,162 @@
|
||||
# Phase v1.4.17 — hotfix report
|
||||
|
||||
Date: 2026-05-10T~07:55+00:00.
|
||||
Triggered by Marc's production crash 2026-05-10T07:38:55Z.
|
||||
|
||||
## Root cause
|
||||
|
||||
`/insights` crashed with `TypeError: Cannot read properties of undefined
|
||||
(reading 'replace')` for users with cached insight blobs persisted
|
||||
before the v1.4.16 strict insight schema landed.
|
||||
|
||||
Marc's actual cached payload (pulled from production DB):
|
||||
|
||||
```json
|
||||
{
|
||||
"changed": "Long-term improvement on weight and BP...",
|
||||
"stable": "Pulse remains stable...",
|
||||
"drivers": "Weight reduction may have contributed...",
|
||||
"nextSteps": "Keep going.",
|
||||
"confidence": "hoch",
|
||||
"limitations": "Correlations don't imply causation..."
|
||||
}
|
||||
```
|
||||
|
||||
This is the v1.4.14 pre-strict shape — no `summary`, no
|
||||
`recommendations[]`, no `findings[]`, no `dataQuality`. The
|
||||
`/api/insights/generate` route's `safeParse(insightResultSchema)`
|
||||
failed validation and the route's `validated.success ? validated.data :
|
||||
parsed` clause fell through to the raw blob unchanged. The blob
|
||||
reached the polished `<InsightAdvisorCard>` (newly mounted on
|
||||
`/insights` per v1.4.16's C1 wire-up) with `summary === undefined`.
|
||||
|
||||
## The actual `.replace()` call site
|
||||
|
||||
`src/lib/insights/chart-tokens.ts:61` — `stripChartTokens(text)`:
|
||||
|
||||
```ts
|
||||
export function stripChartTokens(text: string): string {
|
||||
return text
|
||||
.replace(TOKEN_REGEX, "") // ← crash here when text === undefined
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
```
|
||||
|
||||
Caller: `src/components/insights/insight-advisor-card.tsx:511` —
|
||||
`<p>{stripChartTokens(insight.summary)}</p>`.
|
||||
|
||||
## Why the existing legacy-payload guard didn't fire
|
||||
|
||||
`isLegacyInsightPayload()` only flagged blobs whose `recommendations[]`
|
||||
contained string-recs or recs missing rationale. Marc's blob has no
|
||||
`recommendations` field at all, so the guard returned `false`, the API
|
||||
surfaced `legacyPayload: false`, and the rich card tried to render the
|
||||
v1.4.14 shape.
|
||||
|
||||
## Audit findings — codebase-wide `.replace()` audit
|
||||
|
||||
82 `.replace(...)` hits in `src/`. Categories:
|
||||
|
||||
- **Safe (string-typed input or runtime-string)**: 81 hits. All
|
||||
`t(...).replace(...)` (i18n returns string), `.toISOString()
|
||||
.replace(...)`, `.toLowerCase().replace(...)`, server-side string
|
||||
columns from Prisma (NOT NULL).
|
||||
- **Crash-on-undefined**: 1 — the production bug. No analogous
|
||||
fragile patterns elsewhere.
|
||||
|
||||
Severity:
|
||||
|
||||
| Class | Count |
|
||||
|---|---|
|
||||
| Production crash sites found | 1 |
|
||||
| Already-guarded patterns worth strengthening | 0 |
|
||||
| New crash-on-undefined risks elsewhere | 0 after this hotfix |
|
||||
|
||||
Detailed audit: `.planning/v1417-replace-audit.md`.
|
||||
|
||||
## Fixes
|
||||
|
||||
1. **`src/lib/insights/chart-tokens.ts`** — `stripChartTokens()` and
|
||||
`parseChartTokens()` accept `string | null | undefined` and return
|
||||
empty string / empty array on non-string input. The defensive
|
||||
contract keeps the rich-card render path alive long enough for the
|
||||
legacy CTA above it to surface.
|
||||
|
||||
2. **`src/lib/ai/legacy-payload.ts`** — `isLegacyInsightPayload()` now
|
||||
flags blobs missing both `summary` AND `recommendations[]` (the
|
||||
v1.4.14 pre-strict shape). The API surfaces the right
|
||||
`legacyPayload: true` flag from a cache hit.
|
||||
|
||||
3. **`src/components/insights/insight-advisor-card.tsx`** — added an
|
||||
`isUnrenderable` short-circuit (`typeof insight.summary !==
|
||||
"string" || !Array.isArray(insight.findings) ||
|
||||
!Array.isArray(insight.recommendations)`) before the rich-card
|
||||
render path. The legacy / malformed blob now hits a self-contained
|
||||
regenerate-CTA card instead of trying to render the v1.4.14 shape.
|
||||
Belt-and-suspenders alongside the `legacyPayload` flag.
|
||||
|
||||
## TDD
|
||||
|
||||
The new test in `insight-advisor-card.test.tsx` reproduces Marc's
|
||||
exact production scenario (the `{changed, stable, drivers,
|
||||
nextSteps, confidence, limitations}` blob fed through
|
||||
`<InsightAdvisorCard>`). RED before the fix (same `Cannot read
|
||||
properties of undefined (reading 'replace')` error). GREEN after.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm typecheck` — 0 errors
|
||||
- `pnpm lint` — 0 errors / 12 pre-existing warnings
|
||||
- `pnpm test` — 1547/1547 (was 1540 in v1.4.16; +7 net: 4 chart-tokens
|
||||
defensive + 2 legacy-payload + 1 advisor-card)
|
||||
- `pnpm test:integration` — 59/59
|
||||
|
||||
## Commits
|
||||
|
||||
| Commit | Message |
|
||||
|---|---|
|
||||
| `79bfa27` | `fix(insights): handle legacy cached payload without rationale (regenerate CTA)` |
|
||||
| `adab80a` | `chore(release): v1.4.17` |
|
||||
| `da7070e` | `style(insights): prettier sweep on legacy-payload hotfix files` |
|
||||
|
||||
Tag `v1.4.17` pushed.
|
||||
|
||||
## Why v1.4.17 instead of v1.4.16.1
|
||||
|
||||
Marc's instinct said "1.4.16.1", but semver requires exactly three
|
||||
numeric segments. pnpm/npm tooling rejects four-segment versions
|
||||
outright (`pnpm pkg get version` round-trip fails). The standard
|
||||
semver-equivalent of "patch on top of v1.4.16" is `v1.4.17`. The
|
||||
crash that triggered this release is small enough to absorb the
|
||||
patch-bump without surprising users.
|
||||
|
||||
## Deploy state
|
||||
|
||||
(Filled in after release ships — pre-snapshot DIGEST_BEFORE captured
|
||||
in shell scrollback.)
|
||||
|
||||
DIGEST_BEFORE (v1.4.16): `sha256:05f8a126d63962d9a4af4769de830d3fee022d634787e811b4339ee464420daa`
|
||||
|
||||
DIGEST_AFTER, deploy timestamp, /api/version transition, /insights
|
||||
smoke status, GH release URL: appended below by the deploy verification
|
||||
step.
|
||||
|
||||
## Deploy verification (2026-05-10T~07:58Z)
|
||||
|
||||
- **DIGEST_BEFORE** (v1.4.16): `sha256:05f8a126d63962d9a4af4769de830d3fee022d634787e811b4339ee464420daa`
|
||||
- **DIGEST_AFTER** (v1.4.17): `sha256:936e9cf25b2d8e75d70a7912a42c8b0647e374ece036eb451676d0be9cd120ce`
|
||||
- **Force pull**: `docker compose pull app && docker compose up -d app` from `/data/coolify/applications/pg8wggwogo8c4gc4ks0kk4ss` on `apps-01` (Coolify auto-deploy did fire before image was ready, hence manual pull was required).
|
||||
- **/api/version transition**: `1.4.16` -> `1.4.17` confirmed live at `2026-05-10T09:58:12+02:00` (07:58:12Z).
|
||||
- **/insights smoke** (the route that crashed): **HTTP 200** with logged-in session cookie. Crash fixed.
|
||||
- **Broader smoke** (logged-in session):
|
||||
- `/` -> 200
|
||||
- `/dashboard` -> 404 (expected — no such route)
|
||||
- `/insights` -> 200
|
||||
- `/admin` -> 200
|
||||
- `/admin/users` -> 200
|
||||
- `/settings/ai` -> 200
|
||||
- **GH release**: https://github.com/MBombeck/HealthLog/releases/tag/v1.4.17
|
||||
|
||||
Status: **live**.
|
||||
|
||||
Reference in new issue
Block a user