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>
Builds out the foundation for end-to-end tests against the production
build: Playwright with desktop + mobile projects, axe-core for WCAG
2.1 AA gating, and a GitHub Actions workflow that caches the browser
download and uploads the HTML report on failure.
What lands now (public surfaces only — no DB seed required):
e2e/version.spec.ts /api/version returns shape, no auth
e2e/auth-redirect.spec.ts proxy gate bounces / → /auth/login etc.
e2e/login.spec.ts login form renders + autoComplete hints
+ invalid-credential error surfaces
e2e/locale-switch.spec.ts DE/EN cookie switch, no raw i18n keys
leak into the DOM
e2e/a11y.spec.ts axe-core fails on serious or critical
WCAG 2.1 A/AA violations on /auth/login
The four authenticated flow specs from the v1.4.1 plan
(quick-entry, doctor-report, settings-roundtrip, test-buttons,
onboarding) need a seeded test user; they're tracked as a follow-up
because the seeding pipeline + WebAuthn polyfill they require warrant
their own PR. The smoke specs above cover the proxy, the login surface,
the version healthcheck endpoint, and the locale machinery — i.e.
every path a v1.4.X regression could realistically break without
deeper UAT.
CI workflow (.github/workflows/e2e.yml):
- postgres:16-alpine service container
- prisma migrate deploy seeds an empty schema
- playwright install --with-deps chromium (cached)
- next build, then `playwright test` against `next start`
- playwright-report uploaded as an artifact on failure
Quality gates:
- pnpm typecheck — clean
- pnpm test — 658/658 pass (vitest excludes e2e/**)
- pnpm lint — 0 errors
* test(integration): Postgres testcontainers + 4 critical-path tests
Adds an integration test suite that runs against a real Postgres booted
in-process via testcontainers. Lives under tests/integration/, runs via
`pnpm test:integration`, and is excluded from the default `pnpm test`
loop so the 477-test unit suite stays fast (~1.2s).
Covers four regression-prone behaviours that pure unit tests cannot
validate against a mocked client:
- rate-limit.test.ts — atomic UPSERT under concurrency (5/6 allowed)
and window-reset semantics for src/lib/rate-limit.ts
- idempotency-replay.test.ts — cached replay with X-Idempotent-Replay,
expired-row purge, and the do-not-cache contract for 401/403/408/429/5xx
on top of src/lib/idempotency.ts
- cascade-delete.test.ts — GDPR Art. 17 erasure: user.delete wipes
every onDelete:Cascade table and SetNulls AuditLog/Feedback
- auth-flow.test.ts — createSession/getSession round-trip plus
expired-session purge for src/lib/auth/session.ts
Adds .github/workflows/integration.yml triggering on pull_request and
push to main. Local Docker is not running in this worktree environment;
the suite was verified by collecting all 10 tests via `vitest list` and
will execute in CI.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(integration): rebase onto v1.4 + exclude live agent worktrees from default test scope
Rebase brings PR #137's testcontainers infrastructure on top of the
user-locale migration drift fix landed in #140 — the integration suite
should now bootstrap a fresh Postgres container with all 25 migrations
applied cleanly.
vitest.config.mts also gains a `.claude/worktrees/**` exclude so live
agent worktrees (which mirror `src/` while their parent agents are
running) don't double-execute the project's tests against possibly-stale
copies.
* test(integration): boot one Postgres container per run via vitest globalSetup
The first iteration of the testcontainers suite started/stopped a
fresh container in each test file's beforeAll/afterAll. That works
in isolation but breaks when files share a worker (`pool: forks`,
`fileParallelism: false`, `isolate: false`):
1. Test file A boots container 1, sets DATABASE_URL.
2. The application's Prisma singleton in src/lib/db.ts is built
when test code dynamic-imports @/lib/auth/session — it captures
container 1's URL.
3. afterAll stops container 1.
4. Test file B boots container 2, sets DATABASE_URL to a new port.
5. Test file B imports application code that resolves to the same
singleton — still pointing at the dead container 1. Queries fail
with a generic PrismaClientKnownRequestError.
Fix: move the container lifecycle to vitest's globalSetup. One
container, booted once, torn down at the end of the run. Tests still
get isolation because beforeEach truncates every personal-data table
in dependency-safe CASCADE order.
- tests/integration/global-setup.ts boots postgres:16-alpine,
runs `pnpm db:migrate:deploy`, exports a teardown for end-of-run.
- tests/integration/setup.ts now exports getPrismaClient() (which
delegates to the application singleton) and truncateAllTables().
No more startTestDb / stopTestDb.
- All four test files lose their beforeAll/afterAll boilerplate.
- vitest.integration.config.mts wires the new globalSetup.
- truncate list adds refresh_tokens (added in 0025_refresh_tokens).
Result: pnpm test:integration → 10/10 pass in 3.9s (was 6/10 with
4 cross-file failures). pnpm test → 658/658 unchanged.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pre-existing settings-page monolith has multiple set-state-in-effect
patterns that can only be cleaned up properly when the 2100-line file
gets broken into per-section sub-components (planned for 1.4.0). Until
then, blocking lint would just gate every PR. TypeScript strict checks
remain blocking.
Security hardening from 2026-04-26 audit:
- SEC-002: isPublicUrl() now uses a strict IPv4 parser that rejects
leading-zero forms like 010.0.0.1, which historically slipped past
the h.startsWith('10.') check. Also covers CGNAT (100.64/10),
IPv6 unique-local (fc00::/7), and additional metadata-service edge
cases. Documented as input-time only (DNS rebinding still possible
at fetch time).
- SEC-004: GitHub PAT is now redacted from logged error bodies in
publish-github.ts before flowing to Wide Events / Loki. Adds an
escapeRegex helper for safe RegExp construction.
- SEC-006: PUT /api/user/thresholds now goes through checkRateLimit
(30 writes / 5 min, per-user) — audit-log was already there but
unbounded. Makes audit-log enumeration unattractive.
- PERF-001: insights/comprehensive route now batches medication-intake
events into a single 'medicationId IN (...)' query and groups in
memory instead of N+1 per medication. Latency was scaling poorly
for users with many active meds.
- REL-003: pg-boss now installs SIGTERM/SIGINT handlers that call
boss.stop({ graceful: true, timeout: 30s }) so in-flight handlers
drain on docker stop / coolify redeploy instead of being killed.
- LOG-001: Replaced console.log/error in reminder-worker with a
workerLog helper. Lifecycle errors still go to stderr (only audience
outside any handler context); routine init messages are silent
because pg-boss own events + Wide Events from handlers carry the
state.
- CI: ESLint and TypeScript checks are now blocking — continue-on-error
removed. Tests already were. Required pre-existing 'any' types in
api-handler.ts to be properly justified with eslint-disable comments
(Next.js variadic handler signature; the bound type still constrains
return to Promise<Response>).
Issue #88's biggest blocker was that docs and README pointed at
ghcr.io/mbombeck/healthlog:latest, but no workflow ever pushed an
image — every self-hoster trying to follow the docs hit "manifest
unknown".
This adds .github/workflows/docker-publish.yml. The workflow:
- Triggers on push to main, on v* tags, on PRs (build only, no push),
and via workflow_dispatch
- Builds linux/amd64 + linux/arm64 multi-arch images via buildx
- Tags with `latest` (main only), semver (`1.2.0`, `1.2`), branch name,
PR number, and short SHA — using docker/metadata-action defaults
- Attaches SLSA build provenance and an SBOM to every image
- Caches build layers in GitHub Actions cache for fast iterations
- Uses concurrency group + cancel-in-progress so superseded runs stop
The bundled docker-compose.yml now references
`ghcr.io/mbombeck/healthlog:latest` as the primary `image:` and keeps
`build:` as a fallback so contributors can still
`docker compose up --build` against their working tree.
SECURITY.md gains a Supply Chain section explaining where images come
from, how to verify provenance and SBOMs, and how to pin a specific
version. README's Quick Start updated to reflect that no build is
needed for self-hosters.
The first run of this workflow on main will publish the image; until
then `docker compose up` will fall back to the `build:` block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HealthLog — personal health-tracking PWA with weight, blood pressure,
pulse, mood tracking, medication compliance, Withings integration,
doctor report PDF export, and Dracula-themed UI.