* 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.