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,347 @@
1
+ import { DateTime } from "luxon";
2
+ import { fetchBodyCompositionDay, fetchHeartRateDay, fetchSleepDay, fetchStressDay, fetchVo2MaxDay, } from "../garmin/daily.js";
3
+ import { mapActivity } from "../garmin/daily.js";
4
+ import { parseActivityLocalDateTime, parseIsoDate } from "../utils/helpers.js";
5
+ import { logger } from "../utils/logger.js";
6
+ import { GarminApiError } from "../garmin/types.js";
7
+ import { writeFixture } from "./capture.js";
8
+ import { appendRawPayload, listIngestedDates, markIngested, pruneRawPayloads, putActivities, putMetrics, } from "./store.js";
9
+ // SECTION: Ingest
10
+ //
11
+ // The account is the thing at risk here, and there is no appeal process if
12
+ // Garmin decides this looks like a scraper. So: one request in flight, a
13
+ // deliberate delay between them, and a checkpoint written after every date so a
14
+ // kill resumes where it stopped rather than starting the year again.
15
+ /** Days that are never a final answer -- see NEVER_FINAL below. */
16
+ const STALE_WINDOW_DAYS = 3;
17
+ /** How long a stale-window day is trusted before it is worth asking again. */
18
+ const STALE_RECHECK_SECONDS = 60 * 60;
19
+ const DEFAULT_DAYS = 365;
20
+ const DEFAULT_DELAY_MS = 1000;
21
+ /**
22
+ * A watch has a purchase date, and asking Garmin about the months before it is
23
+ * spending requests against the account for nothing. Walking newest-first, a
24
+ * long unbroken run of empty days is the signal that the record has run out.
25
+ *
26
+ * 60 days rather than something tighter because a real person can stop wearing
27
+ * a watch for a while -- an injury, a holiday, a flat charger -- and the older
28
+ * history behind that gap is still worth having. Set to 0 to walk the whole
29
+ * window regardless.
30
+ */
31
+ const DEFAULT_STOP_AFTER_EMPTY_DAYS = 60;
32
+ /** The sources a full run covers. Exported so no caller keeps its own copy. */
33
+ export const DEFAULT_SOURCES = [
34
+ "sleep",
35
+ "heart_rate",
36
+ "stress",
37
+ "vo2max",
38
+ "body_composition",
39
+ "activities",
40
+ ];
41
+ /** Connect's page size for the activity list. */
42
+ const ACTIVITY_PAGE_SIZE = 100;
43
+ /** Backstop against paging forever if the window start is never reached. */
44
+ const MAX_ACTIVITY_PAGES = 20;
45
+ /** A GarminApiError carrying a 429, however it was wrapped on the way up. */
46
+ function isRateLimit(error) {
47
+ if (error instanceof GarminApiError) {
48
+ return error.statusCode === 429;
49
+ }
50
+ if (error instanceof Error) {
51
+ const message = error.message.toLowerCase();
52
+ return message.includes("429") || message.includes("rate limit");
53
+ }
54
+ return false;
55
+ }
56
+ function activityStartDate(activity) {
57
+ return (parseActivityLocalDateTime(activity.startTimeLocal).toISODate() ??
58
+ activity.startTimeLocal.slice(0, 10));
59
+ }
60
+ function delay(ms, signal) {
61
+ if (ms <= 0) {
62
+ return Promise.resolve();
63
+ }
64
+ return new Promise((resolve) => {
65
+ const timer = setTimeout(resolve, ms);
66
+ signal?.addEventListener("abort", () => {
67
+ clearTimeout(timer);
68
+ resolve();
69
+ }, { once: true });
70
+ });
71
+ }
72
+ /**
73
+ * A day inside the stale window is never a final answer: a sleep score
74
+ * finalizes hours after waking, VO2 max is recomputed after a qualifying
75
+ * activity, and Connect reports -2 for a day still in progress. Outside that
76
+ * window, any checkpoint is final -- including `empty`, which is what stops a
77
+ * backfill re-fetching a year of days the watch was not worn. `error` is always
78
+ * retried; it says nothing about the data.
79
+ */
80
+ function shouldFetch(date, checkpoint, today, nowSeconds) {
81
+ if (!checkpoint) {
82
+ return true;
83
+ }
84
+ if (checkpoint.outcome === "error") {
85
+ return true;
86
+ }
87
+ const age = today.diff(DateTime.fromISO(date, { zone: today.zone }), "days").days;
88
+ const isNeverFinal = age < STALE_WINDOW_DAYS;
89
+ if (!isNeverFinal) {
90
+ return false;
91
+ }
92
+ return nowSeconds - checkpoint.fetchedAt >= STALE_RECHECK_SECONDS;
93
+ }
94
+ export function pendingWork(options = {}) {
95
+ const days = options.days ?? DEFAULT_DAYS;
96
+ const sources = options.sources ?? DEFAULT_SOURCES;
97
+ const now = options.now ?? DateTime.local();
98
+ const today = now.startOf("day");
99
+ const nowSeconds = now.toUnixInteger();
100
+ const work = [];
101
+ for (const source of sources) {
102
+ // Activities are paged, not addressed by date, so the whole window is one
103
+ // unit of work rather than one per day.
104
+ if (source === "activities") {
105
+ const date = today.toISODate();
106
+ if (date && shouldFetch(date, listIngestedDates(source).get(date), today, nowSeconds)) {
107
+ work.push({ date, source });
108
+ }
109
+ continue;
110
+ }
111
+ const checkpoints = listIngestedDates(source);
112
+ // Newest first: an interrupted backfill should leave the most recent
113
+ // history behind, not the oldest.
114
+ for (let offset = 0; offset < days; offset += 1) {
115
+ const date = today.minus({ days: offset }).toISODate();
116
+ if (!date) {
117
+ continue;
118
+ }
119
+ if (shouldFetch(date, checkpoints.get(date), today, nowSeconds)) {
120
+ work.push({ date, source });
121
+ }
122
+ }
123
+ }
124
+ return work;
125
+ }
126
+ function measured(value) {
127
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
128
+ }
129
+ function collect(entries) {
130
+ const metrics = [];
131
+ for (const [kind, raw] of entries) {
132
+ const value = measured(raw);
133
+ if (value !== null) {
134
+ metrics.push({ kind, value });
135
+ }
136
+ }
137
+ return metrics;
138
+ }
139
+ async function fetchSource(source, client, date) {
140
+ switch (source) {
141
+ case "sleep": {
142
+ const { raw, mapped } = await fetchSleepDay(client, date);
143
+ return {
144
+ raw,
145
+ metrics: collect([
146
+ ["sleep_seconds", mapped?.totalSleepSeconds],
147
+ ["sleep_score", mapped?.sleepScore],
148
+ ["hrv_overnight", mapped?.avgOvernightHrv],
149
+ ["sleep_stress", mapped?.avgSleepStress],
150
+ ]),
151
+ };
152
+ }
153
+ case "heart_rate": {
154
+ const { raw, mapped } = await fetchHeartRateDay(client, date);
155
+ return {
156
+ raw,
157
+ metrics: collect([
158
+ ["resting_hr", mapped?.restingHeartRate],
159
+ ["max_hr", mapped?.maxHeartRate],
160
+ ]),
161
+ };
162
+ }
163
+ case "stress": {
164
+ const { raw, mapped } = await fetchStressDay(client, date);
165
+ return {
166
+ raw,
167
+ metrics: collect([
168
+ ["stress_avg", mapped?.averageStress],
169
+ ["stress_max", mapped?.maxStress],
170
+ ]),
171
+ };
172
+ }
173
+ case "vo2max": {
174
+ const { raw, mapped } = await fetchVo2MaxDay(client, date);
175
+ return {
176
+ raw,
177
+ metrics: collect([
178
+ ["vo2max", mapped?.vo2Max],
179
+ ["vo2max_cycling", mapped?.vo2MaxCycling],
180
+ ]),
181
+ measuredOn: mapped?.date,
182
+ };
183
+ }
184
+ case "body_composition": {
185
+ const { raw, mapped } = await fetchBodyCompositionDay(client, date);
186
+ // Several weigh-ins can share a date; the last one is the day's figure.
187
+ const entry = mapped?.at(-1);
188
+ return {
189
+ raw,
190
+ metrics: collect([
191
+ ["weight_kg", entry?.weightKg],
192
+ ["body_fat_pct", entry?.bodyFatPercent],
193
+ ["muscle_mass_kg", entry?.muscleMassKg],
194
+ ]),
195
+ };
196
+ }
197
+ case "activities":
198
+ // Handled by ingestActivities, which pages rather than addressing a date.
199
+ return { raw: null, metrics: [] };
200
+ }
201
+ }
202
+ /**
203
+ * The activity list is paged newest-first, so the first activity older than the
204
+ * window means every remaining page is older still and there is nothing left to
205
+ * ask for.
206
+ */
207
+ async function ingestActivities(client, windowStart, stampedAt, captureDir) {
208
+ let start = 0;
209
+ let stored = 0;
210
+ for (let page = 0; page < MAX_ACTIVITY_PAGES; page += 1) {
211
+ const activities = await client.getActivities(start, ACTIVITY_PAGE_SIZE);
212
+ if (activities.length === 0) {
213
+ break;
214
+ }
215
+ const mapped = activities.map(mapActivity);
216
+ putActivities(mapped, stampedAt);
217
+ stored += mapped.length;
218
+ if (captureDir) {
219
+ writeFixture(captureDir, "activities", `page-${page}`, activities);
220
+ }
221
+ const reachedWindowStart = mapped.some((activity) => activityStartDate(activity) < windowStart);
222
+ if (reachedWindowStart || activities.length < ACTIVITY_PAGE_SIZE) {
223
+ break;
224
+ }
225
+ start += ACTIVITY_PAGE_SIZE;
226
+ }
227
+ return stored;
228
+ }
229
+ export async function runIngest(client, options = {}) {
230
+ const delayMs = options.delayMs ?? DEFAULT_DELAY_MS;
231
+ const work = pendingWork(options);
232
+ // One clock for the whole run. Planning read `options.now` while the
233
+ // checkpoints were stamped from the wall clock, so a caller that supplied a
234
+ // clock got checkpoints the planner could not reason about.
235
+ const stampedAt = (options.now ?? DateTime.local()).toUnixInteger();
236
+ const stopAfterEmptyDays = options.stopAfterEmptyDays ?? DEFAULT_STOP_AFTER_EMPTY_DAYS;
237
+ const emptyRun = new Map();
238
+ const exhausted = new Set();
239
+ const result = {
240
+ fetched: 0,
241
+ skipped: 0,
242
+ errors: 0,
243
+ firstDate: null,
244
+ lastDate: null,
245
+ };
246
+ let done = 0;
247
+ for (const unit of work) {
248
+ if (options.signal?.aborted) {
249
+ result.skipped += work.length - done;
250
+ break;
251
+ }
252
+ // Deliberately not checkpointed. Skipping is not the same claim as "we
253
+ // asked and there was nothing", so a later run -- with a wider window, or
254
+ // with the limit disabled -- can still reach these days.
255
+ if (exhausted.has(unit.source)) {
256
+ result.skipped += 1;
257
+ done += 1;
258
+ continue;
259
+ }
260
+ let outcome;
261
+ try {
262
+ if (unit.source === "activities") {
263
+ const windowStart = (options.now ?? DateTime.local())
264
+ .startOf("day")
265
+ .minus({ days: options.days ?? DEFAULT_DAYS })
266
+ .toISODate() ?? "";
267
+ const stored = await ingestActivities(client, windowStart, stampedAt, options.captureDir);
268
+ markIngested(unit.date, unit.source, stored > 0 ? "data" : "empty", stampedAt);
269
+ result.fetched += 1;
270
+ done += 1;
271
+ options.onProgress?.({
272
+ ...unit,
273
+ outcome: stored > 0 ? "data" : "empty",
274
+ done,
275
+ total: work.length,
276
+ });
277
+ if (done < work.length) {
278
+ await delay(delayMs, options.signal);
279
+ }
280
+ continue;
281
+ }
282
+ // parseIsoDate, not new Date(): the bare constructor parses a date-only
283
+ // string as UTC midnight, and garmin-connect formats the Date it is given
284
+ // in local time. West of UTC that combination asks Garmin for the previous
285
+ // day and stores the answer under this one.
286
+ const { raw, metrics, measuredOn } = await fetchSource(unit.source, client, parseIsoDate(unit.date));
287
+ appendRawPayload(unit.date, unit.source, raw, stampedAt);
288
+ putMetrics(measuredOn ?? unit.date, metrics, stampedAt);
289
+ if (options.captureDir) {
290
+ writeFixture(options.captureDir, unit.source, unit.date, raw);
291
+ }
292
+ // A reading that belongs to another day is not a measurement for this
293
+ // one. Recording it as `data` would also defeat the empty-run stop, since
294
+ // every pre-purchase day would answer with today's reading forever.
295
+ const measuredHere = (measuredOn ?? unit.date) === unit.date;
296
+ outcome = metrics.length > 0 && measuredHere ? "data" : "empty";
297
+ result.fetched += 1;
298
+ }
299
+ catch (error) {
300
+ // One bad day must not end a year-long run -- but a rate limit is not one
301
+ // bad day, it is the upstream saying stop, and carrying on is how a
302
+ // 365-day backfill answered a 429 with seventeen hundred more requests.
303
+ // Each of those deepened the limit and none of them could have succeeded.
304
+ if (isRateLimit(error)) {
305
+ logger.warn({ error, ...unit, remaining: work.length - done }, "Garmin rate limited the ingest; stopping this run");
306
+ markIngested(unit.date, unit.source, "error", stampedAt);
307
+ result.errors += 1;
308
+ result.skipped += work.length - done - 1;
309
+ result.stoppedBy = "rate_limit";
310
+ break;
311
+ }
312
+ logger.warn({ error, ...unit }, "Ingest failed for one day");
313
+ outcome = "error";
314
+ result.errors += 1;
315
+ }
316
+ markIngested(unit.date, unit.source, outcome, stampedAt);
317
+ if (stopAfterEmptyDays > 0) {
318
+ const run = outcome === "empty" ? (emptyRun.get(unit.source) ?? 0) + 1 : 0;
319
+ emptyRun.set(unit.source, run);
320
+ if (run >= stopAfterEmptyDays) {
321
+ exhausted.add(unit.source);
322
+ logger.info({ source: unit.source, date: unit.date, emptyDays: run }, "Reached the start of the record for this source; skipping older days");
323
+ }
324
+ }
325
+ result.firstDate =
326
+ result.firstDate === null || unit.date < result.firstDate ? unit.date : result.firstDate;
327
+ result.lastDate =
328
+ result.lastDate === null || unit.date > result.lastDate ? unit.date : result.lastDate;
329
+ done += 1;
330
+ options.onProgress?.({ ...unit, outcome, done, total: work.length });
331
+ if (done < work.length) {
332
+ await delay(delayMs, options.signal);
333
+ }
334
+ }
335
+ // Every run appends a raw payload per day per source, so the archive grows
336
+ // with the number of runs and not with the amount of history. Bounding it
337
+ // here means the table is trimmed by the thing that fills it, rather than
338
+ // waiting for someone to notice the file.
339
+ if (result.fetched > 0) {
340
+ const pruned = pruneRawPayloads();
341
+ if (pruned.agedOut + pruned.supersededRevisions > 0) {
342
+ logger.debug({ ...pruned }, "Pruned the raw payload archive");
343
+ }
344
+ }
345
+ return result;
346
+ }
347
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../src/history/ingest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface HistorySchedulerOptions {
2
+ intervalMs?: number;
3
+ run?: () => Promise<unknown>;
4
+ }
5
+ /**
6
+ * Returns its own stop function.
7
+ *
8
+ * Three properties this must keep:
9
+ *
10
+ * 1. The timer is unref()'d. A referenced interval holds the Node event loop
11
+ * open, which is exactly the bug that made `npm test` hang until something
12
+ * killed it (802d4d8) -- do not reintroduce it in a different shape.
13
+ * 2. Runs never overlap. A backfill can take an hour; queueing ticks behind it
14
+ * would fire a burst of concurrent requests at Garmin the moment it
15
+ * finished, which is the one thing the ingest pacing exists to prevent.
16
+ * 3. A failing run is logged and swallowed. The watch depends on this process
17
+ * staying up, and a Garmin outage must not take the HTTP server with it.
18
+ */
19
+ export declare function startHistoryScheduler(options?: HistorySchedulerOptions): () => void;
20
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/history/scheduler.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,uBAAuB;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9B;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,uBAA4B,GAAG,MAAM,IAAI,CA+BvF"}
@@ -0,0 +1,54 @@
1
+ import { withGarminClient } from "../garmin/client.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { runIngest } from "./ingest.js";
4
+ // SECTION: History scheduler
5
+ //
6
+ // There is no daemon and the machine is not always on, but Garmin retains the
7
+ // history, so any gap is recoverable. Catching up on start and then ticking
8
+ // while the process lives is enough to keep a year current without asking the
9
+ // user to run anything or install a service.
10
+ const DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
11
+ async function catchUp() {
12
+ return withGarminClient(async (client) => runIngest(client));
13
+ }
14
+ /**
15
+ * Returns its own stop function.
16
+ *
17
+ * Three properties this must keep:
18
+ *
19
+ * 1. The timer is unref()'d. A referenced interval holds the Node event loop
20
+ * open, which is exactly the bug that made `npm test` hang until something
21
+ * killed it (802d4d8) -- do not reintroduce it in a different shape.
22
+ * 2. Runs never overlap. A backfill can take an hour; queueing ticks behind it
23
+ * would fire a burst of concurrent requests at Garmin the moment it
24
+ * finished, which is the one thing the ingest pacing exists to prevent.
25
+ * 3. A failing run is logged and swallowed. The watch depends on this process
26
+ * staying up, and a Garmin outage must not take the HTTP server with it.
27
+ */
28
+ export function startHistoryScheduler(options = {}) {
29
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
30
+ const run = options.run ?? catchUp;
31
+ let running = false;
32
+ let stopped = false;
33
+ const tick = () => {
34
+ if (stopped || running) {
35
+ return;
36
+ }
37
+ running = true;
38
+ void run()
39
+ .catch((error) => {
40
+ logger.warn({ error }, "History catch-up failed");
41
+ })
42
+ .finally(() => {
43
+ running = false;
44
+ });
45
+ };
46
+ tick();
47
+ const timer = setInterval(tick, intervalMs);
48
+ timer.unref();
49
+ return () => {
50
+ stopped = true;
51
+ clearInterval(timer);
52
+ };
53
+ }
54
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/history/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAqB,MAAM,aAAa,CAAC;AAE3D,6BAA6B;AAC7B,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,8EAA8E;AAC9E,6CAA6C;AAE7C,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAO3C,KAAK,UAAU,OAAO;IACpB,OAAO,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CAAC,UAAmC,EAAE;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC;IAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC;IAEnC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,MAAM,IAAI,GAAG,GAAS,EAAE;QACtB,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QAED,OAAO,GAAG,IAAI,CAAC;QACf,KAAK,GAAG,EAAE;aACP,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,yBAAyB,CAAC,CAAC;QACpD,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;IAEF,IAAI,EAAE,CAAC;IAEP,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC5C,KAAK,CAAC,KAAK,EAAE,CAAC;IAEd,OAAO,GAAG,EAAE;QACV,OAAO,GAAG,IAAI,CAAC;QACf,aAAa,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * One row per measurement. Sources that report several numbers for a day write
3
+ * several rows.
4
+ */
5
+ export type MetricKind = "sleep_seconds" | "sleep_score" | "hrv_overnight" | "sleep_stress" | "resting_hr" | "max_hr" | "stress_avg" | "stress_max" | "vo2max" | "vo2max_cycling" | "weight_kg" | "body_fat_pct" | "muscle_mass_kg";
6
+ /** One Garmin endpoint family, fetched a day at a time. */
7
+ export type IngestSource = "sleep" | "heart_rate" | "stress" | "vo2max" | "body_composition" | "activities";
8
+ /**
9
+ * `data` -- the day was fetched and something was measured.
10
+ * `empty` -- the day was fetched and Garmin has nothing, which is a final
11
+ * answer for any date outside the stale window: the watch was not
12
+ * worn and never will have been.
13
+ * `error` -- the request failed. Retried on the next run.
14
+ */
15
+ export type IngestOutcome = "data" | "empty" | "error";
16
+ /**
17
+ * What the user tells us that Garmin has no idea about. This is the whole
18
+ * differentiator: Connect holds the measurements and no notion of who is being
19
+ * measured or what they are training for.
20
+ */
21
+ export type ContextKind = "goal" | "race" | "injury" | "note";
22
+ /** Rated 1-10, one per day per kind. */
23
+ export type SubjectiveKind = "rpe" | "soreness" | "mood";
24
+ export declare const CONTEXT_KINDS: ContextKind[];
25
+ export declare const SUBJECTIVE_KINDS: SubjectiveKind[];
26
+ /**
27
+ * `daily_metric.value` is NOT NULL and a row exists only for a real
28
+ * measurement, so "is there a row" answers "was this measured". The different
29
+ * question -- "have we ever asked Garmin about this day" -- is what ingest_day
30
+ * answers, and keeping the two apart is what stops a backfill from re-fetching
31
+ * a year of days the user did not wear the watch.
32
+ *
33
+ * raw_payload is append-only. Garmin restates data (a sleep score finalizes
34
+ * hours after waking, VO2 max is recomputed after a qualifying activity), and
35
+ * an archive that overwrote would hide exactly the revision worth seeing. It is
36
+ * also the insurance against a mapping bug: this project has twice read a field
37
+ * Connect does not send, and re-deriving from the archive beats re-fetching a
38
+ * year against an unofficial API.
39
+ */
40
+ export declare const HISTORY_SCHEMA = "\n CREATE TABLE IF NOT EXISTS daily_metric (\n date TEXT NOT NULL,\n kind TEXT NOT NULL,\n value REAL NOT NULL,\n fetched_at INTEGER NOT NULL,\n PRIMARY KEY (date, kind)\n );\n\n CREATE TABLE IF NOT EXISTS raw_payload (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n date TEXT NOT NULL,\n source TEXT NOT NULL,\n fetched_at INTEGER NOT NULL,\n json TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS raw_payload_date_source\n ON raw_payload (date, source, fetched_at);\n\n CREATE TABLE IF NOT EXISTS ingest_day (\n date TEXT NOT NULL,\n source TEXT NOT NULL,\n fetched_at INTEGER NOT NULL,\n outcome TEXT NOT NULL,\n PRIMARY KEY (date, source)\n );\n\n CREATE TABLE IF NOT EXISTS activity (\n activity_id INTEGER PRIMARY KEY,\n date TEXT NOT NULL,\n start_time_local TEXT NOT NULL,\n name TEXT NOT NULL,\n type TEXT NOT NULL,\n distance_m REAL,\n duration_s REAL,\n avg_hr REAL,\n max_hr REAL,\n elevation_gain_m REAL,\n calories REAL,\n avg_speed_mps REAL,\n fetched_at INTEGER NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS activity_date ON activity (date);\n\n CREATE TABLE IF NOT EXISTS context_entry (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL,\n text TEXT NOT NULL,\n effective_from TEXT NOT NULL,\n effective_to TEXT,\n created_at INTEGER NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS context_entry_range\n ON context_entry (effective_from, effective_to);\n\n CREATE TABLE IF NOT EXISTS subjective (\n date TEXT NOT NULL,\n kind TEXT NOT NULL,\n value REAL NOT NULL,\n note TEXT,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (date, kind)\n );\n";
41
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/history/schema.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,eAAe,GACf,aAAa,GACb,eAAe,GACf,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,YAAY,GACZ,YAAY,GACZ,QAAQ,GACR,gBAAgB,GAChB,WAAW,GACX,cAAc,GACd,gBAAgB,CAAC;AAErB,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GACpB,OAAO,GACP,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,kBAAkB,GAClB,YAAY,CAAC;AAEjB;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE9D,wCAAwC;AACxC,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,UAAU,GAAG,MAAM,CAAC;AAEzD,eAAO,MAAM,aAAa,EAAE,WAAW,EAAuC,CAAC;AAC/E,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAAgC,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,i7DAkE1B,CAAC"}
@@ -0,0 +1,91 @@
1
+ // SECTION: History schema
2
+ //
3
+ // TrainBud's own copy of the Garmin record. Garmin remains the system of truth
4
+ // -- anything here can be re-fetched -- but nothing can be *asked* of data that
5
+ // was never kept, and the tool cache is TTL-keyed and deletes its own rows.
6
+ // Without this, every answer is derived from one day's numbers, which is the
7
+ // whole reason the app could only ever repeat what Connect already showed.
8
+ export const CONTEXT_KINDS = ["goal", "race", "injury", "note"];
9
+ export const SUBJECTIVE_KINDS = ["rpe", "soreness", "mood"];
10
+ /**
11
+ * `daily_metric.value` is NOT NULL and a row exists only for a real
12
+ * measurement, so "is there a row" answers "was this measured". The different
13
+ * question -- "have we ever asked Garmin about this day" -- is what ingest_day
14
+ * answers, and keeping the two apart is what stops a backfill from re-fetching
15
+ * a year of days the user did not wear the watch.
16
+ *
17
+ * raw_payload is append-only. Garmin restates data (a sleep score finalizes
18
+ * hours after waking, VO2 max is recomputed after a qualifying activity), and
19
+ * an archive that overwrote would hide exactly the revision worth seeing. It is
20
+ * also the insurance against a mapping bug: this project has twice read a field
21
+ * Connect does not send, and re-deriving from the archive beats re-fetching a
22
+ * year against an unofficial API.
23
+ */
24
+ export const HISTORY_SCHEMA = `
25
+ CREATE TABLE IF NOT EXISTS daily_metric (
26
+ date TEXT NOT NULL,
27
+ kind TEXT NOT NULL,
28
+ value REAL NOT NULL,
29
+ fetched_at INTEGER NOT NULL,
30
+ PRIMARY KEY (date, kind)
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS raw_payload (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ date TEXT NOT NULL,
36
+ source TEXT NOT NULL,
37
+ fetched_at INTEGER NOT NULL,
38
+ json TEXT NOT NULL
39
+ );
40
+
41
+ CREATE INDEX IF NOT EXISTS raw_payload_date_source
42
+ ON raw_payload (date, source, fetched_at);
43
+
44
+ CREATE TABLE IF NOT EXISTS ingest_day (
45
+ date TEXT NOT NULL,
46
+ source TEXT NOT NULL,
47
+ fetched_at INTEGER NOT NULL,
48
+ outcome TEXT NOT NULL,
49
+ PRIMARY KEY (date, source)
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS activity (
53
+ activity_id INTEGER PRIMARY KEY,
54
+ date TEXT NOT NULL,
55
+ start_time_local TEXT NOT NULL,
56
+ name TEXT NOT NULL,
57
+ type TEXT NOT NULL,
58
+ distance_m REAL,
59
+ duration_s REAL,
60
+ avg_hr REAL,
61
+ max_hr REAL,
62
+ elevation_gain_m REAL,
63
+ calories REAL,
64
+ avg_speed_mps REAL,
65
+ fetched_at INTEGER NOT NULL
66
+ );
67
+
68
+ CREATE INDEX IF NOT EXISTS activity_date ON activity (date);
69
+
70
+ CREATE TABLE IF NOT EXISTS context_entry (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ kind TEXT NOT NULL,
73
+ text TEXT NOT NULL,
74
+ effective_from TEXT NOT NULL,
75
+ effective_to TEXT,
76
+ created_at INTEGER NOT NULL
77
+ );
78
+
79
+ CREATE INDEX IF NOT EXISTS context_entry_range
80
+ ON context_entry (effective_from, effective_to);
81
+
82
+ CREATE TABLE IF NOT EXISTS subjective (
83
+ date TEXT NOT NULL,
84
+ kind TEXT NOT NULL,
85
+ value REAL NOT NULL,
86
+ note TEXT,
87
+ created_at INTEGER NOT NULL,
88
+ PRIMARY KEY (date, kind)
89
+ );
90
+ `;
91
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/history/schema.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,4EAA4E;AAC5E,6EAA6E;AAC7E,2EAA2E;AAiD3E,MAAM,CAAC,MAAM,aAAa,GAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkE7B,CAAC"}