mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(insights): time-range pills with period-over-period deltas on metric pages
Add a 7d / 30d / 90d / 1y segmented control to every HealthKit metric page, between the stat strip and the chart, with a sentiment-coloured "vs prior period" delta caption beside it. New route GET /api/analytics/range?type=<MeasurementType>&range=<7d|30d|90d|1y> returns the current-window aggregate, the previous comparable window, and the composed delta. Single metric type by construction, so the read is one rollup-tier call covering the trailing 2N days sliced into the two halves — no per-type fan-out, no Prisma-pool burst. Reuses the WMY readers (readBestGranularityRollups + aggregateWmyBuckets) and the two-window pattern the year-ago baseline already uses; count/min/max/mean/sum compose across buckets, SD/slope/r2 stay out (not composable). The cached summaries slice is untouched so the hottest dashboard cache key never fragments. The delta caption reuses the dashboard's metric-aware up-good/up-bad colour logic — extracted from trend-card into a shared trend-sentiment module — with the metric's good direction sourced from the metric-status registry (target-band metrics render neutral, matching deviation framing). When the prior window has no data the caption shows "no prior-period data" rather than a misleading 0%. The range choice persists across metrics via a localStorage seam and defaults to 30d so nothing regresses. Cache keyed per (type, range) through the query-key factory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit eac87a9f8b144f4d8e7a8b950528a19d5dcf8726)
This commit is contained in:
1 parent
f1145c0e91
commit
03925ddece
21 files changed
+1151
-65
No files matched your search
@@ -30,6 +30,7 @@ tags:
|
||||
- name: Measurements
|
||||
- name: Medications
|
||||
- name: Mood
|
||||
- name: Analytics
|
||||
- name: Insights
|
||||
- name: Dashboard
|
||||
- name: Notifications
|
||||
@@ -1954,6 +1955,86 @@ paths:
|
||||
"401": *a1
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
/api/analytics/range:
|
||||
get:
|
||||
tags:
|
||||
- Analytics
|
||||
summary: Single-metric period-over-period range delta
|
||||
description: v1.9.0 — returns the current-window aggregate, the previous comparable window, and the composed delta for
|
||||
ONE metric type over a `7d` / `30d` / `90d` / `1y` range. Single-type by construction (the metric page is
|
||||
single-metric), so the read is one rollup-tier call covering the trailing 2N days sliced into the two halves —
|
||||
no per-type fan-out. Additive route; the `/api/analytics` envelope is unchanged. Auth via cookie or Bearer.
|
||||
parameters:
|
||||
- in: query
|
||||
name: type
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- WEIGHT
|
||||
- BLOOD_PRESSURE_SYS
|
||||
- BLOOD_PRESSURE_DIA
|
||||
- PULSE
|
||||
- BODY_FAT
|
||||
- SLEEP_DURATION
|
||||
- ACTIVITY_STEPS
|
||||
- BLOOD_GLUCOSE
|
||||
- TOTAL_BODY_WATER
|
||||
- BONE_MASS
|
||||
- OXYGEN_SATURATION
|
||||
- HEART_RATE_VARIABILITY
|
||||
- RESTING_HEART_RATE
|
||||
- ACTIVE_ENERGY_BURNED
|
||||
- FLIGHTS_CLIMBED
|
||||
- WALKING_RUNNING_DISTANCE
|
||||
- VO2_MAX
|
||||
- BODY_TEMPERATURE
|
||||
- FAT_FREE_MASS
|
||||
- FAT_MASS
|
||||
- MUSCLE_MASS
|
||||
- SKIN_TEMPERATURE
|
||||
- PULSE_WAVE_VELOCITY
|
||||
- VASCULAR_AGE
|
||||
- VISCERAL_FAT
|
||||
- AUDIO_EXPOSURE_ENV
|
||||
- AUDIO_EXPOSURE_HEADPHONE
|
||||
- TIME_IN_DAYLIGHT
|
||||
- WALKING_STEADINESS
|
||||
- AUDIO_EXPOSURE_EVENT
|
||||
- RESPIRATORY_RATE
|
||||
- BODY_MASS_INDEX
|
||||
- LEAN_BODY_MASS
|
||||
- WALKING_HEART_RATE_AVERAGE
|
||||
- WALKING_ASYMMETRY
|
||||
- WALKING_DOUBLE_SUPPORT
|
||||
- WALKING_STEP_LENGTH
|
||||
- WALKING_SPEED
|
||||
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
|
||||
required: true
|
||||
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
|
||||
- in: query
|
||||
name: range
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- 7d
|
||||
- 30d
|
||||
- 90d
|
||||
- 1y
|
||||
description: "Trailing window: `7d` / `30d` / `90d` / `1y`. The previous comparable window is the equally-sized span
|
||||
immediately before it."
|
||||
required: true
|
||||
description: "Trailing window: `7d` / `30d` / `90d` / `1y`. The previous comparable window is the equally-sized span
|
||||
immediately before it."
|
||||
responses:
|
||||
"200":
|
||||
description: Current + previous window aggregates and the delta.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AnalyticsRangeResponseEnvelope"
|
||||
"401": *a1
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
/api/insights/metric-status:
|
||||
get:
|
||||
tags:
|
||||
@@ -6334,6 +6415,107 @@ components:
|
||||
additionalProperties: false
|
||||
description: Acknowledgement that a full assessment warm was enqueued for the calling user. The generation runs out of
|
||||
band; the text lands in the read-only status routes.
|
||||
AnalyticsRangeResponseEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/AnalyticsRangeResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
AnalyticsRangeResponse:
|
||||
type: object
|
||||
properties:
|
||||
range:
|
||||
type: string
|
||||
enum:
|
||||
- 7d
|
||||
- 30d
|
||||
- 90d
|
||||
- 1y
|
||||
description: The range that was read (echoes the request).
|
||||
windowDays:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: Trailing-window length in days for the chosen range.
|
||||
granularity:
|
||||
type: string
|
||||
description: Rollup granularity the read resolved against (`DAY` / `WEEK` / `MONTH` / `YEAR`, or `none` on a coverage
|
||||
miss).
|
||||
current:
|
||||
description: Aggregate over the current window `[now-N, now)`.
|
||||
$ref: "#/components/schemas/AnalyticsWindowAggregate"
|
||||
previous:
|
||||
description: Aggregate over the previous comparable window `[now-2N, now-N)`.
|
||||
$ref: "#/components/schemas/AnalyticsWindowAggregate"
|
||||
delta:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
description: "`current.mean - previous.mean`; null when either window has no data (never a misleading 0)."
|
||||
deltaPct:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
description: "`delta / previous.mean` as a fraction (0.03 = +3 %); null when the prior window has no / zero mean (no
|
||||
divide-by-zero). The client shows 'no prior-period data' in that case."
|
||||
required:
|
||||
- range
|
||||
- windowDays
|
||||
- granularity
|
||||
- current
|
||||
- previous
|
||||
- delta
|
||||
- deltaPct
|
||||
additionalProperties: false
|
||||
description: Single-metric period-over-period aggregate. Reads the current and previous comparable windows from the WMY
|
||||
rollup tier and composes a count-weighted-mean delta. `count/min/max/mean/sum` are linearly composable across
|
||||
buckets; SD/slope/r² are intentionally excluded (not composable).
|
||||
AnalyticsWindowAggregate:
|
||||
type: object
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: Reading count composed across buckets.
|
||||
min:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
description: Window minimum; null when empty.
|
||||
max:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
description: Window maximum; null when empty.
|
||||
mean:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
description: Count-weighted mean across buckets; null when empty.
|
||||
sum:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
description: Cumulative total for cumulative metrics (steps, energy, distance); null when no bucket carries a sum.
|
||||
required:
|
||||
- count
|
||||
- min
|
||||
- max
|
||||
- mean
|
||||
- sum
|
||||
additionalProperties: false
|
||||
MetricStatusResponseEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user