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.
Files changed (274) hide show
  1. package/.env.example +11 -0
  2. package/CHANGELOG.md +675 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +182 -0
  5. package/README.md +322 -0
  6. package/dist/appDb.d.ts +83 -0
  7. package/dist/appDb.d.ts.map +1 -0
  8. package/dist/appDb.js +239 -0
  9. package/dist/appDb.js.map +1 -0
  10. package/dist/check.d.ts +12 -0
  11. package/dist/check.d.ts.map +1 -0
  12. package/dist/check.js +230 -0
  13. package/dist/check.js.map +1 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +457 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config.d.ts +61 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +241 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/dashboard.d.ts +8 -0
  23. package/dist/dashboard.d.ts.map +1 -0
  24. package/dist/dashboard.js +1004 -0
  25. package/dist/dashboard.js.map +1 -0
  26. package/dist/dashboardCharts.d.ts +99 -0
  27. package/dist/dashboardCharts.d.ts.map +1 -0
  28. package/dist/dashboardCharts.js +317 -0
  29. package/dist/dashboardCharts.js.map +1 -0
  30. package/dist/dashboardData.d.ts +89 -0
  31. package/dist/dashboardData.d.ts.map +1 -0
  32. package/dist/dashboardData.js +228 -0
  33. package/dist/dashboardData.js.map +1 -0
  34. package/dist/detect/baseline.d.ts +17 -0
  35. package/dist/detect/baseline.d.ts.map +1 -0
  36. package/dist/detect/baseline.js +62 -0
  37. package/dist/detect/baseline.js.map +1 -0
  38. package/dist/detect/countdown.d.ts +30 -0
  39. package/dist/detect/countdown.d.ts.map +1 -0
  40. package/dist/detect/countdown.js +79 -0
  41. package/dist/detect/countdown.js.map +1 -0
  42. package/dist/detect/detectors.d.ts +20 -0
  43. package/dist/detect/detectors.d.ts.map +1 -0
  44. package/dist/detect/detectors.js +291 -0
  45. package/dist/detect/detectors.js.map +1 -0
  46. package/dist/detect/findings.d.ts +29 -0
  47. package/dist/detect/findings.d.ts.map +1 -0
  48. package/dist/detect/findings.js +2 -0
  49. package/dist/detect/findings.js.map +1 -0
  50. package/dist/detect/forecast.d.ts +33 -0
  51. package/dist/detect/forecast.d.ts.map +1 -0
  52. package/dist/detect/forecast.js +122 -0
  53. package/dist/detect/forecast.js.map +1 -0
  54. package/dist/detect/index.d.ts +50 -0
  55. package/dist/detect/index.d.ts.map +1 -0
  56. package/dist/detect/index.js +114 -0
  57. package/dist/detect/index.js.map +1 -0
  58. package/dist/detect/sleepQuality.d.ts +21 -0
  59. package/dist/detect/sleepQuality.d.ts.map +1 -0
  60. package/dist/detect/sleepQuality.js +164 -0
  61. package/dist/detect/sleepQuality.js.map +1 -0
  62. package/dist/detect/trimp.d.ts +22 -0
  63. package/dist/detect/trimp.d.ts.map +1 -0
  64. package/dist/detect/trimp.js +77 -0
  65. package/dist/detect/trimp.js.map +1 -0
  66. package/dist/detect/week.d.ts +31 -0
  67. package/dist/detect/week.d.ts.map +1 -0
  68. package/dist/detect/week.js +184 -0
  69. package/dist/detect/week.js.map +1 -0
  70. package/dist/garmin/auth.d.ts +10 -0
  71. package/dist/garmin/auth.d.ts.map +1 -0
  72. package/dist/garmin/auth.js +81 -0
  73. package/dist/garmin/auth.js.map +1 -0
  74. package/dist/garmin/cache.d.ts +42 -0
  75. package/dist/garmin/cache.d.ts.map +1 -0
  76. package/dist/garmin/cache.js +141 -0
  77. package/dist/garmin/cache.js.map +1 -0
  78. package/dist/garmin/client.d.ts +22 -0
  79. package/dist/garmin/client.d.ts.map +1 -0
  80. package/dist/garmin/client.js +237 -0
  81. package/dist/garmin/client.js.map +1 -0
  82. package/dist/garmin/daily.d.ts +24 -0
  83. package/dist/garmin/daily.d.ts.map +1 -0
  84. package/dist/garmin/daily.js +96 -0
  85. package/dist/garmin/daily.js.map +1 -0
  86. package/dist/garmin/garminApiTypes.d.ts +70 -0
  87. package/dist/garmin/garminApiTypes.d.ts.map +1 -0
  88. package/dist/garmin/garminApiTypes.js +3 -0
  89. package/dist/garmin/garminApiTypes.js.map +1 -0
  90. package/dist/garmin/garminConnect.d.ts +17 -0
  91. package/dist/garmin/garminConnect.d.ts.map +1 -0
  92. package/dist/garmin/garminConnect.js +5 -0
  93. package/dist/garmin/garminConnect.js.map +1 -0
  94. package/dist/garmin/partial.d.ts +32 -0
  95. package/dist/garmin/partial.d.ts.map +1 -0
  96. package/dist/garmin/partial.js +38 -0
  97. package/dist/garmin/partial.js.map +1 -0
  98. package/dist/garmin/rawApi.d.ts +59 -0
  99. package/dist/garmin/rawApi.d.ts.map +1 -0
  100. package/dist/garmin/rawApi.js +113 -0
  101. package/dist/garmin/rawApi.js.map +1 -0
  102. package/dist/garmin/types.d.ts +85 -0
  103. package/dist/garmin/types.d.ts.map +1 -0
  104. package/dist/garmin/types.js +11 -0
  105. package/dist/garmin/types.js.map +1 -0
  106. package/dist/history/capture.d.ts +12 -0
  107. package/dist/history/capture.d.ts.map +1 -0
  108. package/dist/history/capture.js +63 -0
  109. package/dist/history/capture.js.map +1 -0
  110. package/dist/history/context.d.ts +51 -0
  111. package/dist/history/context.d.ts.map +1 -0
  112. package/dist/history/context.js +122 -0
  113. package/dist/history/context.js.map +1 -0
  114. package/dist/history/fallback.d.ts +72 -0
  115. package/dist/history/fallback.d.ts.map +1 -0
  116. package/dist/history/fallback.js +185 -0
  117. package/dist/history/fallback.js.map +1 -0
  118. package/dist/history/ingest.d.ts +44 -0
  119. package/dist/history/ingest.d.ts.map +1 -0
  120. package/dist/history/ingest.js +347 -0
  121. package/dist/history/ingest.js.map +1 -0
  122. package/dist/history/scheduler.d.ts +20 -0
  123. package/dist/history/scheduler.d.ts.map +1 -0
  124. package/dist/history/scheduler.js +54 -0
  125. package/dist/history/scheduler.js.map +1 -0
  126. package/dist/history/schema.d.ts +41 -0
  127. package/dist/history/schema.d.ts.map +1 -0
  128. package/dist/history/schema.js +91 -0
  129. package/dist/history/schema.js.map +1 -0
  130. package/dist/history/store.d.ts +112 -0
  131. package/dist/history/store.d.ts.map +1 -0
  132. package/dist/history/store.js +282 -0
  133. package/dist/history/store.js.map +1 -0
  134. package/dist/httpServer.d.ts +55 -0
  135. package/dist/httpServer.d.ts.map +1 -0
  136. package/dist/httpServer.js +1027 -0
  137. package/dist/httpServer.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +11 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/mcpConfig.d.ts +56 -0
  143. package/dist/mcpConfig.d.ts.map +1 -0
  144. package/dist/mcpConfig.js +106 -0
  145. package/dist/mcpConfig.js.map +1 -0
  146. package/dist/pairApi.d.ts +14 -0
  147. package/dist/pairApi.d.ts.map +1 -0
  148. package/dist/pairApi.js +38 -0
  149. package/dist/pairApi.js.map +1 -0
  150. package/dist/paths.d.ts +45 -0
  151. package/dist/paths.d.ts.map +1 -0
  152. package/dist/paths.js +105 -0
  153. package/dist/paths.js.map +1 -0
  154. package/dist/profile.d.ts +123 -0
  155. package/dist/profile.d.ts.map +1 -0
  156. package/dist/profile.js +327 -0
  157. package/dist/profile.js.map +1 -0
  158. package/dist/promptApi.d.ts +43 -0
  159. package/dist/promptApi.d.ts.map +1 -0
  160. package/dist/promptApi.js +328 -0
  161. package/dist/promptApi.js.map +1 -0
  162. package/dist/promptSuggestions.d.ts +16 -0
  163. package/dist/promptSuggestions.d.ts.map +1 -0
  164. package/dist/promptSuggestions.js +141 -0
  165. package/dist/promptSuggestions.js.map +1 -0
  166. package/dist/selfTest.d.ts +53 -0
  167. package/dist/selfTest.d.ts.map +1 -0
  168. package/dist/selfTest.js +225 -0
  169. package/dist/selfTest.js.map +1 -0
  170. package/dist/server.d.ts +10 -0
  171. package/dist/server.d.ts.map +1 -0
  172. package/dist/server.js +67 -0
  173. package/dist/server.js.map +1 -0
  174. package/dist/setup.d.ts +2 -0
  175. package/dist/setup.d.ts.map +1 -0
  176. package/dist/setup.js +210 -0
  177. package/dist/setup.js.map +1 -0
  178. package/dist/toolErrors.d.ts +2 -0
  179. package/dist/toolErrors.d.ts.map +1 -0
  180. package/dist/toolErrors.js +18 -0
  181. package/dist/toolErrors.js.map +1 -0
  182. package/dist/tools/activities.d.ts +18 -0
  183. package/dist/tools/activities.d.ts.map +1 -0
  184. package/dist/tools/activities.js +182 -0
  185. package/dist/tools/activities.js.map +1 -0
  186. package/dist/tools/bodyComposition.d.ts +10 -0
  187. package/dist/tools/bodyComposition.d.ts.map +1 -0
  188. package/dist/tools/bodyComposition.js +139 -0
  189. package/dist/tools/bodyComposition.js.map +1 -0
  190. package/dist/tools/context.d.ts +8 -0
  191. package/dist/tools/context.d.ts.map +1 -0
  192. package/dist/tools/context.js +148 -0
  193. package/dist/tools/context.js.map +1 -0
  194. package/dist/tools/findings.d.ts +7 -0
  195. package/dist/tools/findings.d.ts.map +1 -0
  196. package/dist/tools/findings.js +51 -0
  197. package/dist/tools/findings.js.map +1 -0
  198. package/dist/tools/heartRate.d.ts +10 -0
  199. package/dist/tools/heartRate.d.ts.map +1 -0
  200. package/dist/tools/heartRate.js +105 -0
  201. package/dist/tools/heartRate.js.map +1 -0
  202. package/dist/tools/index.d.ts +66 -0
  203. package/dist/tools/index.d.ts.map +1 -0
  204. package/dist/tools/index.js +101 -0
  205. package/dist/tools/index.js.map +1 -0
  206. package/dist/tools/payloads.d.ts +148 -0
  207. package/dist/tools/payloads.d.ts.map +1 -0
  208. package/dist/tools/payloads.js +2 -0
  209. package/dist/tools/payloads.js.map +1 -0
  210. package/dist/tools/recovery.d.ts +31 -0
  211. package/dist/tools/recovery.d.ts.map +1 -0
  212. package/dist/tools/recovery.js +350 -0
  213. package/dist/tools/recovery.js.map +1 -0
  214. package/dist/tools/sleep.d.ts +10 -0
  215. package/dist/tools/sleep.d.ts.map +1 -0
  216. package/dist/tools/sleep.js +124 -0
  217. package/dist/tools/sleep.js.map +1 -0
  218. package/dist/tools/stress.d.ts +11 -0
  219. package/dist/tools/stress.d.ts.map +1 -0
  220. package/dist/tools/stress.js +108 -0
  221. package/dist/tools/stress.js.map +1 -0
  222. package/dist/tools/trainingInsights.d.ts +15 -0
  223. package/dist/tools/trainingInsights.d.ts.map +1 -0
  224. package/dist/tools/trainingInsights.js +79 -0
  225. package/dist/tools/trainingInsights.js.map +1 -0
  226. package/dist/tools/types.d.ts +12 -0
  227. package/dist/tools/types.d.ts.map +1 -0
  228. package/dist/tools/types.js +2 -0
  229. package/dist/tools/types.js.map +1 -0
  230. package/dist/tools/vo2Max.d.ts +11 -0
  231. package/dist/tools/vo2Max.d.ts.map +1 -0
  232. package/dist/tools/vo2Max.js +110 -0
  233. package/dist/tools/vo2Max.js.map +1 -0
  234. package/dist/tools/week.d.ts +12 -0
  235. package/dist/tools/week.d.ts.map +1 -0
  236. package/dist/tools/week.js +74 -0
  237. package/dist/tools/week.js.map +1 -0
  238. package/dist/usage.d.ts +104 -0
  239. package/dist/usage.d.ts.map +1 -0
  240. package/dist/usage.js +247 -0
  241. package/dist/usage.js.map +1 -0
  242. package/dist/utils/batch.d.ts +2 -0
  243. package/dist/utils/batch.d.ts.map +1 -0
  244. package/dist/utils/batch.js +18 -0
  245. package/dist/utils/batch.js.map +1 -0
  246. package/dist/utils/helpers.d.ts +32 -0
  247. package/dist/utils/helpers.d.ts.map +1 -0
  248. package/dist/utils/helpers.js +188 -0
  249. package/dist/utils/helpers.js.map +1 -0
  250. package/dist/utils/jsonFile.d.ts +19 -0
  251. package/dist/utils/jsonFile.d.ts.map +1 -0
  252. package/dist/utils/jsonFile.js +24 -0
  253. package/dist/utils/jsonFile.js.map +1 -0
  254. package/dist/utils/logger.d.ts +4 -0
  255. package/dist/utils/logger.d.ts.map +1 -0
  256. package/dist/utils/logger.js +97 -0
  257. package/dist/utils/logger.js.map +1 -0
  258. package/dist/utils/secretFile.d.ts +15 -0
  259. package/dist/utils/secretFile.d.ts.map +1 -0
  260. package/dist/utils/secretFile.js +47 -0
  261. package/dist/utils/secretFile.js.map +1 -0
  262. package/dist/utils/stdio.d.ts +10 -0
  263. package/dist/utils/stdio.d.ts.map +1 -0
  264. package/dist/utils/stdio.js +70 -0
  265. package/dist/utils/stdio.js.map +1 -0
  266. package/dist/version.d.ts +2 -0
  267. package/dist/version.d.ts.map +1 -0
  268. package/dist/version.js +7 -0
  269. package/dist/version.js.map +1 -0
  270. package/dist/watchApi.d.ts +264 -0
  271. package/dist/watchApi.d.ts.map +1 -0
  272. package/dist/watchApi.js +333 -0
  273. package/dist/watchApi.js.map +1 -0
  274. 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":"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"}
@@ -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"}