README:
- Add a "How it compares" matrix against Withings web, Apple Health,
Oura, Garmin, and generic CSV. The audit found this section absent;
self-hosters arrive comparing alternatives and bounce when there's
no head-to-head.
- Promote Apple Health import + AI Coach as standalone Key-Features
bullets. Apple Health was the v1.4.34 banner feature with zero
grep matches in the README until now; the audit ranked it the
single highest-intent search keyword for ex-iOS users. AI Coach
+ Insights moved out from under "Multi-Provider AI Insights" into
a dedicated bullet that names the Coach, Daily Briefing, Weekly
Report, and Health Score surfaces.
- Status block under "What it is" so visitors see release cadence
+ current focus (native iOS client) without trawling the changelog.
- Badge row: drop "PRs welcome" + duplicate Self-Hosted, add a live
"Latest release" badge + a GHCR multi-arch badge so the landing
page surfaces version + container distribution at a glance.
- Tagline tightened to lead with "self-hosted health tracker" + the
two integration anchors (Withings + Apple Health) — both phrases
are search-keyword fronts the GitHub search index picks up.
- Drop the dead SESSION_SECRET env entry from Quick Start (3
secrets, not 4) and the Environment Variables table — the env
var was documented as required but never read by code (per the
code-quality audit). Companion env-removal in docker-compose /
.env.example / entrypoint ships in a parallel batch.
- Add a one-line OpenAPI pointer above the API-Reference table so
integrators discover the codegen source.
Layout / OG metadata:
- openGraph + twitter cards now carry images, a higher-intent
description, and a summary_large_image card. Uses the existing
logo-readme.png as the OG asset; replace with a 1200x630 dashboard
capture when one ships.
GitHub repo metadata (via gh CLI, no git diff):
- description tightened from 290 chars to 156 chars, keyword-front.
- homepage flipped from a personal tenant to demo.healthlog.dev so
sidebar clicks land on a working demo instead of a login wall.
- Topics: dropped 6 generic/PII-adjacent entries (glp-1, mounjaro,
tracking, health, dashboard, bloodpressure), added 6 high-intent
ones (apple-health-import, withings-alternative, glucose-tracker,
mood-tracker, ai-insights, personal-dashboard).
Planning artifacts:
- 7 audit reports drop into .planning/round-audit-*.md covering
mobile-security, UX, code-quality, README/discoverability,
perf, mobile-deep (pending), test-coverage, docs-depth. These
are the working notes for the v1.4.34.4 audit-driven release.
16 KiB
Code Quality + Documentation Audit — 2026-05-17
Executive summary
The repo is in unusually good shape for its size: tsc --noEmit and eslint
both pass clean, there are zero TODO/FIXME/XXX/HACK markers in product
code, zero empty catch blocks outside one inline <script> fallback, and all
test files contain at least one expect(). The dominant debt is archaeology
in comments — 2 366 v1.4.x version-tag references across 200+ files, plus
244 wave/phase markers (W5 reconcile (Code-H2), IW-B, phase B8). These
read like commit messages inlined into the source and will only get worse with
each release. The other three real wins are (1) deleting the 7-file
getXSystemPrompt / getXUserPrompt boilerplate in src/lib/ai/prompts/,
(2) removing the dead SESSION_SECRET env contract, and (3) closing the
JSDoc gap on src/lib/** exports (171/535 missing). Recommended order:
F-1 first (low-risk additive cleanup, biggest readability win), then F-2 and
F-9 in the same release, then the long-tail items.
Findings — prioritized
F-1: Version-tagged inline comments have become source-control archaeology
Severity: high
Category: comment debt
File(s): src/proxy.ts:28,32,36,50,179,190, src/app/page.tsx:85,93,99,109,201,207,217,253,259,272,281,288,307,328,353,396,401,539,564,589,675,890,921,935,1196,1214,1225, src/app/api/analytics/route.ts:36,55,67,... (47 hits), src/components/charts/health-chart.tsx (46 hits), src/lib/ai/schema.ts:5,28,32,52,55,... (29 hits), and 30 other files
What's wrong: 2 366 inline references to v1.4.x plus 244 markers
shaped like // v1.4.22 W5 reconcile (Sec-MED-2) — ... or
// v1.4.34 IW-B — .... They were useful while the release was in
flight, but they now narrate the commit history inside the source. A
reader of dashboard-layout.ts:288 doesn't care that the comparison
baseline shipped in v1.4.16 phase B8; they want to know what the
field means. The convention also locks future archaeology into the
codebase forever — every patch release inherits the previous tags.
Fix shape: Sweep in two passes:
- Strip the leading
v1.4.x [WAVE-CODE] —marker from any comment whose remaining body is still meaningful WHY-narration. Reduces every offender without losing intent. - Delete the comment outright when the body is just "added the X" or
"see plan Y". Sample target:
src/proxy.ts:28,32,36collapse into one// public paths bypassed by authblock;src/lib/dashboard-layout.ts:288,294,319,323lose all four tags and the field-doc above each definition stays. Effort: medium (one focused half-day, additive, deletes ~2 000 lines)[hotfix-ready]
F-2: Seven domain prompt files duplicate the same shell
Severity: high
Category: duplicate logic
File(s): src/lib/ai/prompts/bmi.ts:34-61, blood-pressure.ts:?, weight.ts:46-75, pulse.ts:52-81, mood.ts, general-status.ts, medication-compliance.ts
What's wrong: Every file in src/lib/ai/prompts/ exports the same pair —
getXSystemPrompt(locale) returns ${getBaseSystemPrompt(locale)}\n\n${section},
and getXUserPrompt(snapshotJson, todayKey, locale, previousContextBlock?)
returns a date-prefixed prompt with an optional context block. Only the
DE/EN section constants and the analytical "Analyse the X with focus on…"
sentence differ. The function signature drift risk is real — adding a
new arg today means editing seven files.
Fix shape: Replace the seven pairs with a single
buildDomainPrompts({ id, sectionDe, sectionEn, taskDe, taskEn }) factory
in prompts/factory.ts and a registry per metric. The
locale-en/-de branching in getXUserPrompt collapses to one
template-literal interpolation.
Effort: small (mechanical, fully covered by existing __tests__)
F-3: SESSION_SECRET is documented + required by entrypoint but never read
Severity: medium
Category: dead code
File(s): .env.example:48, docker-entrypoint.sh:12, README.md:94,154, CONTRIBUTING.md:25,34, docker-compose.yml:23,68, docs/self-hosting/scaling.md:24
What's wrong: The env var is listed as required, the docker entrypoint
fails-fast if it is missing, and onboarding docs tell every operator to
generate one with openssl rand -hex 32. grep -rn "SESSION_SECRET" src/
returns nothing. Sessions are server-stored in prisma.session keyed by
the cookie value (src/lib/auth/session.ts), so the secret has nothing
to sign. Every self-hoster carries a ceremonial 64-char hex string that
the app ignores. Either (a) wire it into a real signing path (HMAC the
cookie value) or (b) drop the requirement and the docs.
Fix shape: Pick (a) or (b). (b) is one removal in
docker-entrypoint.sh, the .env.example line, two README.md
lines, and the docker-compose.yml mapping. (a) is a real auth
change and should not be a hotfix.
Effort: small (for the deletion path) [hotfix-ready] if route (b) chosen
F-4: 171/535 exported functions in src/lib/** have no JSDoc
Severity: medium
Category: docs gap
File(s): highest offenders by file: src/lib/dashboard-layout.ts (0/3 functions documented), src/lib/telegram.ts (1/6), src/lib/process-type.ts (1/4), src/lib/glucose.ts (1/5), src/lib/utils.ts (0/1), src/lib/api-response.ts (2/4), src/lib/format-locale.ts (1/3)
What's wrong: The most-imported helpers (cn(), apiError(),
safeJson(), getProcessType(), mgdlToMmol(), getGravatarUrl())
have no one-line summary. Some have rich type signatures that carry
the meaning; some don't. safeJson<T>(req, schema) is a wrapper that
returns an apiError Response on parse failure — a non-obvious
shape worth documenting once.
Fix shape: One-line /** … */ on every top-10 import. Aim for
WHY/return-shape only, not WHAT. Marc's pattern in
src/lib/format.ts:40-62 is the model — single-line, locale-aware example
embedded.
Effort: medium (manual sweep, ~170 spots) [hotfix-ready]
F-5: as never and as unknown as chains hide schema drift
Severity: medium
Category: type safety
File(s): src/app/api/admin/backups/[id]/restore/route.ts:299,302,357, src/lib/personal-records/pr-detection-worker.ts:271,357,434,437,438, src/lib/devices/revoke.ts:79, src/lib/ai/coach/snapshot.ts:403,482, src/lib/ai/prompts/insight-generator.ts:729,730,731,737,738,739 (and 14 more), src/lib/medications/route-guards.ts:61, src/lib/jobs/apple-health-import-worker.ts:85,176,194,195
What's wrong: 20 as unknown as T and 9 as never assertions. The
PR worker's where: where as never and dynamic-key orderBy casts
disable the very Prisma narrowing the rest of the file relies on; if
the schema renames a column the worker silently builds a malformed
query at runtime. MEDICAL_REFERENCE_IDS as unknown as [string, ...string[]]
in provider-chain/route.ts:82 exists because Zod wants a non-empty
tuple — a .refine() against the readonly array would type-check
without the cast.
Fix shape: For each cast, either fix the underlying type or add a
WHY-comment. The Prisma JSON casts in chart-overlay-prefs,
widgets, healthkit, doctor-report-prefs (5 sites) collapse onto
one helper toInputJsonValue<T>(value: T): Prisma.InputJsonValue. The
pr-detection-worker.ts dynamic-orderBy can switch to a
Record<string, "asc" | "desc"> and drop the cast entirely.
Effort: medium
F-6: Legacy src/lib/format.ts is a soft-deprecation that won't die
Severity: low
Category: simplify | duplicate logic
File(s): src/lib/format.ts:23-62 (the file itself) and 20 import sites
What's wrong: The file's docstring tells new code to use
useFormatters() instead, but 20 production import sites still call
formatDate / formatDateTime / formatTime / formatDateShort.
The shim reads document.cookie on the client and falls back to en
on the server, which means SSR can render English where the user has
German selected — exactly the locale hydration mismatch the docstring
warns about.
Fix shape: Either migrate the 20 callers to useFormatters()
(client) or makeFormatters(serverLocale) (server) and delete
src/lib/format.ts, or accept it and remove the soft-deprecation
docstring so a future contributor doesn't re-introduce the bug it
warns against.
Effort: small (mechanical migration)
F-7: Mega-files near 1 700 lines in product code
Severity: low
Category: simplify
File(s): src/lib/jobs/reminder-worker.ts (1 653 lines), src/app/api/insights/targets/route.ts (1 198), src/app/api/analytics/route.ts (1 073), src/app/api/gamification/achievements/route.ts (988), src/components/settings/ai-section.tsx (1 739), src/components/charts/health-chart.tsx (1 710), src/app/page.tsx (1 348)
What's wrong: Single files holding multiple concerns. reminder-worker.ts
mixes scheduling, channel adapters, dedup, and tests-flag logic.
/api/analytics/route.ts has an unrelated "slim slice" branch
inlined into the same handler.
Fix shape: Only the worker is genuinely a problem — split per
channel and per phase. The route files are acceptable because they
read top-to-bottom; the chart components have to be one file for
Recharts callbacks to share state cheaply. Reminder worker → split
in a dedicated round, not a "while-I'm-here".
Effort: large (do not bundle into a hotfix)
F-8: coach-prefs excludeMetrics is a subset enum cast at runtime
Severity: low
Category: type safety
File(s): src/lib/ai/coach/snapshot.ts:400-405
What's wrong: CoachExcludeMetric is a strict subset of
CoachScopeSource and the snapshot loop casts each source via
as unknown as CoachExcludeMetric then runs a Set.has() lookup as
defence-in-depth. The cast is documented but the comment says
"the runtime check is just defence-in-depth" — which contradicts
the cast. A (src in EXCLUDABLE_SET) predicate-typed helper would
remove the cast and keep the narrow type at compile time.
Fix shape: Introduce function isExcludable(src: CoachScopeSource): src is CoachExcludeMetric.
Effort: trivial
F-9: Prisma JSON serialisation needs one helper
Severity: low
Category: duplicate logic
File(s): src/app/api/auth/me/doctor-report-prefs/route.ts:81, src/app/api/dashboard/chart-overlay-prefs/route.ts:83, src/app/api/dashboard/widgets/route.ts:156, src/app/api/integrations/healthkit/route.ts:177, src/lib/jobs/apple-health-import-worker.ts:85,176,194,195
What's wrong: Seven sites do value as unknown as Prisma.InputJsonValue
to satisfy Prisma's JsonValue union. The cast is correct (the values are
plain JSON-serialisable objects) but each call site re-invents the same
escape hatch.
Fix shape: One generic helper in src/lib/db.ts:
export const toJson = <T>(v: T) => v as unknown as Prisma.InputJsonValue;
with a WHY-comment about why the cast is needed.
Effort: trivial [hotfix-ready]
F-10: db-compat.ts is the maintained migration system
Severity: informational
Category: dead code (false positive)
File(s): src/lib/db-compat.ts (200 lines), 51 call sites
What's wrong: At first glance this file looks like accumulated
backwards-compat (which Marc's preferences explicitly forbid). On read
it's the production "ALTER TABLE IF NOT EXISTS" path that bootstraps a
fresh container without prisma migrate deploy. Worth a top-of-file
docstring saying so — right now anyone scanning for cruft will flag it.
Fix shape: Add a header comment naming it the "schema-bootstrap" path
and pointing at the prisma/migrations/ directory for the proper
migration history. No code change.
Effort: trivial [hotfix-ready]
F-11: eslint-disable on passkey.ts response: any parameters
Severity: low
Category: type safety
File(s): src/lib/auth/passkey.ts:103-104,169-170
What's wrong: Both verifyRegistration and verifyAuthentication
take response: any with a per-line eslint-disable. The upstream
@simplewebauthn/server exports RegistrationResponseJSON and
AuthenticationResponseJSON types for these payloads.
Fix shape: Import the SimpleWebAuthn types, drop the
eslint-disable lines.
Effort: trivial
F-12: Comment quality wins: WHAT-narration leaks
Severity: low
Category: comment debt
File(s): sample: src/lib/dashboard-layout.ts:262-263 ("Merge with defaults so new widgets…"), src/lib/ai/coach/snapshot.ts:413-416 ("Trim down to the metrics…"), src/app/api/analytics/route.ts:55-66 ("v1.4.33 C1 — slim summaries slice…")
What's wrong: Most long block comments narrate WHAT the code does
in plain English right above identical code. Some carry the WHY (the
/api/analytics slim-slice block does explain the read-budget
trade-off). The WHAT-only sections should collapse.
Fix shape: Treat any block comment whose next 3 lines paraphrase the
comment as deletable. Sample: dashboard-layout.ts:253-258 keeps the
"v1.4.28 retired" version tag (drop per F-1) and the WHY ("PUT route's
Zod enum rejects the entire blob") — drop the WHAT-rephrasing in the
middle.
Effort: medium (folds into F-1 sweep)
F-13: route-guards.ts import is lazy with an unstubbable cast
Severity: informational
Category: type safety | test discipline
File(s): src/lib/medications/route-guards.ts:56-62
What's wrong: loadPrisma() lazy-imports @/lib/db so test files can
stub the module — fine pattern — but the return type forces a
prisma as unknown as MedicationOwnershipPrisma cast at the boundary.
MedicationOwnershipPrisma is a stripped-down interface declared
locally; the real PrismaClient is structurally compatible. The cast
is honest but a brief comment saying "stripped interface for test
stubbability" sits below the function instead of above. Move it up.
Fix shape: WHY-comment relocation.
Effort: trivial
F-14: as never cast comment in backups/restore/route.ts is the right pattern
Severity: informational
Category: positive example
File(s): src/app/api/admin/backups/[id]/restore/route.ts:299
What's wrong: Nothing. type: m.type as never, // already enum-validated above
is the model for every other cast in F-5: the WHY is the trailing
comment, not a block above. Aim for this in F-5.
Effort: n/a
F-15: Test discipline spot-check is clean
Severity: informational
Category: test discipline
File(s): 400 test files scanned
What's wrong: Nothing material. Every test file contains at least one
expect(). No empty catch blocks in production tests. No
try/catch swallow patterns. The codebase has 0 TODO, 0 FIXME,
0 XXX, 0 HACK markers — this is genuinely rare and worth
preserving with a CI grep.
Fix shape: Add a tiny CI grep gate enforcing the zero-TODO rule
explicitly, so a future contributor doesn't reintroduce the pattern
in the next marathon.
Effort: trivial [hotfix-ready]
Counts
- Files with > 10 obsolete version-tagged comments: 35
any/as unknown as/as nevercast-throughs: 6 + 20 + 9 = 35 (excluding tests + generated)- Open TODO / FIXME / XXX / HACK total in product code: 0
- Exported functions in
src/lib/**without doc-comment: 171 / 535 (32 %) - Total
v1.4.xreferences in product code: 2 366 - Test files without
expect(): 0 / 400 tsc --noEmitclean: yespnpm lintclean: yes
Out of scope
- Splitting
reminder-worker.ts(F-7) — wants its own round, not a hotfix. - Refactoring
health-chart.tsx(1 710 lines) — Recharts callbacks share state cheaply only when colocated; a split needs UX validation first. - Migration of legacy
src/lib/format.tscallers (F-6) — touches 20 files across the dashboard; defer until the next i18n pass. - Generated Prisma client (
src/generated/prisma/) — third-party output, not maintained source. - E2E tests under
e2e/andtests/— separate audit scope. - Translation/copy quality in
messages/— separate audit scope.