Dependabot treats `init`, `analyze` and `upload-sarif` as separate
dependencies, so it raised `init` to 4.37.7 in one PR and left `analyze` on
4.37.6 in the same workflow. The action refuses that pairing outright:
"Loaded a configuration file for version '4.37.7', but running version
'4.37.6'", and the CodeQL job failed on the very PR doing the bump.
All five pinned references move together here: init and analyze in
codeql.yml, and the three upload-sarif steps in scorecard.yml and trivy.yml.
The commit SHA is the one the v4.37.7 tag dereferences to, which is also the
SHA both Dependabot PRs pin.
The determinism guard reads the job that runs the browser, which is now
the shard matrix rather than the gate that only carries the required
check name. The report artifact is named per shard so the two uploads
cannot clobber each other.
The single job passed at about eighteen minutes against a twenty-minute
cap, and the settings geometry measurements pushed it over, so the run
died at the timeout with everything to that point green. Same remedy as
the integration suite, split rather than raise: two Playwright shards,
each with its own database and production build, and a gate job carrying
the exact name branch protection requires that fails when any shard
failed or was cancelled.
Two shards cleared in about fourteen minutes on a quiet day, but with six
pull requests running side by side the larger shard hit the same 25-minute
guard the split was meant to escape. Three shards keep the biggest slice
around sixty files, which clears the guard with real headroom on the
slowest runner observed. The gate job is unchanged.
The suite outgrew its runner. The 25-minute guard was sized for 128 files
taking about 13 minutes; the sharing release took the suite to 180 files and
past the cap on CI runners, so every run died at the timeout with every test
to that point green, reported as cancelled. The guard's own rule applies:
split, don't raise.
The files run as two vitest shards, each with its own Postgres testcontainer.
A gate job carries the exact name branch protection requires and fails when
any shard failed or was cancelled, so a half-green matrix cannot satisfy it.
vitest is invoked directly because pnpm passes the -- separator through to
the script literally: the flag never applied through the script form, and
both "shards" ran the full suite. Proven by paired local runs: the script
form ran 180 files twice, the direct form splits 90/90 with 927 plus 689
tests, all green.
Most of what Scorecard looks for this repository already does: Actions
pinned by commit SHA, a security policy with a private reporting path,
Dependabot with auto-merge, keyless signing on the release image, a
branch-protected trunk. The value is not the score, it is that the checks
keep running after the person who set them up has moved on, and that
anyone evaluating the project can read the result without being given CI
access.
publish_results is on, which is what makes the score citable. It reads
repository metadata and the workflow files; nothing about the code or its
contents goes anywhere. The token is an OIDC exchange, the same mechanism
the release workflow already uses to sign the image.
No badge in the README yet. The public API has no entry for this
repository until the first run publishes one, and the badge URL currently
renders 'invalid repo path'. It goes in once there is a score behind it.
Two questions the existing scans cannot answer.
The dependency audit runs with --prod, which is the right scope for a
blocking gate but leaves a gap: a dev dependency can still ship. pdfjs-dist
is the case that made this concrete. It sits in devDependencies, and
rasterize-pdf.ts imports it at runtime, so standalone output tracing copies
it into the production image where the prod-scoped audit has never looked.
Trivy reads 627 packages out of pnpm-lock.yaml without --include-dev-deps
and 1455 with it; pdfjs-dist is only in the second set. Trivy's help text
lists the flag as npm/yarn/gradle only, but it works on the pnpm lockfile
too, which is worth knowing before someone deletes it as a no-op.
The container scan builds the image from the current main. Nobody runs
that image. What self-hosters pull is the one built from the last release
tag, and an advisory published after the tag lands against an image
nothing is about to rebuild. So the second job scans ghcr.io/mbombeck/
healthlog:latest instead of a local build.
Schedule only, no push or pull_request trigger, because the change-
triggered half is already covered and would just run twice. The lockfile
job reports into code scanning without failing: widening to dev
dependencies surfaces transitive tool advisories by the dozen, and a red
badge from day one gets muted inside a week. The image job fails on
CRITICAL, after its own report has been uploaded by a separate step.
Static analysis was the one class of check the pipeline had no answer for:
lint catches style, the type checker catches shape, the tests catch
behaviour, and nothing walked the data flow from an untrusted input to a
sink. This is GitHub's default setup written out as a workflow file, so the
query suite, the excluded paths and the schedule sit in the diff rather
than behind a settings toggle.
Pull requests and a weekly run on the default branch. No push trigger:
everything reaches main through a pull request that was already analysed,
and the weekly run is what keeps the Security tab current.
src/generated is excluded. Extracting the 9 MB Prisma client costs minutes
and can produce nothing anyone would act on, because the fix for anything
found in there is to regenerate it.
Vitest is a knip plugin, so every file under `src/**/__tests__/**` is an
entry, and a module whose only importer is its own test counts as used
for as long as the test survives. Planting a module with a single test
importing it left `pnpm knip` at exit 0 — the gate reports green on code
production stopped reaching.
Knip can answer the question, just not in the pass CI runs: production
mode drops the plugin's test entries and keeps the entry and project
patterns suffixed with `!`. Mark them, which is inert in the default
pass, so `pnpm knip --production --include files` is one command away.
It stays a review tool rather than a second gate, and the config says
why: twenty-nine files today, some test-only by construction and some
unreachable from production on purpose, plus at least one false positive
where a script's `@/` import does not resolve. Baselining that set
unread would encode noise as an invariant.
Move the config to JSONC so all of the above lives next to the settings
it describes instead of in a commit message nobody will run `git log`
for.
The e2e job called next build directly, skipping the prebuild hook
that generates the service-worker version stamp and the static i18n
catalog assets — fourteen specs timed out waiting on a UI that never
got its catalog.
The measured first-load gap on / was never the server: FCP painted the
SSR shell at ~1.8 s but LCP landed at ~5.5 s (mobile-throttled) because
the tiles waited for JS download + hydrate + a CLIENT-side snapshot
fetch — while /api/dashboard/snapshot answers in ~8 ms server-side.
src/app/page.tsx is now a thin RSC wrapper: it runs the same cached
snapshot read the API route uses (extracted to
src/lib/dashboard/snapshot-read.ts, one shared SWR cell for both
readers) and hands the result to TanStack via HydrationBoundary +
dehydrate, so the first HTML carries real tile data and the mounted
cell starts warm (staleTime 60 s, refetchOnMount false — no double
fetch). Query keys come only from the central factory
(dashboardSnapshot(locale) + the dashboardWidgets seed the client cell
already performs); the payload is JSON-round-tripped so the hydrated
shape is byte-equal to the wire shape. Fail-soft: no session or any
builder error renders the page exactly as before. The 1,727-line client
page moves unchanged to src/app/page-client.tsx; no component-tree
rewrite.
Companion: the documented bundle tooling was dead on Turbopack
(ANALYZE=1 silent no-op, bundle-report read a webpack-only JSON), so
H2/H3-class regressions shipped invisibly. scripts/check-bundle-budget.mjs
derives per-route gzip totals from the Turbopack client-reference
manifests + rootMainFiles, prints the report (pnpm bundle-report), and
enforces bundle-budget.json (pnpm bundle-check): per-route caps,
total-client-JS cap, a recharts-duplication guard (fingerprint count
must stay 1), and a catalog guard (no message catalog referenced by any
route manifest). Wired into the existing e2e workflow after next build.
H4
next 16.2.10, @tanstack/react-query 5.101.2, nodemailer 9.0.3,
react-hook-form 7.81.0, @types/node 26.1.0; docker buildx/metadata/
build-push action pins to their current releases.
A deterministic golden-set + red-team floor that gates every Coach change
offline, plus an opt-in live-judge layer that scores warmth and groundedness
with a pinned model only when COACH_EVAL_API_KEY is set.
- 42 golden cases across grounding, cross-metric, data-honesty, provider-parity
and own-baseline taxonomies, graded by behaviour-anchored binary criteria.
- Structured-claim grounding helpers extend the numeric verifier from numbers
to threshold / own-baseline / confident-verdict claims; the numeric API is
unchanged so the existing grounding tests stay green.
- Red-team battery asserts the existing inbound refusal, outbound dose screen,
acute red-flag escalation and learn-link guards fire.
- Nightly + manual workflow runs the deterministic floor always; the live judge
step runs only when the secret is present and is non-blocking.
Wire `pnpm format:check` into the Lint/Typecheck/Test job and extend
.prettierignore so prettier never rewrites generator-owned artifacts:
the OpenAPI contract (whose exact bytes the openapi:check job enforces),
the build-emitted service worker, the Prisma client, and the lockfile.
Published manifests are now cosign-signed keyless with a documented
digest-pinning path, a lefthook pre-commit runs gitleaks over staged
changes, responses opt into a credentialless embedder policy, the
deploy webhook can require a fresh timestamp, and a monthly drill
decrypts and parses the newest offhost backup so a broken key or format
surfaces before an emergency does.
The freshly released metadata-action v6.1.0 exits without output on the
pull-request build path; both call sites pin back to the proven v6.0.0
commit. The dead-code gate also caught an unused read helper and an
unused analytics type left behind by the wave — both removed.
Every workflow action is pinned to a commit SHA, dependabot auto-merge
is limited to patch and minor updates behind the now-required status
checks on main, and the audit, Trivy and ESLint gates block instead of
warning. The published image finally carries its build SHA and build
timestamp so the version endpoint can prove which commit is running.
The API-token HMAC key fails closed under 32 characters, compose pins
the database minor and denies privilege escalation, and the changelog
drops working-note references in favour of neutral wording.
The project licence changes from AGPL-3.0 to the PolyForm Noncommercial
License 1.0.0 for this and future versions: free to use, modify and
self-host for noncommercial purposes; commercial use needs a separate
arrangement. Releases up to and including v1.15.18 were published under
AGPL-3.0 and remain available under that licence. LICENSE carries the
verbatim PolyForm text, and the SPDX identifier is updated in
package.json, the version endpoint, the OpenAPI info block, the image
labels and the README.
Blood-pressure fixtures in the in-target integration suite were copied
from live readings; they are now synthetic with the same in/out split so
every assertion holds. Hard-coded operator hostnames move behind env vars
(DEPLOY_LOGS_URL, HEALTHLOG_PROD_URL), the codex client identifies via
the public repository URL, the OpenAPI servers entry points at a neutral
example host, and remaining workflow, docs and comment references to the
operator's instance or working notes are generalised. The compose
whitelist also gains DEPLOY_WEBHOOK_SECRET, which was missing and made
the deploy webhook unusable on compose installs.
Both are Dependabot bumps that have been waiting on the queue; landing
them directly so the lock files stay coherent with the rest of v1.5.5.
knip: minor; docker/metadata-action: clean major (workflow change is
exactly the @v5 → @v6 string in two places).
Closes#209Closes#183
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS-operator-blocking hotfix for REG-11 — Home dashboard tile renders
neither chart nor latest value for BP / Puls / Körperfett when the
most recent reading is older than 7 days. Root cause was in
`/api/dashboard/summary` SQL gates; `latestIn7d` + `sparkBuckets` both
required `measured_at >= sevenDaysAgo`, which returned empty for
sparse accounts, leaving the iOS tile with `latestValue: null` +
`sparkline: []` to render from. Detail-screen
(`/api/measurements/series?kind=…`) escaped because of its broader
window.
Server-side changes:
- `latestIn7d` → `latestEver` (drops 7-day filter; DISTINCT ON returns
latest reading EVER per type, bounded at N_metrics rows).
- `sparkBuckets` rewritten via ROW_NUMBER() OVER (PARTITION BY type
ORDER BY bucket_start DESC) so the trailing N daily buckets per
type render regardless of age, bounded at SPARK_DAYS × N_metrics.
- BP + Pulse metrics.push gated on `latest || allTimeCount > 0` like
BodyFat already is — accounts with zero readings ever no longer get
empty placeholder tiles.
- 4 new regression tests in summary/__tests__/route.test.ts cover
sparse accounts, zero-data accounts, within-7d guards.
Other develop work bundled in this release:
- W4-QoL — copy nits.
- W5-WORKOUTS — user source priority threaded through write-time
picker + batch route.
- W7-WITHINGS-RELIABILITY — response classifier + sync hardening.
- W8-OPS — ops phase docs.
- B11 — NEXT_PUBLIC_APP_VERSION build-arg routing so /api/version
cannot drift from CI tag.
- env-check workflow — manifest / example lockstep enforcement.
Resolves PR #194 (squash-merge produced via local -X theirs strategy
because GitHub's UI couldn't auto-resolve develop's post-v1.4.42
contiguous work against the v1.4.42 squash on main; resulting tree is
byte-identical to develop's HEAD).
Six touch-disjoint waves landed on develop before this release:
- W1-KNIP — exports/types tiers flipped to enforcing-mode on main;
35 unused exports + 52 unused types triaged to 0/0; shadcn surface
+ zod-infer contract types scoped via knip.json ignore-block.
- W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper +
`/api/dashboard/widgets` 422 now returns every Zod issue + worker
audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43.
- W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files
migrated to the queryKeys factory; 9 new factory entries; ESLint
rule + test-guard scope extended in lockstep.
- W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine
sites (seven status helpers + bucket-series + bp-in-target);
Suspense double-comment consolidated; doctor-report sanitiser regex
rewritten to escape-form so diffs are reviewable; pr-detection-
worker soft-delete filter; offhost-backup DR-intent comment.
- W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source
dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard
tile-strip placeholder parity with the live TrendCard chrome.
- W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier
(transient/reauth/persistent) + WithingsApiError typed throw + the
`pnpm check-env` CLI. APNs `allOrNone: true` closes the AP-2
silent-disable gap the wave was conceived to close.
No migration, no env-var change, no API contract break for iOS v0.5.4.
The new `/api/dashboard/widgets` 422 envelope is additively extended.
pnpm test --run green at 4815 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green.
Eight touch-disjoint waves landed on develop before this release:
- W-INSIGHTS-HOT + W-RECONCILE-INSIGHTS — extract the timeout-stub
persist helper and wire all seven *-status routes through it, closing
the recurring ~14 s warm response the iOS client sees on dashboard
open once the AI provider has stalled once that day.
- W-DELETED-2 — close the remaining three soft-delete reader-tier gaps
(export bundle, gamification achievements, doctor-report aggregator)
with a five-route integration-test pin.
- W-ORG — types consolidation into src/types/analytics.ts and
src/types/backups.ts; prompt directory unification.
- W-FRONTEND-FACTORY — queryKey factory expansion + ESLint rule lift.
- W-IOS-COORD — POST /api/auth/check-user four-branch onboarding
discovery, identifier matched verbatim, per-IP rate-limited.
- W-SIMPLIFIER — today-intake projection helper extract; unused tx?
params dropped; 13 knip-flagged dead exports trimmed.
- W-PERF-OPS — v1.4.39 W-SUM legacy-NULL UNION discovery arm retired.
- W-PROCESS-DOCS — pg.Pool sizing operator doc; ESLint rule
formalisation; knip CI gate staged for v1.4.42 flip.
No migration, no env-var change, no API contract break for iOS v0.5.4.
The new check-user endpoint is an additive surface. AP-2 .p8 key is
installed; SB-5 time-sensitive APNs is live as of this deploy.
pnpm test --run green at 4732 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip green.
Eleven touch-disjoint waves landed on develop plus a six-axis QA
reconcile round before tagging. See CHANGELOG.md for the user-facing
prose; this commit message documents the wave + report inventory for
release-management traceability.
Waves shipped (each with a phase report under .planning/):
- phase-W-POOL-v1440-report.md — p-limit(4) wrapper around the
analytics 15-way fan-out + pg.Pool max raised 10 -> 20. Closes the
v1.4.39 empirical-trace finding #1 (Prisma pool starvation).
- phase-W-DELETED-v1440-report.md — 11 reader-tier helpers + 3
integration contracts close audit Critical Finding #3 (soft-delete
invisibility) across dashboard, analytics, summaries, rollup tiers,
and the Coach snapshot path.
- phase-W-INSIGHTS-v1440-report.md — mood-rollup swap on the three
/api/insights/{features,targets,cards} routes plus the
compliance-rollup hook gap closure for bulk-projection mints; the
post-QA reconcile patch closes the six remaining measurement.findMany
sites in the cluster that still leaked tombstoned rows.
- phase-W-INFRA-v1440-report.md — rollup helper umbrella consolidation
under src/lib/rollups/* with zero orphan re-exports; lint, typecheck,
knip, and 93/93 rollup tests green post-restructure.
- phase-W-RSC-v1440-report.md — per-tile Suspense boundaries plus the
queryKey factory-bypass walker test (extended to comparison/ in the
reconcile pass).
- phase-W-PRIVACY-v1440-report.md — bilingual /privacy page with 17
paired-section tests pinning the 9 SB-3 disclosure requirements.
- phase-W-AASA-v1440-report.md — /.well-known/apple-app-site-association
endpoint pinned to S8WDX4W5KX.dev.healthlog.app with app-ID parity
assertions guarding against split rotation.
- phase-W-APNS-NOTIFY-v1440-report.md — MEDICATION_REMINDER
time-sensitive APNs payload (priority 10 + interruption-level)
scoped via a parameterised test that pins all 6 other event-types
do not bypass Focus. AP-2 .p8 install gates real delivery.
- phase-W-CONSENT-v1440-report.md — AI consent receipts CRUD with
append-only invariant pinned in test; revoke-then-regrant leaves
both rows; idempotent DELETE returns 200 + receipt: null.
- phase-W-WMY-WIRE-v1440-report.md — three previously-dead WMY readers
(week/month/year) wired through summaries-slice and
health-score-fast-path with a linear-composability parity test
pinning the cross-consumer routing contract.
- phase-W-GHOSTS-v1440-report.md — ~1 177 lines of dead code removed
across 9 atomic commits; startOfUtcDay + wallClockInTz consolidation
eliminates a sibling-drift risk; knip whitelist trimmed to three
files pending follow-up.
QA reconcile patches applied per the v1.4.40 QA round (six reports
under .planning/round-v1440-QA-*-findings.md):
- senior C1 — dashboard-suspense-boundaries regex aligned with the
post-W-INFRA Thread 2 userTimezone-local lift.
- senior H1 — soft-delete filter added to the six remaining
insights measurement.findMany sites.
- specialist H1 — Promise.allSettled around bulk-projection
compliance recompute on both /api/medications/intake and
/api/dashboard/summary.
- ux H1 — compare-toggle migrated to queryKeys.dashboardWidgets
factory; walker test extended to src/components/comparison/.
- security M1 — consent artefact 64 KB cap enforced via
Buffer.byteLength (UTF-8 bytes, not UTF-16 code units).
Quality gates green at tag: pnpm typecheck, pnpm lint, pnpm knip
--include files,dependencies,binaries,unlisted, pnpm test --run
(4726 passing / 1 skipped). Tests 4525 -> 4726 unit + 1 skipped over
the v1.4.39 -> v1.4.40 span.
User-facing prose: see CHANGELOG.md.
Consolidated hotfix landing the findings from a six-axis audit
covering mobile security, UX + accessibility, code quality + docs,
README + discoverability, web performance, and mobile-deep responsive
behaviour. None of the changes break the public API or the existing
data shape; every fix is additive and SAFE under the v1.4.34.x web
freeze. Full audit reports live in `.planning/round-audit-*.md`.
Security
- Password rotation now wipes API tokens + refresh tokens in the
same `$transaction` as session revocation. A stolen iOS Bearer no
longer survives a user-initiated password change or an admin reset.
- `/.well-known/` proxy bypass tightened to an explicit-allowlist
`Set`. Stale `/api/auth/codex/callback` PUBLIC_PATHS entry dropped.
- HSTS gains `preload`. Withings host removed from the global CSP
and gated to the integration's own routes.
- `getClientIp` warns once per process on `TRUST_PROXY_HOPS`/XFF
mismatch + exposes a tagged helper for tighter universal buckets.
- Service worker rejects off-origin notification-click URLs. Offline
fallback body switched to a locale-neutral icon. Manifest gains
`scope`, `id`, `display_override`.
UX + accessibility
- i18n leaks closed on Dialog/Sheet close-X, ChartSkeleton's loading
announcement, and the notifications-section breadcrumb.
- Focus rings on bottom-nav, the More overflow button, and the
mobile top-bar user menu.
- Tap-target floor: Input / NativeSelect / Select / DateTimeInput
switch to `h-11 sm:h-10`.
- Admin chip-strip auto-scroll-into-view (matches v1.4.33 settings).
- List-page subtitles visible on mobile.
- Insights empty-state CTA vocabulary normalised so the link opens
the right measurement form (was silently failing for
BODY_TEMPERATURE / BLOOD_GLUCOSE / PULSE).
Mobile-deep
- `viewport.themeColor` reacts to light/dark mode.
- TopBar + Coach FAB reserve safe-area-inset-top / -bottom for
notched iPhones.
- Sonner toaster switches to `theme="system"`.
Code quality
- `SESSION_SECRET` dropped — the env var was documented as required
but never read by code. Three secrets, not four. Self-hosters
upgrading can delete the existing line.
- `toJson<T>()` helper consolidates seven Prisma JSON-cast sites.
- `db-compat.ts` gains a header docstring naming it the
schema-bootstrap path.
- Passkey response types adopted from `@simplewebauthn/server`;
`any` + eslint-disable pragmas drop out.
- Zero-TODO CI gate added at `.github/workflows/no-todo-markers.yml`.
Documentation + discoverability
- README rework: How-it-compares matrix, Apple Health + AI Coach
promoted to standalone Key-Features bullets, status block, latest
release + GHCR badges, OpenAPI pointer, three-secret Quick Start.
- openGraph + twitter cards now carry `images` and a
`summary_large_image` Twitter card.
- GitHub repo metadata: description tightened from 290 to 156
chars + keyword-front, `homepageUrl` flipped from a personal
tenant to the demo site, six generic / PII-adjacent topics
swapped for six high-intent ones.
- Five new user-facing docs: `docs/self-hosting/getting-started.md`,
`docs/self-hosting/reverse-proxy.md`,
`docs/integrations/apple-health.md`,
`docs/integrations/withings.md`,
`docs/integrations/ai-providers.md`.
- `docs/README.md` clarifies the docs tree as internal playbooks
vs the user-facing `docs.healthlog.dev` site.