Files
HealthLog/.planning/round-audit-code-quality.md
T
Marc-André Bombeck e0fe7c0a3d docs: README discoverability rework + OG metadata + planning audit reports
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.
2026-05-17 00:28:05 +02:00

291 lines
16 KiB
Markdown

# 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:
1. 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.
2. Delete the comment outright when the body is just "added the X" or
"see plan Y". Sample target: `src/proxy.ts:28,32,36` collapse into one
`// public paths bypassed by auth` block; `src/lib/dashboard-layout.ts:288,294,319,323` lose 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 never` cast-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.x` references in product code: **2 366**
- Test files without `expect()`: **0 / 400**
- `tsc --noEmit` clean: yes
- `pnpm lint` clean: 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.ts` callers (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/` and `tests/` — separate audit scope.
- Translation/copy quality in `messages/` — separate audit scope.