Compare commits

..
6 Commits
Author SHA1 Message Date
Marc-André BombeckandMarc-André Bombeck 643e6ec3a0 release: v1.4.2 — resurrect v1.4.1 work + bug-fix triple (#152)
* Revert "revert: roll source tree back to v1.4.0 to restore production"

This reverts commit e444a90b10.

* fix(v1.4.2): summaries.WEIGHT + medications array guards + healthcheck IPv4

Three production bugs surfaced after the v1.4.0 deploy stabilized:

1. `TypeError: undefined is not an object (evaluating 'E?.summaries.WEIGHT')`
   on /dashboard and /insights for users without weight readings —
   the optional chain stopped at `data` when it needed to also cover
   `data.summaries`. Fixed: `data?.summaries?.WEIGHT` (one level deeper).

2. `TypeError: y?.filter is not a function` on /medications — the
   medications query value was non-array (likely a stale
   service-worker response from an older API shape). Replaced with
   `Array.isArray(medications) ? medications : []` so the page is
   immune to any future shape drift.

3. Healthcheck-induced 503 outage (already in 1.4.1 — included here
   so 1.4.2 is the single coherent release). The docker-compose
   healthcheck used `localhost` which busybox-wget resolves to IPv6
   ::1; Next.js standalone listens on IPv4 0.0.0.0 only. Pinned
   to 127.0.0.1.

This release also includes everything from v1.4.1 (which never
published a clean GHCR tag): the per-section admin extraction, the
testcontainers integration suite, the Playwright + axe-core E2E
foundation, the moodLog SSRF fix, the central error-redaction in
WideEventBuilder + Glitchtip, the medical-citation drift fixes
(ESH 2023, Saint-Maurice attribution, steps source), and the docs
sync.

Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
2026-05-08 17:46:31 +02:00
Marc-André Bombeck 956d29032f fix(deploy): pin to :1.4.0 (the :1.4.1 GHCR tag never published)
The docker-publish workflow's run for the v1.4.1 tag reported success
in GH Actions, but no `1.4.1` tag landed on the ghcr.io registry
(`docker pull ghcr.io/mbombeck/healthlog:1.4.1` returns 'manifest
unknown'). Coolify tries to pull, fails, and never replaces the old
container — so the site stays 503 with no upstream at all.

Repinned to `:1.4.0` which is verifiably present on GHCR (2h old).
Healthcheck fix from the previous commit (127.0.0.1) stays.

The :1.4.1 publish failure is a separate followup — likely related
to the docker-publish workflow not handling the squash-merge release
flow correctly for the tag-on-main scenario. Tracked for v1.4.2.
2026-05-08 17:40:08 +02:00
Marc-André Bombeck 38320360a3 fix(deploy): healthcheck uses 127.0.0.1 + image pinned to :1.4.1
Root cause of today's production outage: the docker-compose
healthcheck used `wget --spider http://localhost:3000/api/version`.
Inside Alpine containers, busybox-wget resolves `localhost` to
IPv6 `::1` first. Next.js standalone (server.js) listens on IPv4
`0.0.0.0:3000` only — `::1` is not bound, so wget gets ECONNREFUSED.
Docker marks the container unhealthy after 3 retries; Traefik takes
the upstream out of rotation; Cloudflare returns 503 "no available
server" because there's no healthy origin behind the route.

Confirmed via SSH on apps-01:
  $ docker exec <container> wget -O- http://localhost:3000/api/version
  Connecting to localhost:3000 ([::1]:3000)
  wget: can't connect to remote host: Connection refused
  $ netstat -tlnp
  tcp  0.0.0.0:3000  LISTEN  1/next-server   (IPv4 only, as expected)

Switching the URL to `http://127.0.0.1:3000/api/version` forces IPv4
and matches the Dockerfile's own HEALTHCHECK directive (which already
used 127.0.0.1). Same fix shape for self-hosters: their `wget` will
do the same thing on Alpine.

This explains why today's deploys looked broken even after rolling
back to v1.4.0 source and even after pinning to :1.3.3. The image
contents were fine; the healthcheck path was always going to fail
on this Alpine + Next.js combo. Why it worked yesterday: the
previous deploy chain happened to skip the new healthcheck override
in the compose file (the Dockerfile-level HEALTHCHECK uses 127.0.0.1
and was authoritative). Today's compose-level override flipped that.

Image pinned back to `:1.4.1` (the multi-arch GHCR build that landed
this afternoon — both `1.4.1` and `1.4.0` are healthy images, the
healthcheck is what was sick).

Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
2026-05-08 17:34:04 +02:00
Marc-André Bombeck a8a32bc718 hotfix: pin docker-compose to GHCR :1.3.3 — rule out runtime, prove infra (#151)
Production at healthlog.bombeck.io has been 503-ing through every
attempted recovery today: Coolify restart, force-rebuild, pin to
:1.4.0, full source-tree rollback to the v1.4.0 commit. Each deploy
finishes clean, the container boots ("Ready in 0ms", pg-boss workers
running), but no HTTP request ever lands inside the app. Other apps
on the same Coolify host also stopped responding to direct-origin
probes (workbalance, birthdaylog, helpdesk, 4goats — all curl 000),
which makes a server-side Traefik / docker-network regression the
most likely culprit, not anything we shipped.

This commit pins to GHCR `:1.3.3` (yesterday's release, verifiably
healthy across self-host installs) and removes the `build:` block so
Coolify pulls the prebuilt multi-arch image instead of attempting
another local rebuild. If this image boots and Traefik routes to
it, the regression is in v1.4.x source. If it 503s identically, the
infra fault on apps-01 is conclusive.

The v1.4.0 / v1.4.1 work is preserved — both the v1.4.0 GHCR image
and the v1.4.1 GHCR image (built earlier today) remain available
for re-pinning once the deployment path is healthy again.
2026-05-08 17:31:52 +02:00
Marc-André Bombeck e444a90b10 revert: roll source tree back to v1.4.0 to restore production
Production at healthlog.bombeck.io has been 503-ing since the v1.4.1
deploys started landing on apps-01 (Coolify). The container boots —
Next.js prints "Ready" and the pg-boss workers run — but never
accepts HTTP on :3000, so the Docker healthcheck fails and Traefik
takes the upstream out of rotation. A manual restart, a Coolify
force-rebuild, and a docker-compose pin to the GHCR :1.4.0 multi-arch
image all failed to bring the site back up — Coolify rebuilds the
image from main HEAD on every deploy regardless of the compose
directives.

This commit resets the working tree to commit 21bd46d (v1.4.0
release). Same content that's been running for self-hosters since
yesterday's tag-and-release. The next Coolify deploy will build
from this tree and produce a healthy container.

The v1.4.1 work is NOT lost:
  - PRs #144, #145, #137, #146, #147, #148, #149, #150 remain in
    git history.
  - Their commits are still tagged (`v1.4.1`), still on the GHCR
    multi-arch image (`ghcr.io/mbombeck/healthlog:1.4.1`), still in
    the GitHub Release notes.
  - Self-hosters who have already pulled the v1.4.1 image keep it.
  - Local development continues from main HEAD with the v1.4.1
    code — the regression only surfaced under the Coolify build
    flow.

Re-applying v1.4.1 to production will need a separate cycle to
reproduce the runtime failure under the Coolify build path. That
work is tracked in docs/ops/v141-followup-issues.md (added back
when the tree is reapplied) and the deploy gating in
.github/workflows/e2e.yml will catch this class of bug going
forward.

No DB migration. No env-var change. No API contract change.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
2026-05-08 17:15:34 +02:00
Marc-André Bombeck e33be0d06e hotfix: pin docker-compose to GHCR :1.4.0 to restore production (#150)
Production at healthlog.bombeck.io is returning 503 from Traefik
("no available server") since the v1.4.1 deploys started landing on
apps-01. The container boots — Next.js prints "Ready" and the
pg-boss background workers run — but never accepts HTTP on :3000,
so the Docker healthcheck (`wget --spider /api/version`) fails and
Traefik takes the upstream out of rotation.

Locally the v1.4.1 source passes typecheck, all 669 unit tests, and
the 10-test integration suite. The runtime regression only surfaces
in the Coolify-built image. Suspected cause: a layer-cache corruption
left over from the failed PR #146 deploy at 14:08 (which OOM-killed
during the builder COPY step), or a build interaction between the
new dev-deps (@playwright/test, @axe-core/playwright, testcontainers)
and Next.js standalone bundling. A `force: true` rebuild via Coolify
did not resolve it, which suggests it's not just stale cache.

This commit removes the `build:` block from the app service and
pins the image to the v1.4.0 GHCR tag — the last release verified
healthy on production. Coolify will pull the multi-arch image and
run it directly. Site comes back up immediately.

The v1.4.1 fixes are NOT lost — the source still ships in main, the
GHCR :1.4.1 image was built successfully by the docker-publish
workflow, and we re-pin once the runtime regression is reproduced
locally and fixed.

Self-hosters who want to keep building from source can add a
docker-compose.override.yml with the `build:` block. The compose
override pattern is documented and stable.

No DB migration. No env-var change.
2026-05-08 17:06:42 +02:00
6 changed files with 44 additions and 12 deletions

No files matched your search

+29
View File
@@ -1,5 +1,34 @@
# Changelog
## [1.4.2] — 2026-05-08
### Fixed — Production deploy hotfixes
- **Dashboard and Insights pages no longer crash for users without
weight data.** `data?.summaries.WEIGHT` only protected the outer
object — the optional chain stopped one level too early, so
brand-new users (where `summaries` is undefined) hit
`TypeError: undefined is not an object (evaluating 'E?.summaries.WEIGHT')`
on first load. Now `data?.summaries?.WEIGHT`.
- **Container healthcheck uses `127.0.0.1` instead of `localhost`.**
busybox-`wget` in Alpine resolves `localhost` to IPv6 `::1` first,
but Next.js standalone listens on IPv4 `0.0.0.0:3000` only — so the
healthcheck always returned ECONNREFUSED, Docker marked the
container unhealthy, and Traefik returned 503 from the public URL
even though the app was actually running. The Dockerfile-level
`HEALTHCHECK` already used 127.0.0.1; the `docker-compose.yml`
override was the one that drifted to `localhost`. Fixed.
### Notes
- The 1.4.1 GHCR image never published cleanly (the docker-publish
workflow reported success in GH Actions but the
`ghcr.io/mbombeck/healthlog:1.4.1` tag returned `manifest unknown`
when Coolify tried to pull). The 1.4.2 release supersedes 1.4.1
and includes everything 1.4.1 was supposed to ship — the v1.4.1
source on `main` was always healthy; only the Coolify deploy
surface was broken.
## [1.4.1] — 2026-05-08
### Security
+7 -5
View File
@@ -4,10 +4,12 @@ services:
# For horizontal scaling switch to the web/worker split below
# (HEALTHLOG_PROCESS_TYPE=web on this service + uncomment app-worker).
app:
image: ghcr.io/mbombeck/healthlog:latest
build:
context: .
dockerfile: Dockerfile
# Pinned to GHCR :1.3.3 multi-arch image (gestern released, audit-tested).
# Production on apps-01 has been 503-ing since today's deploys; rolling back
# to the last release that's verifiably running on self-host installs to
# rule out runtime / Docker-network issues. No `build:` block on purpose:
# we want Coolify to PULL the prebuilt image, not build from source.
image: ghcr.io/mbombeck/healthlog:1.4.0
container_name: healthlog-app
ports:
- "3000:3000"
@@ -39,7 +41,7 @@ services:
test:
[
"CMD-SHELL",
"wget --no-verbose --tries=1 --spider http://localhost:3000/api/version || exit 1",
"wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/version || exit 1",
]
interval: 30s
timeout: 5s
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.4.1",
"version": "1.4.2",
"private": true,
"packageManager": "pnpm@10.31.0",
"scripts": {
+1 -1
View File
@@ -632,7 +632,7 @@ export default function InsightsPage() {
enabled: isAuthenticated,
staleTime: 60 * 1000,
});
const w = analytics?.summaries.WEIGHT;
const w = analytics?.summaries?.WEIGHT;
const sys = analytics?.summaries.BLOOD_PRESSURE_SYS;
const dia = analytics?.summaries.BLOOD_PRESSURE_DIA;
const p = analytics?.summaries.PULSE;
+5 -4
View File
@@ -130,10 +130,11 @@ export default function MedicationsPage() {
const byName = (a: Medication, b: Medication) =>
a.name.localeCompare(b.name, "de", { sensitivity: "base" });
const activeMeds = (medications?.filter((m) => m.active) ?? []).sort(byName);
const inactiveMeds = (medications?.filter((m) => !m.active) ?? []).sort(
byName,
);
// Defensive against stale service-worker responses or any future API
// shape change: only filter when we actually have an array.
const medsArray = Array.isArray(medications) ? medications : [];
const activeMeds = medsArray.filter((m) => m.active).sort(byName);
const inactiveMeds = medsArray.filter((m) => !m.active).sort(byName);
return (
<div className="space-y-5">
+1 -1
View File
@@ -197,7 +197,7 @@ export default function DashboardPage() {
enabled: isAuthenticated,
});
const w = data?.summaries.WEIGHT;
const w = data?.summaries?.WEIGHT;
const sys = data?.summaries.BLOOD_PRESSURE_SYS;
const dia = data?.summaries.BLOOD_PRESSURE_DIA;
const p = data?.summaries.PULSE;