mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
feat(measurements): add custom metrics (define, log, chart)
Add a user-defined custom-metric store alongside the built-in measurements on the Measurements surface. A user defines an arbitrary metric (name, unit, optional target window, decimals, description), logs values, and charts them. The store is a separate generic catalog + values pair, isolated from the closed measurement-type system: not synced, not in FHIR, not in insights — log and chart only. Names and units are plaintext, mirroring the biomarker catalog. The inline list lives beneath the measurement list (no new nav entry); tapping a metric opens a detail page with the trend chart, target band, stat strip, and an editable value history. Migration 0224 adds custom_metrics and custom_metric_entries.
This commit is contained in:
1 parent
150b827d01
commit
b3e6ba03d7
35 files changed
+4956
No files matched your search
@@ -9116,6 +9116,352 @@ paths:
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/custom-metrics:
|
||||
get:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: List the caller's custom metrics
|
||||
description: Returns every custom metric the caller has defined, name-ordered, each with its latest logged value and
|
||||
total value count.
|
||||
responses:
|
||||
"200":
|
||||
description: Custom-metric catalog.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ListCustomMetricsEnvelope"
|
||||
"401": *a1
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
post:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: Define a custom metric
|
||||
description: Creates a user-scoped custom metric. Re-creating a name that was previously soft-deleted revives that
|
||||
definition. Audits as `customMetric.create`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 120
|
||||
unit:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 40
|
||||
targetLow:
|
||||
type: number
|
||||
targetHigh:
|
||||
type: number
|
||||
decimals:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 6
|
||||
description:
|
||||
type: string
|
||||
maxLength: 2000
|
||||
required:
|
||||
- name
|
||||
- unit
|
||||
responses:
|
||||
"201":
|
||||
description: Created custom metric.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateCustomMetricResponse"
|
||||
"401": *a1
|
||||
"409": &a15
|
||||
description: A custom metric with this name already exists for the caller.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/custom-metrics/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: Fetch a single custom metric
|
||||
description: Returns the custom-metric definition. Cross-user rows surface as 404.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: Custom-metric detail.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/GetCustomMetricResponse"
|
||||
"401": *a1
|
||||
"404": &a14
|
||||
description: Custom metric not found (or owned by another user).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
patch:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: Edit a custom metric
|
||||
description: Partial edit; omitted fields are untouched, an explicit null clears a target bound / decimals /
|
||||
description. Audits as `customMetric.update`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 120
|
||||
unit:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 40
|
||||
targetLow:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
targetHigh:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
decimals:
|
||||
anyOf:
|
||||
- type: integer
|
||||
minimum: 0
|
||||
maximum: 6
|
||||
- type: "null"
|
||||
description:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 2000
|
||||
- type: "null"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated custom metric.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UpdateCustomMetricResponse"
|
||||
"401": *a1
|
||||
"404": *a14
|
||||
"409": *a15
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
delete:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: Delete a custom metric
|
||||
description: Soft-deletes the metric (stamps `deletedAt`); its logged values are retained and re-creating the name
|
||||
revives it. Audits as `customMetric.delete`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: Deletion succeeded.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteCustomMetricResponse"
|
||||
"401": *a1
|
||||
"404": *a14
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/custom-metrics/{id}/entries:
|
||||
get:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: List a custom metric's logged values
|
||||
description: Offset-paginated value feed for one custom metric (the chart + history read). Cross-user / unknown metric
|
||||
ids surface as 404.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 500
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 9007199254740991
|
||||
- in: query
|
||||
name: sortDir
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
responses:
|
||||
"200":
|
||||
description: Logged values.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ListCustomMetricEntriesEnvelope"
|
||||
"401": *a1
|
||||
"404": *a14
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
post:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: Log a value
|
||||
description: Records a value against the custom metric, snapshotting the metric's unit. Audits as `customMetricEntry.create`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: number
|
||||
measuredAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
|
||||
note:
|
||||
anyOf:
|
||||
- type: string
|
||||
maxLength: 2000
|
||||
- type: string
|
||||
const: ""
|
||||
required:
|
||||
- value
|
||||
- measuredAt
|
||||
responses:
|
||||
"201":
|
||||
description: Created value.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateCustomMetricEntryResponse"
|
||||
"401": *a1
|
||||
"404": *a14
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/custom-metrics/{id}/entries/{entryId}:
|
||||
patch:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: Edit a logged value
|
||||
description: Partial edit of a value; omitted fields untouched, explicit null clears `note`. Audits as
|
||||
`customMetricEntry.update`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: path
|
||||
name: entryId
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: number
|
||||
measuredAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
|
||||
note:
|
||||
anyOf:
|
||||
- anyOf:
|
||||
- type: string
|
||||
maxLength: 2000
|
||||
- type: string
|
||||
const: ""
|
||||
- type: "null"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated value.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UpdateCustomMetricEntryResponse"
|
||||
"401": *a1
|
||||
"404": &a16
|
||||
description: Custom metric entry not found (or owned by another user).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
delete:
|
||||
tags:
|
||||
- Custom metrics
|
||||
summary: Delete a logged value
|
||||
description: Hard-deletes the value. Audits as `customMetricEntry.delete`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: path
|
||||
name: entryId
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
description: Deletion succeeded.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/DeleteCustomMetricEntryResponse"
|
||||
"401": *a1
|
||||
"404": *a16
|
||||
"422": *a3
|
||||
"429": *a4
|
||||
/api/onboarding/disclaimer:
|
||||
post:
|
||||
tags:
|
||||
@@ -25527,6 +25873,297 @@ components:
|
||||
description: "The outcome per decision: `approved` (committed, with the new record ref), `rejected` (discarded),
|
||||
`needsReview` (refused — edit first, fail-closed), `failed` (a per-fact commit miss, e.g. a numeric observation
|
||||
with no unit)."
|
||||
ListCustomMetricsEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/ListCustomMetricsResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
ListCustomMetricsResponse:
|
||||
type: object
|
||||
properties:
|
||||
customMetrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CustomMetric"
|
||||
required:
|
||||
- customMetrics
|
||||
additionalProperties: false
|
||||
CustomMetric:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
targetLow:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
targetHigh:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
decimals:
|
||||
anyOf:
|
||||
- type: number
|
||||
- type: "null"
|
||||
description:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
latest:
|
||||
anyOf:
|
||||
- type: object
|
||||
properties:
|
||||
value:
|
||||
type: number
|
||||
unit:
|
||||
type: string
|
||||
measuredAt:
|
||||
type: string
|
||||
required:
|
||||
- value
|
||||
- unit
|
||||
- measuredAt
|
||||
additionalProperties: false
|
||||
- type: "null"
|
||||
entryCount:
|
||||
type: number
|
||||
createdAt:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- unit
|
||||
- targetLow
|
||||
- targetHigh
|
||||
- decimals
|
||||
- description
|
||||
- latest
|
||||
- entryCount
|
||||
- createdAt
|
||||
- updatedAt
|
||||
additionalProperties: false
|
||||
description: A user-defined custom metric. `latest` is the most recently logged value (or null); `entryCount` is the
|
||||
total logged values. The optional target window is the user's own good range, charted as a reference band.
|
||||
CreateCustomMetricResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/CustomMetric"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
GetCustomMetricResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/CustomMetric"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
UpdateCustomMetricResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/CustomMetric"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
DeleteCustomMetricResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
deleted:
|
||||
type: boolean
|
||||
required:
|
||||
- deleted
|
||||
additionalProperties: false
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
ListCustomMetricEntriesEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/ListCustomMetricEntriesResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
ListCustomMetricEntriesResponse:
|
||||
type: object
|
||||
properties:
|
||||
entries:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CustomMetricEntry"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: number
|
||||
limit:
|
||||
type: number
|
||||
offset:
|
||||
type: number
|
||||
required:
|
||||
- total
|
||||
- limit
|
||||
- offset
|
||||
additionalProperties: false
|
||||
required:
|
||||
- entries
|
||||
- meta
|
||||
additionalProperties: false
|
||||
CustomMetricEntry:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
customMetricId:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
unit:
|
||||
type: string
|
||||
measuredAt:
|
||||
type: string
|
||||
note:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
createdAt:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- customMetricId
|
||||
- value
|
||||
- unit
|
||||
- measuredAt
|
||||
- note
|
||||
- createdAt
|
||||
additionalProperties: false
|
||||
description: A single logged value for a custom metric. `unit` is a snapshot of the metric's unit at write time.
|
||||
CreateCustomMetricEntryResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/CustomMetricEntry"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
UpdateCustomMetricEntryResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/CustomMetricEntry"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
DeleteCustomMetricEntryResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
deleted:
|
||||
type: boolean
|
||||
required:
|
||||
- deleted
|
||||
additionalProperties: false
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
DisclaimerAckEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user