Skip to content

feat: dynamic health status + canonical reference ranges + DB migrations#4

Open
Chocksy wants to merge 1 commit intozmeyer44:mainfrom
Chocksy:pr/dynamic-status-ranges
Open

feat: dynamic health status + canonical reference ranges + DB migrations#4
Chocksy wants to merge 1 commit intozmeyer44:mainfrom
Chocksy:pr/dynamic-status-ranges

Conversation

@Chocksy
Copy link
Copy Markdown

@Chocksy Chocksy commented Apr 7, 2026

Summary

  • Dynamic status system: useDynamicStatus hook computes optimal/borderline/elevated from DB reference ranges instead of hardcoded thresholds
  • Production-safe DB migrations: Seed data via migrations (not seeds) to avoid duplicates on redeploy. Includes NULL-safe unique index, HOMA-IR backfill, and metric code consolidation.
  • Extended observations router: correct/confirm/delete mutations with original value tracking

Key Changes

  • apps/web/hooks/use-dynamic-status.ts — Dynamic status hook
  • apps/web/lib/panel-config.ts — Clinical category mapping
  • apps/web/lib/health-utils.ts — Percentage-based range evaluation
  • packages/database/drizzle/0010_seed_missing_metric_data.sql — Migration for metric defs + optimal ranges + HOMA-IR backfill
  • packages/database/drizzle/0011_consolidate_metric_codes.sql — Canonical code consolidation
  • apps/web/server/trpc/routers/observations.ts — Extended CRUD mutations
  • packages/database/src/seed/data/optimal-ranges.ts — 40+ biomarker ranges

Test plan

  • Run migrations on fresh DB — verify no errors
  • Verify labs detail page shows dynamic status colors
  • Verify HOMA-IR observations created from glucose+insulin pairs
  • Verify /labs/hba1c shows data (not empty due to code mismatch)

Adds dynamic status computation, production-safe migrations, and
comprehensive reference range data.

Dynamic status system:
- useDynamicStatus hook computes optimal/borderline/elevated from
  reference ranges instead of hardcoded thresholds
- Panel config maps metrics to clinical categories with display metadata
- Enhanced health-utils with percentage-based range evaluation

Database migrations (production-safe):
- 0009: Schema additions (observations metadata, user demographics)
- 0010: Seed missing metric definitions + optimal ranges via migration
  (not seeds, to avoid duplicates on redeploy). Includes NULL-safe
  unique index on optimal_ranges. Backfills calculated observations
  (HOMA-IR) from same-date glucose+insulin pairs.
- 0011: Consolidate duplicate metric codes to canonical forms
  (hemoglobin_a1c → hba1c, etc.)

Other improvements:
- Extended observations router with correct/confirm/delete mutations
- Observation correction tracking (original values preserved)
- Optimal ranges seed data for 40+ biomarkers
- Labs detail page uses dynamic status
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 7, 2026

@Chocksy is attempting to deploy a commit to the Zach's Projects Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 7, 2026

Greptile Summary

This PR replaces hardcoded abnormality thresholds with a useDynamicStatus hook that computes status from DB-backed optimal/reference ranges, adds inline edit/delete for observations, and delivers production-safe migrations seeding metric definitions, optimal ranges, and derived-metric backfills. The key concern is a stale-closure bug in resultColumns where getStatus is missing from the useMemo dependency array, causing status badges to show Normal for out-of-range values on metrics where displayPrecision does not change on load.

Confidence Score: 4/5

Safe to merge after fixing the stale getStatus closure; without it dynamic status silently displays wrong badges

The missing getStatus dependency is a present display defect that directly undermines the PR primary goal. One-line dep-array fix resolves it. All other findings are P2 style or performance suggestions.

apps/web/app/(dashboard)/(main)/labs/[metricCode]/page.tsx - the useMemo dependency array on line 255

Important Files Changed

Filename Overview
apps/web/app/(dashboard)/(main)/labs/[metricCode]/page.tsx Adds inline edit/delete UI and dynamic status rendering; stale getStatus in resultColumns memo can show wrong status badges and delete is an unguarded hard delete
apps/web/hooks/use-dynamic-status.ts Clean hook building rangesMap from metrics and optimal ranges with correct optimal-over-reference priority
apps/web/lib/health-utils.ts Well-structured utilities; critical threshold logic is reasonable; single-sided range severities handled correctly
apps/web/lib/panel-config.ts Panel config updated to canonical metric codes matching migration consolidation
apps/web/server/trpc/routers/observations.ts Correct userId ownership checks on all mutations; list limit increase to 1000 is a minor memory concern
packages/database/drizzle/0010_seed_missing_metric_data.sql Idempotent inserts with NULL-safe functional unique index; backfill uses exact timestamp equality which may silently miss same-draw pairs
packages/database/drizzle/0011_consolidate_metric_codes.sql Clean metric code consolidation with matching optimal-range copies to canonical codes
packages/database/drizzle/0009_purple_dragon_man.sql Schema-only migration adding flagged_extractions table
packages/database/src/schema/observations.ts Schema unchanged; original value tracking fields already present
packages/database/src/seed/data/optimal-ranges.ts 40+ biomarker ranges added; alias codes are intentional for backward compatibility

