trainbud 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -0
- package/CHANGELOG.md +675 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +182 -0
- package/README.md +322 -0
- package/dist/appDb.d.ts +83 -0
- package/dist/appDb.d.ts.map +1 -0
- package/dist/appDb.js +239 -0
- package/dist/appDb.js.map +1 -0
- package/dist/check.d.ts +12 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +230 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +457 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +61 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +241 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +1004 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/dashboardCharts.d.ts +99 -0
- package/dist/dashboardCharts.d.ts.map +1 -0
- package/dist/dashboardCharts.js +317 -0
- package/dist/dashboardCharts.js.map +1 -0
- package/dist/dashboardData.d.ts +89 -0
- package/dist/dashboardData.d.ts.map +1 -0
- package/dist/dashboardData.js +228 -0
- package/dist/dashboardData.js.map +1 -0
- package/dist/detect/baseline.d.ts +17 -0
- package/dist/detect/baseline.d.ts.map +1 -0
- package/dist/detect/baseline.js +62 -0
- package/dist/detect/baseline.js.map +1 -0
- package/dist/detect/countdown.d.ts +30 -0
- package/dist/detect/countdown.d.ts.map +1 -0
- package/dist/detect/countdown.js +79 -0
- package/dist/detect/countdown.js.map +1 -0
- package/dist/detect/detectors.d.ts +20 -0
- package/dist/detect/detectors.d.ts.map +1 -0
- package/dist/detect/detectors.js +291 -0
- package/dist/detect/detectors.js.map +1 -0
- package/dist/detect/findings.d.ts +29 -0
- package/dist/detect/findings.d.ts.map +1 -0
- package/dist/detect/findings.js +2 -0
- package/dist/detect/findings.js.map +1 -0
- package/dist/detect/forecast.d.ts +33 -0
- package/dist/detect/forecast.d.ts.map +1 -0
- package/dist/detect/forecast.js +122 -0
- package/dist/detect/forecast.js.map +1 -0
- package/dist/detect/index.d.ts +50 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +114 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/detect/sleepQuality.d.ts +21 -0
- package/dist/detect/sleepQuality.d.ts.map +1 -0
- package/dist/detect/sleepQuality.js +164 -0
- package/dist/detect/sleepQuality.js.map +1 -0
- package/dist/detect/trimp.d.ts +22 -0
- package/dist/detect/trimp.d.ts.map +1 -0
- package/dist/detect/trimp.js +77 -0
- package/dist/detect/trimp.js.map +1 -0
- package/dist/detect/week.d.ts +31 -0
- package/dist/detect/week.d.ts.map +1 -0
- package/dist/detect/week.js +184 -0
- package/dist/detect/week.js.map +1 -0
- package/dist/garmin/auth.d.ts +10 -0
- package/dist/garmin/auth.d.ts.map +1 -0
- package/dist/garmin/auth.js +81 -0
- package/dist/garmin/auth.js.map +1 -0
- package/dist/garmin/cache.d.ts +42 -0
- package/dist/garmin/cache.d.ts.map +1 -0
- package/dist/garmin/cache.js +141 -0
- package/dist/garmin/cache.js.map +1 -0
- package/dist/garmin/client.d.ts +22 -0
- package/dist/garmin/client.d.ts.map +1 -0
- package/dist/garmin/client.js +237 -0
- package/dist/garmin/client.js.map +1 -0
- package/dist/garmin/daily.d.ts +24 -0
- package/dist/garmin/daily.d.ts.map +1 -0
- package/dist/garmin/daily.js +96 -0
- package/dist/garmin/daily.js.map +1 -0
- package/dist/garmin/garminApiTypes.d.ts +70 -0
- package/dist/garmin/garminApiTypes.d.ts.map +1 -0
- package/dist/garmin/garminApiTypes.js +3 -0
- package/dist/garmin/garminApiTypes.js.map +1 -0
- package/dist/garmin/garminConnect.d.ts +17 -0
- package/dist/garmin/garminConnect.d.ts.map +1 -0
- package/dist/garmin/garminConnect.js +5 -0
- package/dist/garmin/garminConnect.js.map +1 -0
- package/dist/garmin/partial.d.ts +32 -0
- package/dist/garmin/partial.d.ts.map +1 -0
- package/dist/garmin/partial.js +38 -0
- package/dist/garmin/partial.js.map +1 -0
- package/dist/garmin/rawApi.d.ts +59 -0
- package/dist/garmin/rawApi.d.ts.map +1 -0
- package/dist/garmin/rawApi.js +113 -0
- package/dist/garmin/rawApi.js.map +1 -0
- package/dist/garmin/types.d.ts +85 -0
- package/dist/garmin/types.d.ts.map +1 -0
- package/dist/garmin/types.js +11 -0
- package/dist/garmin/types.js.map +1 -0
- package/dist/history/capture.d.ts +12 -0
- package/dist/history/capture.d.ts.map +1 -0
- package/dist/history/capture.js +63 -0
- package/dist/history/capture.js.map +1 -0
- package/dist/history/context.d.ts +51 -0
- package/dist/history/context.d.ts.map +1 -0
- package/dist/history/context.js +122 -0
- package/dist/history/context.js.map +1 -0
- package/dist/history/fallback.d.ts +72 -0
- package/dist/history/fallback.d.ts.map +1 -0
- package/dist/history/fallback.js +185 -0
- package/dist/history/fallback.js.map +1 -0
- package/dist/history/ingest.d.ts +44 -0
- package/dist/history/ingest.d.ts.map +1 -0
- package/dist/history/ingest.js +347 -0
- package/dist/history/ingest.js.map +1 -0
- package/dist/history/scheduler.d.ts +20 -0
- package/dist/history/scheduler.d.ts.map +1 -0
- package/dist/history/scheduler.js +54 -0
- package/dist/history/scheduler.js.map +1 -0
- package/dist/history/schema.d.ts +41 -0
- package/dist/history/schema.d.ts.map +1 -0
- package/dist/history/schema.js +91 -0
- package/dist/history/schema.js.map +1 -0
- package/dist/history/store.d.ts +112 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/store.js +282 -0
- package/dist/history/store.js.map +1 -0
- package/dist/httpServer.d.ts +55 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +1027 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/mcpConfig.d.ts +56 -0
- package/dist/mcpConfig.d.ts.map +1 -0
- package/dist/mcpConfig.js +106 -0
- package/dist/mcpConfig.js.map +1 -0
- package/dist/pairApi.d.ts +14 -0
- package/dist/pairApi.d.ts.map +1 -0
- package/dist/pairApi.js +38 -0
- package/dist/pairApi.js.map +1 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +105 -0
- package/dist/paths.js.map +1 -0
- package/dist/profile.d.ts +123 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +327 -0
- package/dist/profile.js.map +1 -0
- package/dist/promptApi.d.ts +43 -0
- package/dist/promptApi.d.ts.map +1 -0
- package/dist/promptApi.js +328 -0
- package/dist/promptApi.js.map +1 -0
- package/dist/promptSuggestions.d.ts +16 -0
- package/dist/promptSuggestions.d.ts.map +1 -0
- package/dist/promptSuggestions.js +141 -0
- package/dist/promptSuggestions.js.map +1 -0
- package/dist/selfTest.d.ts +53 -0
- package/dist/selfTest.d.ts.map +1 -0
- package/dist/selfTest.js +225 -0
- package/dist/selfTest.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +67 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +210 -0
- package/dist/setup.js.map +1 -0
- package/dist/toolErrors.d.ts +2 -0
- package/dist/toolErrors.d.ts.map +1 -0
- package/dist/toolErrors.js +18 -0
- package/dist/toolErrors.js.map +1 -0
- package/dist/tools/activities.d.ts +18 -0
- package/dist/tools/activities.d.ts.map +1 -0
- package/dist/tools/activities.js +182 -0
- package/dist/tools/activities.js.map +1 -0
- package/dist/tools/bodyComposition.d.ts +10 -0
- package/dist/tools/bodyComposition.d.ts.map +1 -0
- package/dist/tools/bodyComposition.js +139 -0
- package/dist/tools/bodyComposition.js.map +1 -0
- package/dist/tools/context.d.ts +8 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +148 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/findings.d.ts +7 -0
- package/dist/tools/findings.d.ts.map +1 -0
- package/dist/tools/findings.js +51 -0
- package/dist/tools/findings.js.map +1 -0
- package/dist/tools/heartRate.d.ts +10 -0
- package/dist/tools/heartRate.d.ts.map +1 -0
- package/dist/tools/heartRate.js +105 -0
- package/dist/tools/heartRate.js.map +1 -0
- package/dist/tools/index.d.ts +66 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +101 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/payloads.d.ts +148 -0
- package/dist/tools/payloads.d.ts.map +1 -0
- package/dist/tools/payloads.js +2 -0
- package/dist/tools/payloads.js.map +1 -0
- package/dist/tools/recovery.d.ts +31 -0
- package/dist/tools/recovery.d.ts.map +1 -0
- package/dist/tools/recovery.js +350 -0
- package/dist/tools/recovery.js.map +1 -0
- package/dist/tools/sleep.d.ts +10 -0
- package/dist/tools/sleep.d.ts.map +1 -0
- package/dist/tools/sleep.js +124 -0
- package/dist/tools/sleep.js.map +1 -0
- package/dist/tools/stress.d.ts +11 -0
- package/dist/tools/stress.d.ts.map +1 -0
- package/dist/tools/stress.js +108 -0
- package/dist/tools/stress.js.map +1 -0
- package/dist/tools/trainingInsights.d.ts +15 -0
- package/dist/tools/trainingInsights.d.ts.map +1 -0
- package/dist/tools/trainingInsights.js +79 -0
- package/dist/tools/trainingInsights.js.map +1 -0
- package/dist/tools/types.d.ts +12 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/vo2Max.d.ts +11 -0
- package/dist/tools/vo2Max.d.ts.map +1 -0
- package/dist/tools/vo2Max.js +110 -0
- package/dist/tools/vo2Max.js.map +1 -0
- package/dist/tools/week.d.ts +12 -0
- package/dist/tools/week.d.ts.map +1 -0
- package/dist/tools/week.js +74 -0
- package/dist/tools/week.js.map +1 -0
- package/dist/usage.d.ts +104 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +247 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/batch.d.ts +2 -0
- package/dist/utils/batch.d.ts.map +1 -0
- package/dist/utils/batch.js +18 -0
- package/dist/utils/batch.js.map +1 -0
- package/dist/utils/helpers.d.ts +32 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +188 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/jsonFile.d.ts +19 -0
- package/dist/utils/jsonFile.d.ts.map +1 -0
- package/dist/utils/jsonFile.js +24 -0
- package/dist/utils/jsonFile.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +97 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/secretFile.d.ts +15 -0
- package/dist/utils/secretFile.d.ts.map +1 -0
- package/dist/utils/secretFile.js +47 -0
- package/dist/utils/secretFile.js.map +1 -0
- package/dist/utils/stdio.d.ts +10 -0
- package/dist/utils/stdio.d.ts.map +1 -0
- package/dist/utils/stdio.js +70 -0
- package/dist/utils/stdio.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/dist/watchApi.d.ts +264 -0
- package/dist/watchApi.d.ts.map +1 -0
- package/dist/watchApi.js +333 -0
- package/dist/watchApi.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import { getPendingPairings } from "./pairApi.js";
|
|
3
|
+
import { activeContext, upcomingContext } from "./history/context.js";
|
|
4
|
+
import { appConfig } from "./config.js";
|
|
5
|
+
import { isAiConfigured, getCachedDailyInsight } from "./promptApi.js";
|
|
6
|
+
import { getMetricSeries } from "./history/store.js";
|
|
7
|
+
import { buildDetectorInput, describeFindingsCoverage, runDetectors } from "./detect/index.js";
|
|
8
|
+
import { buildWeekReview } from "./detect/week.js";
|
|
9
|
+
import { nextRace } from "./detect/countdown.js";
|
|
10
|
+
import { median } from "./detect/baseline.js";
|
|
11
|
+
import { getProfile, stateFor } from "./profile.js";
|
|
12
|
+
import { budgetState, dailyAiSpend, featureCounts, monthToDateSpend, } from "./usage.js";
|
|
13
|
+
import { densify } from "./dashboardCharts.js";
|
|
14
|
+
// SECTION: Dashboard data
|
|
15
|
+
//
|
|
16
|
+
// Everything the dashboard draws, assembled from local SQLite and nothing else.
|
|
17
|
+
//
|
|
18
|
+
// That is a deliberate constraint, not a shortcut. The old page needed no data
|
|
19
|
+
// beyond a pairing list, so nobody had to decide; a training dashboard could
|
|
20
|
+
// easily have reached for the same six Garmin calls the watch makes, and the
|
|
21
|
+
// page would then take as long to paint as Connect takes to answer, fail when
|
|
22
|
+
// the session expires, and spend rate limit on every refresh. Every number
|
|
23
|
+
// below is already in the history store, which is the whole reason the memory
|
|
24
|
+
// layer exists.
|
|
25
|
+
//
|
|
26
|
+
// The one number that is NOT here is the recovery score, because Garmin
|
|
27
|
+
// computes it and never stores it. Rather than block the page on one call, the
|
|
28
|
+
// dashboard shows what this machine actually knows -- which is also the half
|
|
29
|
+
// Connect itself cannot show the user, since it does not compare anything to
|
|
30
|
+
// their own baseline.
|
|
31
|
+
const TREND_DAYS = 30;
|
|
32
|
+
function seriesFor(kind, days, transform) {
|
|
33
|
+
const today = DateTime.local();
|
|
34
|
+
const start = today.minus({ days: days - 1 }).toISODate() ?? "";
|
|
35
|
+
const end = today.toISODate() ?? "";
|
|
36
|
+
const raw = getMetricSeries(kind, start, end).map((point) => ({
|
|
37
|
+
date: point.date,
|
|
38
|
+
value: transform ? transform(point.value) : point.value,
|
|
39
|
+
}));
|
|
40
|
+
const values = raw.map((point) => point.value);
|
|
41
|
+
return {
|
|
42
|
+
points: densify(raw, days, today),
|
|
43
|
+
// A median, not a mean: one unrecorded-looking outlier should not move the
|
|
44
|
+
// line the user is being compared against.
|
|
45
|
+
baseline: median(values),
|
|
46
|
+
unit: "",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function round(value, places = 1) {
|
|
50
|
+
const factor = 10 ** places;
|
|
51
|
+
return Math.round(value * factor) / factor;
|
|
52
|
+
}
|
|
53
|
+
/** The newest measured value in a densified series, or null. */
|
|
54
|
+
function latest(points) {
|
|
55
|
+
return latestPoint(points)?.value ?? null;
|
|
56
|
+
}
|
|
57
|
+
/** The newest measured point, with its date, or null. */
|
|
58
|
+
function latestPoint(points) {
|
|
59
|
+
for (let i = points.length - 1; i >= 0; i -= 1) {
|
|
60
|
+
const point = points[i];
|
|
61
|
+
if (point && point.value !== null && point.value !== undefined) {
|
|
62
|
+
return { date: point.date, value: point.value };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* What to call the most recent night.
|
|
69
|
+
*
|
|
70
|
+
* "Last night" is a claim about a date, and the store's newest night is only
|
|
71
|
+
* last night if it was recorded. Read at two in the morning the newest row is
|
|
72
|
+
* the night BEFORE last, and the tile said "Last night" over it -- the same
|
|
73
|
+
* fault as the sleep card calling a fortnight-old night "last night", which
|
|
74
|
+
* this project has already fixed once on the watch.
|
|
75
|
+
*/
|
|
76
|
+
export function nightLabel(date, today = DateTime.local()) {
|
|
77
|
+
if (date === null) {
|
|
78
|
+
return "Last night";
|
|
79
|
+
}
|
|
80
|
+
const recorded = DateTime.fromISO(date).startOf("day");
|
|
81
|
+
const yesterday = today.startOf("day").minus({ days: 1 });
|
|
82
|
+
if (!recorded.isValid || recorded >= yesterday) {
|
|
83
|
+
return "Last night";
|
|
84
|
+
}
|
|
85
|
+
return `Night of ${recorded.toFormat("d LLL")}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The four tiles.
|
|
89
|
+
*
|
|
90
|
+
* Every one of them carries a comparison, because the number on its own is
|
|
91
|
+
* what Connect already shows on the same wrist. "58 bpm" is a fact; "58 bpm,
|
|
92
|
+
* 2 above your 30-day median" is the only version that tells the user
|
|
93
|
+
* anything they could act on.
|
|
94
|
+
*/
|
|
95
|
+
function buildTiles(profile, trends, hrv) {
|
|
96
|
+
const tiles = [];
|
|
97
|
+
const sleepPoint = latestPoint(trends.sleep.points);
|
|
98
|
+
const sleep = sleepPoint?.value ?? null;
|
|
99
|
+
tiles.push({
|
|
100
|
+
key: "sleep",
|
|
101
|
+
label: nightLabel(sleepPoint?.date ?? null),
|
|
102
|
+
value: sleep === null ? "—" : `${round(sleep)} h`,
|
|
103
|
+
note: trends.sleep.baseline === null || sleep === null
|
|
104
|
+
? null
|
|
105
|
+
: `${sleep >= trends.sleep.baseline ? "+" : ""}${round(sleep - trends.sleep.baseline)} h vs your usual`,
|
|
106
|
+
state: stateFor("sleepHours", sleep, profile),
|
|
107
|
+
});
|
|
108
|
+
const rhr = latest(trends.restingHr.points);
|
|
109
|
+
const rhrDelta = rhr === null || trends.restingHr.baseline === null ? null : round(rhr - trends.restingHr.baseline);
|
|
110
|
+
tiles.push({
|
|
111
|
+
key: "resting_hr",
|
|
112
|
+
label: "Resting HR",
|
|
113
|
+
value: rhr === null ? "—" : `${Math.round(rhr)} bpm`,
|
|
114
|
+
note: rhrDelta === null ? null : `${rhrDelta >= 0 ? "+" : ""}${rhrDelta} vs your median`,
|
|
115
|
+
// Graded on the distance from this person's own median, never the rate.
|
|
116
|
+
state: stateFor("restingHrDelta", rhrDelta, profile),
|
|
117
|
+
});
|
|
118
|
+
const stress = latest(trends.stress.points);
|
|
119
|
+
tiles.push({
|
|
120
|
+
key: "stress",
|
|
121
|
+
label: "Stress",
|
|
122
|
+
value: stress === null ? "—" : String(Math.round(stress)),
|
|
123
|
+
note: trends.stress.baseline === null || stress === null
|
|
124
|
+
? null
|
|
125
|
+
: `median ${Math.round(trends.stress.baseline)}`,
|
|
126
|
+
state: stateFor("stress", stress, profile),
|
|
127
|
+
});
|
|
128
|
+
const hrvNow = latest(hrv.points);
|
|
129
|
+
tiles.push({
|
|
130
|
+
key: "hrv",
|
|
131
|
+
label: "HRV",
|
|
132
|
+
value: hrvNow === null ? "—" : `${Math.round(hrvNow)} ms`,
|
|
133
|
+
note: hrv.baseline === null || hrvNow === null ? null : `median ${Math.round(hrv.baseline)}`,
|
|
134
|
+
// HRV has no user-facing band: it is meaningful only as a trend, and a
|
|
135
|
+
// green or red HRV number would be a claim this product cannot support.
|
|
136
|
+
state: "unknown",
|
|
137
|
+
});
|
|
138
|
+
return tiles;
|
|
139
|
+
}
|
|
140
|
+
export function getDashboardStatus(publicUrl) {
|
|
141
|
+
const now = Math.floor(Date.now() / 1000);
|
|
142
|
+
const today = DateTime.local().toISODate() ?? "";
|
|
143
|
+
return {
|
|
144
|
+
pending: getPendingPairings().map((token) => ({
|
|
145
|
+
code: token.code,
|
|
146
|
+
expires_in: Math.max(0, token.expires_at - now),
|
|
147
|
+
})),
|
|
148
|
+
ai_configured: isAiConfigured(),
|
|
149
|
+
insight: getCachedDailyInsight(),
|
|
150
|
+
public_url: publicUrl ?? appConfig.publicUrl,
|
|
151
|
+
context: [...activeContext(today), ...upcomingContext(today)].map((entry) => ({
|
|
152
|
+
id: entry.id,
|
|
153
|
+
kind: entry.kind,
|
|
154
|
+
text: entry.text,
|
|
155
|
+
effective_from: entry.effectiveFrom,
|
|
156
|
+
effective_to: entry.effectiveTo,
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export function getDashboardData(publicUrl) {
|
|
161
|
+
const status = getDashboardStatus(publicUrl);
|
|
162
|
+
const profile = getProfile();
|
|
163
|
+
const today = DateTime.local().toISODate() ?? "";
|
|
164
|
+
const detection = runDetectors();
|
|
165
|
+
const coverage = describeFindingsCoverage(detection.coverage);
|
|
166
|
+
const trends = {
|
|
167
|
+
restingHr: { ...seriesFor("resting_hr", TREND_DAYS), unit: " bpm" },
|
|
168
|
+
sleep: { ...seriesFor("sleep_seconds", TREND_DAYS, (value) => value / 3600), unit: " h" },
|
|
169
|
+
stress: { ...seriesFor("stress_avg", TREND_DAYS), unit: "" },
|
|
170
|
+
};
|
|
171
|
+
const hrv = seriesFor("hrv_overnight", TREND_DAYS);
|
|
172
|
+
const review = buildWeekReview(buildDetectorInput());
|
|
173
|
+
const rows = [
|
|
174
|
+
{ label: "Sessions", unit: "", current: review.sessions, previous: review.previousSessions },
|
|
175
|
+
{
|
|
176
|
+
label: "Moving",
|
|
177
|
+
unit: " min",
|
|
178
|
+
current: review.movingMinutes,
|
|
179
|
+
previous: review.previousMovingMinutes,
|
|
180
|
+
},
|
|
181
|
+
...review.metrics.map((metric) => ({
|
|
182
|
+
label: metric.label,
|
|
183
|
+
unit: metric.unit === "TRIMP" ? "" : metric.unit,
|
|
184
|
+
current: metric.current,
|
|
185
|
+
previous: metric.previous,
|
|
186
|
+
})),
|
|
187
|
+
];
|
|
188
|
+
const race = nextRace([...activeContext(today), ...upcomingContext(today)], today);
|
|
189
|
+
return {
|
|
190
|
+
...status,
|
|
191
|
+
profile,
|
|
192
|
+
tiles: buildTiles(profile, trends, hrv),
|
|
193
|
+
findings: detection.findings.map((finding) => ({
|
|
194
|
+
severity: finding.severity,
|
|
195
|
+
headline: finding.headline,
|
|
196
|
+
detail: finding.detail,
|
|
197
|
+
})),
|
|
198
|
+
// An empty findings list means two opposite things, and the page has to say
|
|
199
|
+
// which. "Nothing stands out" is a clean bill of health; "the record stops
|
|
200
|
+
// three weeks ago" is a claim about days nobody measured.
|
|
201
|
+
coverageNote: coverage.state === "ready"
|
|
202
|
+
? null
|
|
203
|
+
: coverage.fix
|
|
204
|
+
? `${coverage.detail} ${coverage.fix}`
|
|
205
|
+
: coverage.detail,
|
|
206
|
+
coverageDays: detection.coverage.days,
|
|
207
|
+
coverageThrough: detection.coverage.throughDate,
|
|
208
|
+
week: {
|
|
209
|
+
ready: review.ready,
|
|
210
|
+
headline: review.headline,
|
|
211
|
+
rows,
|
|
212
|
+
forecastRatio: review.forecast.projectedRatio,
|
|
213
|
+
forecastVerdict: review.forecast.verdict,
|
|
214
|
+
sleepDebtHours: review.sleep.debtHours,
|
|
215
|
+
},
|
|
216
|
+
trends,
|
|
217
|
+
race: race ? { text: race.text, daysAway: race.daysAway, phase: race.phase } : null,
|
|
218
|
+
usage: {
|
|
219
|
+
month: monthToDateSpend(),
|
|
220
|
+
budget: budgetState(),
|
|
221
|
+
daily: dailyAiSpend(TREND_DAYS),
|
|
222
|
+
features: featureCounts(TREND_DAYS),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Tests only. */
|
|
227
|
+
export const __nightLabelForTests = nightLabel;
|
|
228
|
+
//# sourceMappingURL=dashboardData.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboardData.js","sourceRoot":"","sources":["../src/dashboardData.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAA0C,MAAM,cAAc,CAAC;AAC5F,OAAO,EACL,WAAW,EACX,YAAY,EACZ,aAAa,EACb,gBAAgB,GAKjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAsC,MAAM,sBAAsB,CAAC;AAGnF,0BAA0B;AAC1B,EAAE;AACF,gFAAgF;AAChF,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAC9E,gBAAgB;AAChB,EAAE;AACF,wEAAwE;AACxE,+EAA+E;AAC/E,6EAA6E;AAC7E,6EAA6E;AAC7E,sBAAsB;AAEtB,MAAM,UAAU,GAAG,EAAE,CAAC;AAoEtB,SAAS,SAAS,CAAC,IAAgB,EAAE,IAAY,EAAE,SAAqC;IACtF,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IAChE,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IAEpC,MAAM,GAAG,GAAG,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK;KACxD,CAAC,CAAC,CAAC;IAEJ,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC;QACjC,2EAA2E;QAC3E,2CAA2C;QAC3C,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC;QACxB,IAAI,EAAE,EAAE;KACT,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,KAAa,EAAE,MAAM,GAAG,CAAC;IACtC,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM,CAAC;IAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC;AAC7C,CAAC;AAED,gEAAgE;AAChE,SAAS,MAAM,CAAC,MAAqB;IACnC,OAAO,WAAW,CAAC,MAAM,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;AAC5C,CAAC;AAED,yDAAyD;AACzD,SAAS,WAAW,CAAC,MAAqB;IACxC,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/D,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;QAClD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,IAAmB,EAAE,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE;IACtE,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1D,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;QAC/C,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,OAAO,YAAY,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CACjB,OAAwB,EACxB,MAAoF,EACpF,GAAmB;IAEnB,MAAM,KAAK,GAAoB,EAAE,CAAC;IAElC,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,UAAU,EAAE,KAAK,IAAI,IAAI,CAAC;IACxC,KAAK,CAAC,IAAI,CAAC;QACT,GAAG,EAAE,OAAO;QACZ,KAAK,EAAE,UAAU,CAAC,UAAU,EAAE,IAAI,IAAI,IAAI,CAAC;QAC3C,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI;QACjD,IAAI,EACF,MAAM,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI;YAC9C,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,GAAG,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,kBAAkB;QAC3G,KAAK,EAAE,QAAQ,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,CAAC;KAC9C,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,QAAQ,GACZ,GAAG,KAAK,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACrG,KAAK,CAAC,IAAI,CAAC;QACT,GAAG,EAAE,YAAY;QACjB,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM;QACpD,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,iBAAiB;QACxF,wEAAwE;QACxE,KAAK,EAAE,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,EAAE,OAAO,CAAC;KACrD,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,CAAC,IAAI,CAAC;QACT,GAAG,EAAE,QAAQ;QACb,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACzD,IAAI,EACF,MAAM,CAAC,MAAM,CAAC,QAAQ,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI;YAChD,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE;QACpD,KAAK,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC;KAC3C,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAClC,KAAK,CAAC,IAAI,CAAC;QACT,GAAG,EAAE,KAAK;QACV,KAAK,EAAE,KAAK;QACZ,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK;QACzD,IAAI,EAAE,GAAG,CAAC,QAAQ,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;QAC5F,uEAAuE;QACvE,wEAAwE;QACxE,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,SAAkB;IACnD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IACjD,OAAO;QACL,OAAO,EAAE,kBAAkB,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC5C,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,GAAG,CAAC;SAChD,CAAC,CAAC;QACH,aAAa,EAAE,cAAc,EAAE;QAC/B,OAAO,EAAE,qBAAqB,EAAE;QAChC,UAAU,EAAE,SAAS,IAAI,SAAS,CAAC,SAAS;QAC5C,OAAO,EAAE,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,EAAE,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC5E,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,cAAc,EAAE,KAAK,CAAC,aAAa;YACnC,YAAY,EAAE,KAAK,CAAC,WAAW;SAChC,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAkB;IACjD,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IAEjD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,MAAM,QAAQ,GAAG,wBAAwB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAE9D,MAAM,MAAM,GAAG;QACb,SAAS,EAAE,EAAE,GAAG,SAAS,CAAC,YAAY,EAAE,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE;QACnE,KAAK,EAAE,EAAE,GAAG,SAAS,CAAC,eAAe,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE;QACzF,MAAM,EAAE,EAAE,GAAG,SAAS,CAAC,YAAY,EAAE,UAAU,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE;KAC7D,CAAC;IACF,MAAM,GAAG,GAAG,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IAEnD,MAAM,MAAM,GAAG,eAAe,CAAC,kBAAkB,EAAE,CAAC,CAAC;IACrD,MAAM,IAAI,GAAkB;QAC1B,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,gBAAgB,EAAE;QAC5F;YACE,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,MAAM,CAAC,aAAa;YAC7B,QAAQ,EAAE,MAAM,CAAC,qBAAqB;SACvC;QACD,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACjC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI;YAChD,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC,CAAC;KACJ,CAAC;IAEF,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,EAAE,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAEnF,OAAO;QACL,GAAG,MAAM;QACT,OAAO;QACP,KAAK,EAAE,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC;QACvC,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAC7C,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;QACH,4EAA4E;QAC5E,2EAA2E;QAC3E,0DAA0D;QAC1D,YAAY,EACV,QAAQ,CAAC,KAAK,KAAK,OAAO;YACxB,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,QAAQ,CAAC,GAAG;gBACZ,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,GAAG,EAAE;gBACtC,CAAC,CAAC,QAAQ,CAAC,MAAM;QACvB,YAAY,EAAE,SAAS,CAAC,QAAQ,CAAC,IAAI;QACrC,eAAe,EAAE,SAAS,CAAC,QAAQ,CAAC,WAAW;QAC/C,IAAI,EAAE;YACJ,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI;YACJ,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,cAAc;YAC7C,eAAe,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO;YACxC,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS;SACvC;QACD,MAAM;QACN,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI;QACnF,KAAK,EAAE;YACL,KAAK,EAAE,gBAAgB,EAAE;YACzB,MAAM,EAAE,WAAW,EAAE;YACrB,KAAK,EAAE,YAAY,CAAC,UAAU,CAAC;YAC/B,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC;SACpC;KACF,CAAC;AACJ,CAAC;AAED,kBAAkB;AAClB,MAAM,CAAC,MAAM,oBAAoB,GAAG,UAAU,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MetricPoint } from "../history/store.js";
|
|
2
|
+
export interface Baseline {
|
|
3
|
+
median: number;
|
|
4
|
+
mad: number;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function median(values: number[]): number | null;
|
|
8
|
+
export declare function buildBaseline(points: MetricPoint[], minimumCount?: number): Baseline | null;
|
|
9
|
+
/**
|
|
10
|
+
* Returns null when the series never varies. A MAD of zero divides to Infinity,
|
|
11
|
+
* which compares true against every threshold and would fire every detector at
|
|
12
|
+
* once on the most stable data in the store -- the exact opposite of what the
|
|
13
|
+
* data says.
|
|
14
|
+
*/
|
|
15
|
+
export declare function robustZ(value: number, baseline: Baseline): number | null;
|
|
16
|
+
export declare function meanOf(points: MetricPoint[]): number | null;
|
|
17
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/detect/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAoBvD,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CActD;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,WAAW,EAAE,EACrB,YAAY,SAAwB,GACnC,QAAQ,GAAG,IAAI,CAmBjB;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAMxE;AAED,wBAAgB,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,IAAI,CAQ3D"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SECTION: Robust baselines
|
|
2
|
+
//
|
|
3
|
+
// Every detector compares a recent value against the user's own history rather
|
|
4
|
+
// than against a population figure, so the shape of "normal" has to come from
|
|
5
|
+
// the series itself.
|
|
6
|
+
//
|
|
7
|
+
// Median and median-absolute-deviation, not mean and standard deviation. Both
|
|
8
|
+
// of those are dragged by exactly the outlier a detector most wants to notice:
|
|
9
|
+
// one 11-hour recovery sleep after a race lifts the mean and inflates the
|
|
10
|
+
// deviation, so the bar rises and the next three short nights read as normal.
|
|
11
|
+
// The median ignores it.
|
|
12
|
+
/** Makes MAD comparable to a standard deviation for normally distributed data. */
|
|
13
|
+
const MAD_TO_SIGMA = 0.6745;
|
|
14
|
+
/** Two weeks is the least that describes a person rather than a fortnight's mood. */
|
|
15
|
+
const DEFAULT_MINIMUM_COUNT = 14;
|
|
16
|
+
export function median(values) {
|
|
17
|
+
const sorted = values.filter((value) => Number.isFinite(value)).sort((a, b) => a - b);
|
|
18
|
+
if (sorted.length === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const middle = Math.floor(sorted.length / 2);
|
|
22
|
+
if (sorted.length % 2 === 1) {
|
|
23
|
+
return sorted[middle] ?? null;
|
|
24
|
+
}
|
|
25
|
+
return ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
26
|
+
}
|
|
27
|
+
export function buildBaseline(points, minimumCount = DEFAULT_MINIMUM_COUNT) {
|
|
28
|
+
const values = points.map((point) => point.value).filter((value) => Number.isFinite(value));
|
|
29
|
+
if (values.length < minimumCount) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const centre = median(values);
|
|
33
|
+
if (centre === null) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const deviations = values.map((value) => Math.abs(value - centre));
|
|
37
|
+
return {
|
|
38
|
+
median: centre,
|
|
39
|
+
mad: median(deviations) ?? 0,
|
|
40
|
+
count: values.length,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns null when the series never varies. A MAD of zero divides to Infinity,
|
|
45
|
+
* which compares true against every threshold and would fire every detector at
|
|
46
|
+
* once on the most stable data in the store -- the exact opposite of what the
|
|
47
|
+
* data says.
|
|
48
|
+
*/
|
|
49
|
+
export function robustZ(value, baseline) {
|
|
50
|
+
if (baseline.mad <= 0 || !Number.isFinite(value)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return (MAD_TO_SIGMA * (value - baseline.median)) / baseline.mad;
|
|
54
|
+
}
|
|
55
|
+
export function meanOf(points) {
|
|
56
|
+
const values = points.map((point) => point.value).filter((value) => Number.isFinite(value));
|
|
57
|
+
if (values.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/detect/baseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ContextEntry } from "../history/context.js";
|
|
2
|
+
/** How the current week relates to the event. */
|
|
3
|
+
export type RacePhase = "race_week" | "taper" | "build" | "far_out";
|
|
4
|
+
export interface RaceCountdown {
|
|
5
|
+
text: string;
|
|
6
|
+
date: string;
|
|
7
|
+
daysAway: number;
|
|
8
|
+
phase: RacePhase;
|
|
9
|
+
/** Short enough for a watch card. */
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The next race on or after `today`, nearest first.
|
|
14
|
+
*
|
|
15
|
+
* A race entry's effective_from is the day of the event. Entries whose date has
|
|
16
|
+
* passed are not returned -- but they are deliberately not deleted either, so
|
|
17
|
+
* "how did the last build go" still has something to read.
|
|
18
|
+
*
|
|
19
|
+
* A race with no parseable date is skipped rather than guessed at. Showing a
|
|
20
|
+
* countdown to the wrong day is worse than showing none: the whole point of the
|
|
21
|
+
* number is that the user stops doing their own arithmetic.
|
|
22
|
+
*/
|
|
23
|
+
export declare function nextRace(entries: ContextEntry[], today: string): RaceCountdown | null;
|
|
24
|
+
/**
|
|
25
|
+
* One sentence for the model's context. Says what the phase means for training
|
|
26
|
+
* and nothing about how the user should feel or whether they are ready --
|
|
27
|
+
* neither is knowable from a date.
|
|
28
|
+
*/
|
|
29
|
+
export declare function describeRace(race: RaceCountdown | null): string | null;
|
|
30
|
+
//# sourceMappingURL=countdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"countdown.d.ts","sourceRoot":"","sources":["../../src/detect/countdown.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAa1D,iDAAiD;AACjD,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAEpE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;CACf;AAeD;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAwCrF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAiBtE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
const TAPER_DAYS = 21;
|
|
3
|
+
const FAR_OUT_DAYS = 84;
|
|
4
|
+
function phaseFor(daysAway) {
|
|
5
|
+
if (daysAway <= 7) {
|
|
6
|
+
return "race_week";
|
|
7
|
+
}
|
|
8
|
+
if (daysAway <= TAPER_DAYS) {
|
|
9
|
+
return "taper";
|
|
10
|
+
}
|
|
11
|
+
return daysAway <= FAR_OUT_DAYS ? "build" : "far_out";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The next race on or after `today`, nearest first.
|
|
15
|
+
*
|
|
16
|
+
* A race entry's effective_from is the day of the event. Entries whose date has
|
|
17
|
+
* passed are not returned -- but they are deliberately not deleted either, so
|
|
18
|
+
* "how did the last build go" still has something to read.
|
|
19
|
+
*
|
|
20
|
+
* A race with no parseable date is skipped rather than guessed at. Showing a
|
|
21
|
+
* countdown to the wrong day is worse than showing none: the whole point of the
|
|
22
|
+
* number is that the user stops doing their own arithmetic.
|
|
23
|
+
*/
|
|
24
|
+
export function nextRace(entries, today) {
|
|
25
|
+
const from = DateTime.fromISO(today).startOf("day");
|
|
26
|
+
if (!from.isValid) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
let best = null;
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (entry.kind !== "race") {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const date = DateTime.fromISO(entry.effectiveFrom).startOf("day");
|
|
35
|
+
if (!date.isValid) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const daysAway = Math.round(date.diff(from, "days").days);
|
|
39
|
+
if (daysAway < 0) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (best === null || daysAway < best.daysAway) {
|
|
43
|
+
best = {
|
|
44
|
+
text: entry.text,
|
|
45
|
+
date: entry.effectiveFrom,
|
|
46
|
+
daysAway,
|
|
47
|
+
phase: phaseFor(daysAway),
|
|
48
|
+
label: daysAway === 0
|
|
49
|
+
? "Today"
|
|
50
|
+
: daysAway === 1
|
|
51
|
+
? "Tomorrow"
|
|
52
|
+
: `${daysAway} days`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return best;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One sentence for the model's context. Says what the phase means for training
|
|
60
|
+
* and nothing about how the user should feel or whether they are ready --
|
|
61
|
+
* neither is knowable from a date.
|
|
62
|
+
*/
|
|
63
|
+
export function describeRace(race) {
|
|
64
|
+
if (!race) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const lead = `${race.text} is in ${race.daysAway} day${race.daysAway === 1 ? "" : "s"} (${race.date}).`;
|
|
68
|
+
switch (race.phase) {
|
|
69
|
+
case "race_week":
|
|
70
|
+
return `${lead} This is race week: load should be coming down, and a light week is the plan rather than a lapse.`;
|
|
71
|
+
case "taper":
|
|
72
|
+
return `${lead} This is the taper window, so a falling load ratio is intended.`;
|
|
73
|
+
case "build":
|
|
74
|
+
return `${lead} Still in the build, so load should be climbing gradually rather than holding flat.`;
|
|
75
|
+
default:
|
|
76
|
+
return `${lead} Far enough out that this week's training is general rather than race-specific.`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=countdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"countdown.js","sourceRoot":"","sources":["../../src/detect/countdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AA0BjC,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,YAAY,GAAG,EAAE,CAAC;AAExB,SAAS,QAAQ,CAAC,QAAgB;IAChC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QAClB,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;QAC3B,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,QAAQ,IAAI,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AACxD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAuB,EAAE,KAAa;IAC7D,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,IAAI,GAAyB,IAAI,CAAC;IAEtC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAClE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,SAAS;QACX,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACjB,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,IAAI,IAAI,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9C,IAAI,GAAG;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,IAAI,EAAE,KAAK,CAAC,aAAa;gBACzB,QAAQ;gBACR,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC;gBACzB,KAAK,EACH,QAAQ,KAAK,CAAC;oBACZ,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,QAAQ,KAAK,CAAC;wBACd,CAAC,CAAC,UAAU;wBACZ,CAAC,CAAC,GAAG,QAAQ,OAAO;aAC3B,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAA0B;IACrD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC,QAAQ,OAAO,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC;IAExG,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC;QACnB,KAAK,WAAW;YACd,OAAO,GAAG,IAAI,mGAAmG,CAAC;QACpH,KAAK,OAAO;YACV,OAAO,GAAG,IAAI,iEAAiE,CAAC;QAClF,KAAK,OAAO;YACV,OAAO,GAAG,IAAI,qFAAqF,CAAC;QACtG;YACE,OAAO,GAAG,IAAI,iFAAiF,CAAC;IACpG,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DetectorInput, Finding } from "./findings.js";
|
|
2
|
+
/**
|
|
3
|
+
* How far this person's recent resting heart rate sits above their own median,
|
|
4
|
+
* whether or not that is far enough to be a finding.
|
|
5
|
+
*
|
|
6
|
+
* Split out because two things need it and only one of them is a threshold. The
|
|
7
|
+
* detector fires at a fixed bar; the colour on a watch card is graded against
|
|
8
|
+
* the band the user chose, which may be tighter or looser. Deriving the colour
|
|
9
|
+
* from "did a finding fire" would mean the card could only ever be green or
|
|
10
|
+
* red, and would move whenever the detector's own constants moved.
|
|
11
|
+
*
|
|
12
|
+
* Null when there is not enough history to have a baseline at all -- which is
|
|
13
|
+
* different from a delta of zero, and has to stay different.
|
|
14
|
+
*/
|
|
15
|
+
export declare function restingHrDeltaBpm(input: DetectorInput): number | null;
|
|
16
|
+
export declare function detectRestingHrElevation(input: DetectorInput): Finding | null;
|
|
17
|
+
export declare function detectSleepDebt(input: DetectorInput): Finding | null;
|
|
18
|
+
export declare function detectHrvTrendBreak(input: DetectorInput): Finding | null;
|
|
19
|
+
export declare function detectLoadRatio(input: DetectorInput): Finding | null;
|
|
20
|
+
//# sourceMappingURL=detectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectors.d.ts","sourceRoot":"","sources":["../../src/detect/detectors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAiF5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAerE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,GAAG,IAAI,CA6C7E;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,GAAG,IAAI,CA+DpE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,GAAG,IAAI,CA6BxE;AA4CD,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,GAAG,IAAI,CAiEpE"}
|