trainbud 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -0
- package/CHANGELOG.md +675 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +182 -0
- package/README.md +322 -0
- package/dist/appDb.d.ts +83 -0
- package/dist/appDb.d.ts.map +1 -0
- package/dist/appDb.js +239 -0
- package/dist/appDb.js.map +1 -0
- package/dist/check.d.ts +12 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +230 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +457 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +61 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +241 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +1004 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/dashboardCharts.d.ts +99 -0
- package/dist/dashboardCharts.d.ts.map +1 -0
- package/dist/dashboardCharts.js +317 -0
- package/dist/dashboardCharts.js.map +1 -0
- package/dist/dashboardData.d.ts +89 -0
- package/dist/dashboardData.d.ts.map +1 -0
- package/dist/dashboardData.js +228 -0
- package/dist/dashboardData.js.map +1 -0
- package/dist/detect/baseline.d.ts +17 -0
- package/dist/detect/baseline.d.ts.map +1 -0
- package/dist/detect/baseline.js +62 -0
- package/dist/detect/baseline.js.map +1 -0
- package/dist/detect/countdown.d.ts +30 -0
- package/dist/detect/countdown.d.ts.map +1 -0
- package/dist/detect/countdown.js +79 -0
- package/dist/detect/countdown.js.map +1 -0
- package/dist/detect/detectors.d.ts +20 -0
- package/dist/detect/detectors.d.ts.map +1 -0
- package/dist/detect/detectors.js +291 -0
- package/dist/detect/detectors.js.map +1 -0
- package/dist/detect/findings.d.ts +29 -0
- package/dist/detect/findings.d.ts.map +1 -0
- package/dist/detect/findings.js +2 -0
- package/dist/detect/findings.js.map +1 -0
- package/dist/detect/forecast.d.ts +33 -0
- package/dist/detect/forecast.d.ts.map +1 -0
- package/dist/detect/forecast.js +122 -0
- package/dist/detect/forecast.js.map +1 -0
- package/dist/detect/index.d.ts +50 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +114 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/detect/sleepQuality.d.ts +21 -0
- package/dist/detect/sleepQuality.d.ts.map +1 -0
- package/dist/detect/sleepQuality.js +164 -0
- package/dist/detect/sleepQuality.js.map +1 -0
- package/dist/detect/trimp.d.ts +22 -0
- package/dist/detect/trimp.d.ts.map +1 -0
- package/dist/detect/trimp.js +77 -0
- package/dist/detect/trimp.js.map +1 -0
- package/dist/detect/week.d.ts +31 -0
- package/dist/detect/week.d.ts.map +1 -0
- package/dist/detect/week.js +184 -0
- package/dist/detect/week.js.map +1 -0
- package/dist/garmin/auth.d.ts +10 -0
- package/dist/garmin/auth.d.ts.map +1 -0
- package/dist/garmin/auth.js +81 -0
- package/dist/garmin/auth.js.map +1 -0
- package/dist/garmin/cache.d.ts +42 -0
- package/dist/garmin/cache.d.ts.map +1 -0
- package/dist/garmin/cache.js +141 -0
- package/dist/garmin/cache.js.map +1 -0
- package/dist/garmin/client.d.ts +22 -0
- package/dist/garmin/client.d.ts.map +1 -0
- package/dist/garmin/client.js +237 -0
- package/dist/garmin/client.js.map +1 -0
- package/dist/garmin/daily.d.ts +24 -0
- package/dist/garmin/daily.d.ts.map +1 -0
- package/dist/garmin/daily.js +96 -0
- package/dist/garmin/daily.js.map +1 -0
- package/dist/garmin/garminApiTypes.d.ts +70 -0
- package/dist/garmin/garminApiTypes.d.ts.map +1 -0
- package/dist/garmin/garminApiTypes.js +3 -0
- package/dist/garmin/garminApiTypes.js.map +1 -0
- package/dist/garmin/garminConnect.d.ts +17 -0
- package/dist/garmin/garminConnect.d.ts.map +1 -0
- package/dist/garmin/garminConnect.js +5 -0
- package/dist/garmin/garminConnect.js.map +1 -0
- package/dist/garmin/partial.d.ts +32 -0
- package/dist/garmin/partial.d.ts.map +1 -0
- package/dist/garmin/partial.js +38 -0
- package/dist/garmin/partial.js.map +1 -0
- package/dist/garmin/rawApi.d.ts +59 -0
- package/dist/garmin/rawApi.d.ts.map +1 -0
- package/dist/garmin/rawApi.js +113 -0
- package/dist/garmin/rawApi.js.map +1 -0
- package/dist/garmin/types.d.ts +85 -0
- package/dist/garmin/types.d.ts.map +1 -0
- package/dist/garmin/types.js +11 -0
- package/dist/garmin/types.js.map +1 -0
- package/dist/history/capture.d.ts +12 -0
- package/dist/history/capture.d.ts.map +1 -0
- package/dist/history/capture.js +63 -0
- package/dist/history/capture.js.map +1 -0
- package/dist/history/context.d.ts +51 -0
- package/dist/history/context.d.ts.map +1 -0
- package/dist/history/context.js +122 -0
- package/dist/history/context.js.map +1 -0
- package/dist/history/fallback.d.ts +72 -0
- package/dist/history/fallback.d.ts.map +1 -0
- package/dist/history/fallback.js +185 -0
- package/dist/history/fallback.js.map +1 -0
- package/dist/history/ingest.d.ts +44 -0
- package/dist/history/ingest.d.ts.map +1 -0
- package/dist/history/ingest.js +347 -0
- package/dist/history/ingest.js.map +1 -0
- package/dist/history/scheduler.d.ts +20 -0
- package/dist/history/scheduler.d.ts.map +1 -0
- package/dist/history/scheduler.js +54 -0
- package/dist/history/scheduler.js.map +1 -0
- package/dist/history/schema.d.ts +41 -0
- package/dist/history/schema.d.ts.map +1 -0
- package/dist/history/schema.js +91 -0
- package/dist/history/schema.js.map +1 -0
- package/dist/history/store.d.ts +112 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/store.js +282 -0
- package/dist/history/store.js.map +1 -0
- package/dist/httpServer.d.ts +55 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +1027 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/mcpConfig.d.ts +56 -0
- package/dist/mcpConfig.d.ts.map +1 -0
- package/dist/mcpConfig.js +106 -0
- package/dist/mcpConfig.js.map +1 -0
- package/dist/pairApi.d.ts +14 -0
- package/dist/pairApi.d.ts.map +1 -0
- package/dist/pairApi.js +38 -0
- package/dist/pairApi.js.map +1 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +105 -0
- package/dist/paths.js.map +1 -0
- package/dist/profile.d.ts +123 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +327 -0
- package/dist/profile.js.map +1 -0
- package/dist/promptApi.d.ts +43 -0
- package/dist/promptApi.d.ts.map +1 -0
- package/dist/promptApi.js +328 -0
- package/dist/promptApi.js.map +1 -0
- package/dist/promptSuggestions.d.ts +16 -0
- package/dist/promptSuggestions.d.ts.map +1 -0
- package/dist/promptSuggestions.js +141 -0
- package/dist/promptSuggestions.js.map +1 -0
- package/dist/selfTest.d.ts +53 -0
- package/dist/selfTest.d.ts.map +1 -0
- package/dist/selfTest.js +225 -0
- package/dist/selfTest.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +67 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +210 -0
- package/dist/setup.js.map +1 -0
- package/dist/toolErrors.d.ts +2 -0
- package/dist/toolErrors.d.ts.map +1 -0
- package/dist/toolErrors.js +18 -0
- package/dist/toolErrors.js.map +1 -0
- package/dist/tools/activities.d.ts +18 -0
- package/dist/tools/activities.d.ts.map +1 -0
- package/dist/tools/activities.js +182 -0
- package/dist/tools/activities.js.map +1 -0
- package/dist/tools/bodyComposition.d.ts +10 -0
- package/dist/tools/bodyComposition.d.ts.map +1 -0
- package/dist/tools/bodyComposition.js +139 -0
- package/dist/tools/bodyComposition.js.map +1 -0
- package/dist/tools/context.d.ts +8 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +148 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/findings.d.ts +7 -0
- package/dist/tools/findings.d.ts.map +1 -0
- package/dist/tools/findings.js +51 -0
- package/dist/tools/findings.js.map +1 -0
- package/dist/tools/heartRate.d.ts +10 -0
- package/dist/tools/heartRate.d.ts.map +1 -0
- package/dist/tools/heartRate.js +105 -0
- package/dist/tools/heartRate.js.map +1 -0
- package/dist/tools/index.d.ts +66 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +101 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/payloads.d.ts +148 -0
- package/dist/tools/payloads.d.ts.map +1 -0
- package/dist/tools/payloads.js +2 -0
- package/dist/tools/payloads.js.map +1 -0
- package/dist/tools/recovery.d.ts +31 -0
- package/dist/tools/recovery.d.ts.map +1 -0
- package/dist/tools/recovery.js +350 -0
- package/dist/tools/recovery.js.map +1 -0
- package/dist/tools/sleep.d.ts +10 -0
- package/dist/tools/sleep.d.ts.map +1 -0
- package/dist/tools/sleep.js +124 -0
- package/dist/tools/sleep.js.map +1 -0
- package/dist/tools/stress.d.ts +11 -0
- package/dist/tools/stress.d.ts.map +1 -0
- package/dist/tools/stress.js +108 -0
- package/dist/tools/stress.js.map +1 -0
- package/dist/tools/trainingInsights.d.ts +15 -0
- package/dist/tools/trainingInsights.d.ts.map +1 -0
- package/dist/tools/trainingInsights.js +79 -0
- package/dist/tools/trainingInsights.js.map +1 -0
- package/dist/tools/types.d.ts +12 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/vo2Max.d.ts +11 -0
- package/dist/tools/vo2Max.d.ts.map +1 -0
- package/dist/tools/vo2Max.js +110 -0
- package/dist/tools/vo2Max.js.map +1 -0
- package/dist/tools/week.d.ts +12 -0
- package/dist/tools/week.d.ts.map +1 -0
- package/dist/tools/week.js +74 -0
- package/dist/tools/week.js.map +1 -0
- package/dist/usage.d.ts +104 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +247 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/batch.d.ts +2 -0
- package/dist/utils/batch.d.ts.map +1 -0
- package/dist/utils/batch.js +18 -0
- package/dist/utils/batch.js.map +1 -0
- package/dist/utils/helpers.d.ts +32 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +188 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/jsonFile.d.ts +19 -0
- package/dist/utils/jsonFile.d.ts.map +1 -0
- package/dist/utils/jsonFile.js +24 -0
- package/dist/utils/jsonFile.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +97 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/secretFile.d.ts +15 -0
- package/dist/utils/secretFile.d.ts.map +1 -0
- package/dist/utils/secretFile.js +47 -0
- package/dist/utils/secretFile.js.map +1 -0
- package/dist/utils/stdio.d.ts +10 -0
- package/dist/utils/stdio.d.ts.map +1 -0
- package/dist/utils/stdio.js +70 -0
- package/dist/utils/stdio.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/dist/watchApi.d.ts +264 -0
- package/dist/watchApi.d.ts.map +1 -0
- package/dist/watchApi.js +333 -0
- package/dist/watchApi.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,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":"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"}
|
|
@@ -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"}
|