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,112 @@
1
+ import Database from "better-sqlite3";
2
+ import type { ActivitySummary } from "../garmin/types.js";
3
+ import { type IngestOutcome, type IngestSource, type MetricKind } from "./schema.js";
4
+ export interface MetricPoint {
5
+ date: string;
6
+ value: number;
7
+ }
8
+ export interface StoredActivity {
9
+ activityId: number;
10
+ date: string;
11
+ startTimeLocal: string;
12
+ name: string;
13
+ type: string;
14
+ distanceMeters: number | null;
15
+ durationSeconds: number | null;
16
+ avgHr: number | null;
17
+ maxHr: number | null;
18
+ elevationGainMeters: number | null;
19
+ calories: number | null;
20
+ averageSpeedMps: number | null;
21
+ }
22
+ export interface HistoryStats {
23
+ metricRows: number;
24
+ rawRows: number;
25
+ activityRows: number;
26
+ oldestDate: string | null;
27
+ newestDate: string | null;
28
+ /** Days asked about that Garmin had nothing for -- usually pre-purchase. */
29
+ emptyDays: number;
30
+ }
31
+ export declare function openHistoryDb(databasePath?: string): Database.Database;
32
+ /** The same handle, for the sibling modules that share this database. */
33
+ export declare function getHistoryDb(): Database.Database;
34
+ export declare function closeHistoryDb(): void;
35
+ export declare function putMetrics(date: string, metrics: Array<{
36
+ kind: MetricKind;
37
+ value: number;
38
+ }>, fetchedAt?: number): void;
39
+ /**
40
+ * Every measurement recorded for one day, by kind.
41
+ *
42
+ * The thin half of the store fallback: `raw_payload` is bounded to 180 days,
43
+ * these rows are never pruned, so this is what can still answer for a day
44
+ * older than the archive. It carries the numbers the detectors need and none of
45
+ * the detail behind them, which is the honest trade -- the alternative was
46
+ * having nothing to say at all.
47
+ */
48
+ export declare function getMetricsOn(date: string): Map<MetricKind, number>;
49
+ /**
50
+ * The most recent dates the store holds anything for, newest first.
51
+ *
52
+ * Needed because every tool's window is anchored to today. With the record
53
+ * ending 2026-08-21 and Garmin unreachable on 2026-09-03, "the last 7 nights"
54
+ * selects seven days the store has never had a row for, and the answer comes
55
+ * back empty while seventy nights sit in the table. The window has to be able
56
+ * to move to where the data is, and then say that it did.
57
+ *
58
+ * Union of both halves of the store: `raw_payload` is per-source and bounded to
59
+ * 180 days, `daily_metric` is shared and never pruned. A date that turns out to
60
+ * hold nothing for the source asked about is dropped by the caller's mapper.
61
+ */
62
+ export declare function newestStoredDates(source: IngestSource, limit: number): string[];
63
+ export declare function getMetricSeries(kind: MetricKind, startDate: string, endDate: string): MetricPoint[];
64
+ export declare function appendRawPayload(date: string, source: IngestSource, payload: unknown, fetchedAt?: number): void;
65
+ /**
66
+ * How many revisions of one (date, source) are worth keeping.
67
+ *
68
+ * The archive exists because Garmin restates data -- a sleep score finalises
69
+ * hours after waking, VO2 max is recomputed after a qualifying activity -- and
70
+ * an archive that overwrote would hide the revision worth seeing. But the table
71
+ * was append-only with no ceiling at all, and every backfill re-writes every day
72
+ * it touches: a nightly catch-up over a year of history adds a row per day per
73
+ * source per run, forever. Three revisions keep the restatement visible; the
74
+ * fourth only records that a scheduler ran.
75
+ */
76
+ export declare const RAW_REVISIONS_KEPT = 3;
77
+ /**
78
+ * Age past which a raw payload is dropped entirely. The derived rows --
79
+ * daily_metric, activity, ingest_day -- are never touched by pruning, so
80
+ * history and every baseline computed from it survive intact. What is lost is
81
+ * only the ability to re-derive a day older than this from the original JSON.
82
+ */
83
+ export declare const RAW_RETENTION_DAYS = 180;
84
+ export interface RawPruneResult {
85
+ /** Rows dropped for being older than the retention window. */
86
+ agedOut: number;
87
+ /** Rows dropped for being the fourth or later revision of one day. */
88
+ supersededRevisions: number;
89
+ }
90
+ /**
91
+ * Bound the raw archive. Safe to call repeatedly; it is a no-op once the table
92
+ * is already within both limits.
93
+ */
94
+ export declare function pruneRawPayloads(now?: number, retentionDays?: number, revisionsKept?: number): RawPruneResult;
95
+ export declare function rawPayloadRevisions(date: string, source: IngestSource): Array<{
96
+ fetchedAt: number;
97
+ json: string;
98
+ }>;
99
+ export declare function markIngested(date: string, source: IngestSource, outcome: IngestOutcome, fetchedAt?: number): void;
100
+ export declare function getIngestCheckpoint(date: string, source: IngestSource): {
101
+ fetchedAt: number;
102
+ outcome: IngestOutcome;
103
+ } | null;
104
+ /** Every checkpointed date for a source, for the ingest planner. */
105
+ export declare function listIngestedDates(source: IngestSource): Map<string, {
106
+ fetchedAt: number;
107
+ outcome: IngestOutcome;
108
+ }>;
109
+ export declare function putActivities(activities: ActivitySummary[], fetchedAt?: number): void;
110
+ export declare function getActivitiesBetween(startDate: string, endDate: string): StoredActivity[];
111
+ export declare function historyStats(): HistoryStats;
112
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/history/store.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAItC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAG1D,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AASrB,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;CACnB;AAQD,wBAAgB,aAAa,CAAC,YAAY,SAAuB,GAAG,QAAQ,CAAC,QAAQ,CAepF;AAMD,yEAAyE;AACzE,wBAAgB,YAAY,IAAI,QAAQ,CAAC,QAAQ,CAEhD;AAED,wBAAgB,cAAc,IAAI,IAAI,CAKrC;AAQD,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EACnD,SAAS,SAAe,GACvB,IAAI,CA2BN;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAMlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAc/E;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,WAAW,EAAE,CAQf;AAID,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,OAAO,EAChB,SAAS,SAAe,GACvB,IAAI,CAMN;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,SAAe,EAClB,aAAa,SAAqB,EAClC,aAAa,SAAqB,GACjC,cAAc,CA6BhB;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,YAAY,GACnB,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ5C;AAID,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,aAAa,EACtB,SAAS,SAAe,GACvB,IAAI,CAUN;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,YAAY,GACnB;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,GAAG,IAAI,CAQtD;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC,CAQlH;AAWD,wBAAgB,aAAa,CAC3B,UAAU,EAAE,eAAe,EAAE,EAC7B,SAAS,SAAe,GACvB,IAAI,CA+CN;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAqBzF;AAID,wBAAgB,YAAY,IAAI,YAAY,CAwC3C"}
@@ -0,0 +1,282 @@
1
+ import Database from "better-sqlite3";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { appConfig } from "../config.js";
5
+ import { parseActivityLocalDateTime } from "../utils/helpers.js";
6
+ import { restrictExistingFile } from "../utils/secretFile.js";
7
+ import { HISTORY_SCHEMA, } from "./schema.js";
8
+ let db = null;
9
+ function defaultHistoryPath() {
10
+ return path.resolve(path.dirname(appConfig.cachePath), "history.db");
11
+ }
12
+ export function openHistoryDb(databasePath = defaultHistoryPath()) {
13
+ if (db) {
14
+ return db;
15
+ }
16
+ fs.mkdirSync(path.dirname(databasePath), { recursive: true });
17
+ db = new Database(databasePath);
18
+ // A year of the user's sleep, heart rate and HRV, plus every raw Connect
19
+ // response behind it. `app.db` was hardened for holding an API key and this
20
+ // was left at better-sqlite3's default 0644 -- readable by every account on
21
+ // the machine. It is the most personal file the app writes.
22
+ restrictExistingFile(databasePath);
23
+ db.exec(HISTORY_SCHEMA);
24
+ return db;
25
+ }
26
+ function getDb() {
27
+ return db ?? openHistoryDb();
28
+ }
29
+ /** The same handle, for the sibling modules that share this database. */
30
+ export function getHistoryDb() {
31
+ return getDb();
32
+ }
33
+ export function closeHistoryDb() {
34
+ if (db) {
35
+ db.close();
36
+ db = null;
37
+ }
38
+ }
39
+ function nowSeconds() {
40
+ return Math.floor(Date.now() / 1000);
41
+ }
42
+ // SECTION: Metrics
43
+ export function putMetrics(date, metrics, fetchedAt = nowSeconds()) {
44
+ if (metrics.length === 0) {
45
+ return;
46
+ }
47
+ const statement = getDb().prepare(`INSERT INTO daily_metric (date, kind, value, fetched_at)
48
+ VALUES (?, ?, ?, ?)
49
+ ON CONFLICT(date, kind) DO UPDATE SET
50
+ value = excluded.value,
51
+ fetched_at = excluded.fetched_at`);
52
+ const writeAll = getDb().transaction((rows) => {
53
+ for (const row of rows) {
54
+ // A non-finite value is not a measurement. NaN reaching a store is how
55
+ // the recovery score spent months rendering as "null/100" -- guard at
56
+ // the boundary rather than discovering it three layers up.
57
+ if (Number.isFinite(row.value)) {
58
+ statement.run(date, row.kind, row.value, fetchedAt);
59
+ }
60
+ }
61
+ });
62
+ writeAll(metrics);
63
+ }
64
+ /**
65
+ * Every measurement recorded for one day, by kind.
66
+ *
67
+ * The thin half of the store fallback: `raw_payload` is bounded to 180 days,
68
+ * these rows are never pruned, so this is what can still answer for a day
69
+ * older than the archive. It carries the numbers the detectors need and none of
70
+ * the detail behind them, which is the honest trade -- the alternative was
71
+ * having nothing to say at all.
72
+ */
73
+ export function getMetricsOn(date) {
74
+ const rows = getDb()
75
+ .prepare("SELECT kind, value FROM daily_metric WHERE date = ?")
76
+ .all(date);
77
+ return new Map(rows.map((row) => [row.kind, row.value]));
78
+ }
79
+ /**
80
+ * The most recent dates the store holds anything for, newest first.
81
+ *
82
+ * Needed because every tool's window is anchored to today. With the record
83
+ * ending 2026-08-21 and Garmin unreachable on 2026-09-03, "the last 7 nights"
84
+ * selects seven days the store has never had a row for, and the answer comes
85
+ * back empty while seventy nights sit in the table. The window has to be able
86
+ * to move to where the data is, and then say that it did.
87
+ *
88
+ * Union of both halves of the store: `raw_payload` is per-source and bounded to
89
+ * 180 days, `daily_metric` is shared and never pruned. A date that turns out to
90
+ * hold nothing for the source asked about is dropped by the caller's mapper.
91
+ */
92
+ export function newestStoredDates(source, limit) {
93
+ const rows = getDb()
94
+ .prepare(`SELECT date FROM (
95
+ SELECT DISTINCT date FROM raw_payload WHERE source = ?
96
+ UNION
97
+ SELECT DISTINCT date FROM daily_metric
98
+ )
99
+ ORDER BY date DESC
100
+ LIMIT ?`)
101
+ .all(source, limit);
102
+ return rows.map((row) => row.date);
103
+ }
104
+ export function getMetricSeries(kind, startDate, endDate) {
105
+ return getDb()
106
+ .prepare(`SELECT date, value FROM daily_metric
107
+ WHERE kind = ? AND date >= ? AND date <= ?
108
+ ORDER BY date ASC`)
109
+ .all(kind, startDate, endDate);
110
+ }
111
+ // SECTION: Raw archive
112
+ export function appendRawPayload(date, source, payload, fetchedAt = nowSeconds()) {
113
+ getDb()
114
+ .prepare("INSERT INTO raw_payload (date, source, fetched_at, json) VALUES (?, ?, ?, ?)")
115
+ .run(date, source, fetchedAt, JSON.stringify(payload ?? null));
116
+ }
117
+ /**
118
+ * How many revisions of one (date, source) are worth keeping.
119
+ *
120
+ * The archive exists because Garmin restates data -- a sleep score finalises
121
+ * hours after waking, VO2 max is recomputed after a qualifying activity -- and
122
+ * an archive that overwrote would hide the revision worth seeing. But the table
123
+ * was append-only with no ceiling at all, and every backfill re-writes every day
124
+ * it touches: a nightly catch-up over a year of history adds a row per day per
125
+ * source per run, forever. Three revisions keep the restatement visible; the
126
+ * fourth only records that a scheduler ran.
127
+ */
128
+ export const RAW_REVISIONS_KEPT = 3;
129
+ /**
130
+ * Age past which a raw payload is dropped entirely. The derived rows --
131
+ * daily_metric, activity, ingest_day -- are never touched by pruning, so
132
+ * history and every baseline computed from it survive intact. What is lost is
133
+ * only the ability to re-derive a day older than this from the original JSON.
134
+ */
135
+ export const RAW_RETENTION_DAYS = 180;
136
+ /**
137
+ * Bound the raw archive. Safe to call repeatedly; it is a no-op once the table
138
+ * is already within both limits.
139
+ */
140
+ export function pruneRawPayloads(now = nowSeconds(), retentionDays = RAW_RETENTION_DAYS, revisionsKept = RAW_REVISIONS_KEPT) {
141
+ const database = getDb();
142
+ const cutoff = now - retentionDays * 86_400;
143
+ return database.transaction(() => {
144
+ const agedOut = database
145
+ .prepare("DELETE FROM raw_payload WHERE fetched_at < ?")
146
+ .run(cutoff).changes;
147
+ // Rank within each (date, source) newest-first and drop everything past the
148
+ // cut. Ordering by id as well as fetched_at keeps this deterministic when
149
+ // two writes land in the same second, which a batched backfill does often.
150
+ const supersededRevisions = database
151
+ .prepare(`DELETE FROM raw_payload
152
+ WHERE id IN (
153
+ SELECT id FROM (
154
+ SELECT id, ROW_NUMBER() OVER (
155
+ PARTITION BY date, source ORDER BY fetched_at DESC, id DESC
156
+ ) AS rank
157
+ FROM raw_payload
158
+ )
159
+ WHERE rank > ?
160
+ )`)
161
+ .run(revisionsKept).changes;
162
+ return { agedOut, supersededRevisions };
163
+ })();
164
+ }
165
+ export function rawPayloadRevisions(date, source) {
166
+ return getDb()
167
+ .prepare(`SELECT fetched_at AS fetchedAt, json FROM raw_payload
168
+ WHERE date = ? AND source = ?
169
+ ORDER BY fetched_at ASC, id ASC`)
170
+ .all(date, source);
171
+ }
172
+ // SECTION: Ingest checkpoints
173
+ export function markIngested(date, source, outcome, fetchedAt = nowSeconds()) {
174
+ getDb()
175
+ .prepare(`INSERT INTO ingest_day (date, source, fetched_at, outcome)
176
+ VALUES (?, ?, ?, ?)
177
+ ON CONFLICT(date, source) DO UPDATE SET
178
+ fetched_at = excluded.fetched_at,
179
+ outcome = excluded.outcome`)
180
+ .run(date, source, fetchedAt, outcome);
181
+ }
182
+ export function getIngestCheckpoint(date, source) {
183
+ const row = getDb()
184
+ .prepare("SELECT fetched_at AS fetchedAt, outcome FROM ingest_day WHERE date = ? AND source = ?")
185
+ .get(date, source);
186
+ return row ?? null;
187
+ }
188
+ /** Every checkpointed date for a source, for the ingest planner. */
189
+ export function listIngestedDates(source) {
190
+ const rows = getDb()
191
+ .prepare("SELECT date, fetched_at AS fetchedAt, outcome FROM ingest_day WHERE source = ?")
192
+ .all(source);
193
+ return new Map(rows.map((row) => [row.date, { fetchedAt: row.fetchedAt, outcome: row.outcome }]));
194
+ }
195
+ // SECTION: Activities
196
+ function activityDate(activity) {
197
+ return (parseActivityLocalDateTime(activity.startTimeLocal).toISODate() ??
198
+ activity.startTimeLocal.slice(0, 10));
199
+ }
200
+ export function putActivities(activities, fetchedAt = nowSeconds()) {
201
+ if (activities.length === 0) {
202
+ return;
203
+ }
204
+ const statement = getDb().prepare(`INSERT INTO activity (
205
+ activity_id, date, start_time_local, name, type,
206
+ distance_m, duration_s, avg_hr, max_hr,
207
+ elevation_gain_m, calories, avg_speed_mps, fetched_at
208
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
209
+ ON CONFLICT(activity_id) DO UPDATE SET
210
+ date = excluded.date,
211
+ start_time_local = excluded.start_time_local,
212
+ name = excluded.name,
213
+ type = excluded.type,
214
+ distance_m = excluded.distance_m,
215
+ duration_s = excluded.duration_s,
216
+ avg_hr = excluded.avg_hr,
217
+ max_hr = excluded.max_hr,
218
+ elevation_gain_m = excluded.elevation_gain_m,
219
+ calories = excluded.calories,
220
+ avg_speed_mps = excluded.avg_speed_mps,
221
+ fetched_at = excluded.fetched_at`);
222
+ const writeAll = getDb().transaction((rows) => {
223
+ for (const activity of rows) {
224
+ statement.run(activity.activityId, activityDate(activity), activity.startTimeLocal, activity.name, activity.type, activity.distanceMeters, activity.durationSeconds, activity.averageHeartRate, activity.maxHeartRate, activity.elevationGainMeters, activity.calories, activity.averageSpeedMps, fetchedAt);
225
+ }
226
+ });
227
+ writeAll(activities);
228
+ }
229
+ export function getActivitiesBetween(startDate, endDate) {
230
+ return getDb()
231
+ .prepare(`SELECT
232
+ activity_id AS activityId,
233
+ date,
234
+ start_time_local AS startTimeLocal,
235
+ name,
236
+ type,
237
+ distance_m AS distanceMeters,
238
+ duration_s AS durationSeconds,
239
+ avg_hr AS avgHr,
240
+ max_hr AS maxHr,
241
+ elevation_gain_m AS elevationGainMeters,
242
+ calories,
243
+ avg_speed_mps AS averageSpeedMps
244
+ FROM activity
245
+ WHERE date >= ? AND date <= ?
246
+ ORDER BY start_time_local ASC`)
247
+ .all(startDate, endDate);
248
+ }
249
+ // SECTION: Stats
250
+ export function historyStats() {
251
+ const database = getDb();
252
+ const count = (table) => database.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get().count;
253
+ const span = database
254
+ .prepare("SELECT MIN(date) AS oldest, MAX(date) AS newest FROM daily_metric")
255
+ .get();
256
+ // Days, not rows. `ingest_day` is keyed (date, source) across six sources, so
257
+ // counting rows reported "Days Garmin had no data for: 1590" on a store
258
+ // covering 366 dates -- four and a half years' worth, from a line whose whole
259
+ // job is to reassure someone that a short span is Garmin's fault rather than a
260
+ // broken backfill. A number that cannot be a number of days does the opposite.
261
+ //
262
+ // A day Garmin had no data for is a date where no source returned data AND at
263
+ // least one said so. A date whose only checkpoints are errors is a day nobody
264
+ // managed to ask about, which is a different fact and stays out of this count.
265
+ const emptyDays = database
266
+ .prepare(`SELECT COUNT(*) AS count FROM (
267
+ SELECT date
268
+ FROM ingest_day
269
+ GROUP BY date
270
+ HAVING SUM(outcome = 'data') = 0 AND SUM(outcome = 'empty') > 0
271
+ )`)
272
+ .get().count;
273
+ return {
274
+ metricRows: count("daily_metric"),
275
+ rawRows: count("raw_payload"),
276
+ activityRows: count("activity"),
277
+ oldestDate: span.oldest,
278
+ newestDate: span.newest,
279
+ emptyDays,
280
+ };
281
+ }
282
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/history/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import http from "node:http";
2
+ export declare function liveMcpSessionCount(): number;
3
+ export interface HttpMcpServer {
4
+ start: () => Promise<void>;
5
+ close: () => Promise<void>;
6
+ }
7
+ /** Exposed for tests: drops every session, as a restart would. */
8
+ export declare function clearDashboardSessions(): void;
9
+ /**
10
+ * Pull a field from a body that may be JSON or URL-encoded, without re-reading
11
+ * the stream. The dashboard posts URL-encoded; API clients post JSON.
12
+ */
13
+ export declare function readFormOrJsonField(raw: string, field: string): string | null;
14
+ /**
15
+ * Deliberately separate from readFormOrJsonField, which returns null for any
16
+ * non-string JSON value. That is not an oversight there: a pair code is a
17
+ * zero-padded six-digit string, `000042` is not valid JSON as a number, and
18
+ * silently coercing 42 would turn "you sent this in a form that cannot
19
+ * represent it" into a failed lookup.
20
+ *
21
+ * A row id has no such problem, and a JSON client naturally sends {"id": 5}.
22
+ */
23
+ export declare function readNumericField(raw: string, field: string): number | null;
24
+ export declare function redactQuery(search: string | null | undefined): string;
25
+ /**
26
+ * Redact a live pairing code out of a request path.
27
+ *
28
+ * The query string was redacted and the path was not, so every status poll wrote
29
+ * `/api/pair/418902/status` into the log -- a code that is, for the next five
30
+ * minutes, a bearer credential: `/api/pair/<code>/status` hands out the API key
31
+ * the moment it is approved. The log file sits next to the database and, until
32
+ * the fix above, was as readable as it was.
33
+ *
34
+ * The code is the thing worth hiding; the shape of the path is worth keeping,
35
+ * because "did the watch poll at all" is the question this log exists to answer.
36
+ */
37
+ export declare function redactPath(pathname: string): string;
38
+ /**
39
+ * Both redactions, applied to a RAW `req.url`.
40
+ *
41
+ * The request-logging line was fixed to redact and the unhandled-error handler
42
+ * was not, so a 500 wrote `?token=<the live API key>` into `.trainbud/mcp.log`
43
+ * in plaintext. Same file, same log, same key, one fix short.
44
+ *
45
+ * `req.url` is an origin-form path, not an absolute URL, so it cannot go
46
+ * through `new URL()` without a base -- and this runs inside the handler whose
47
+ * entire purpose is that nothing in it may take the process down, so it must
48
+ * not throw on a malformed one either. Split by hand, and fall back to naming
49
+ * the shape rather than printing the value.
50
+ */
51
+ export declare function redactRequestUrl(rawUrl: string | undefined): string;
52
+ export declare function resolvePublicUrl(req: http.IncomingMessage): string;
53
+ export declare function createHttpMcpServer(): HttpMcpServer;
54
+ export declare function getRemoteConnectorInstructions(publicUrl: string): string;
55
+ //# sourceMappingURL=httpServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpServer.d.ts","sourceRoot":"","sources":["../src/httpServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAgE7B,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AA2JD,kEAAkE;AAClE,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAkFD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiB7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwB1E;AAwGD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAYrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAanE;AAWD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,MAAM,CAYlE;AAgBD,wBAAgB,mBAAmB,IAAI,aAAa,CA8kBnD;AAED,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAqBxE"}