Sequence Diagram

sequenceDiagram
    participant Page as LabDetailPage
    participant Hook as useDynamicStatus
    participant TRPC as tRPC Server
    participant DB as Database
    Page->>TRPC: observations.list({metricCode})
    Page->>TRPC: metrics.list()
    Page->>TRPC: optimalRanges.forUser({metricCode})
    TRPC->>DB: SELECT observations
    TRPC->>DB: SELECT metric_definitions
    TRPC->>DB: SELECT optimal_ranges
    DB-->>TRPC: rows
    TRPC-->>Hook: metricsData + optimalRangesData
    Hook->>Hook: useMemo builds rangesMap
    Hook-->>Page: getStatus, getRanges, isAbnormal
    Page->>Page: resultColumns useMemo captures getStatus
    Note over Page: BUG if displayPrecision unchanged on load
    Page->>Page: DataTable renders via captured getStatus
Loading

Reviews (1): Last reviewed commit: "feat: dynamic health status + canonical ..." | Re-trigger Greptile

},
],
[metricCode, displayPrecision],
[metricCode, displayPrecision, deleteMutation],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale getStatus closure in memoized columns

getStatus is captured in the cell callbacks for the value and status columns (lines 165 and 216) but is not listed in the useMemo dependency array. Because getStatus is not wrapped in useCallback inside useDynamicStatus, it is a new reference each render that closes over rangesMap. When metricsData or optimalRangesData loads asynchronously, a fresh getStatus is produced but resultColumns will not recompute unless displayPrecision also changes. For metrics where displayPrecision is null both before and after data loads (metric absent from metricsData or precision is null), the cells keep using the stale getStatus that sees an empty rangesMap and always returns normal. Status badges show green Normal for out-of-range values while the un-memoized getRowTint on line 519 correctly shows the warning tint.

Suggested change
[metricCode, displayPrecision, deleteMutation],
[metricCode, displayPrecision, deleteMutation, getStatus],

Comment on lines +244 to 251
<button
onClick={() => deleteMutation.mutate({ id: obs.id })}
className="rounded-md p-1 text-neutral-300 transition-all hover:bg-red-50 hover:text-red-500"
title="Remove"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unguarded permanent delete of health data

The Trash2 button fires deleteMutation.mutate directly on click with no confirmation step. The server-side delete procedure does a hard ctx.db.delete with no soft-delete or recycle bin. A single misclick irrecoverably removes a health observation. Consider adding a window.confirm or inline confirm state before triggering the mutation.

Comment on lines +161 to +176
FROM observations g
JOIN observations i
ON g.user_id = i.user_id
AND g.observed_at = i.observed_at
AND i.metric_code = 'insulin'
AND i.value_numeric IS NOT NULL
AND i.value_numeric > 0
WHERE g.metric_code = 'glucose'
AND g.value_numeric IS NOT NULL
AND g.value_numeric > 0
AND NOT EXISTS (
SELECT 1 FROM observations ex
WHERE ex.user_id = g.user_id
AND ex.metric_code = 'homa_ir'
AND ex.observed_at = g.observed_at
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Backfill joins on exact timestamp equality

All four derived-metric backfills join on g.observed_at = i.observed_at using exact timestamp equality. Lab systems sometimes stamp results from the same blood draw with timestamps seconds apart. Any such pair silently produces no derived observation. Consider widening to date-level equality (DATE(g.observed_at) = DATE(i.observed_at)) or documenting this constraint so operators know what to check if expected HOMA-IR rows are missing post-migration.

dateFrom: z.date().optional(),
dateTo: z.date().optional(),
status: z.string().optional(),
limit: z.number().min(1).max(1000).default(50),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 List limit raised from 200 to 1000

The detail page requests at most 200 observations so the higher ceiling is only reachable via direct API calls or future callers. A request for 1000 rows loads everything into memory at once with no streaming. The cursor-based pagination is already in place; consider keeping the ceiling at 200 and encouraging callers to page through results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant