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
package/dist/usage.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import { getAppDb } from "./appDb.js";
|
|
3
|
+
import { getProfile } from "./profile.js";
|
|
4
|
+
import { logger } from "./utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Published per-million-token rates.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately a small table of the models this product can actually be
|
|
9
|
+
* pointed at, rather than a guess at a general pricing API. A model missing
|
|
10
|
+
* from here is unpriced, and says so.
|
|
11
|
+
*/
|
|
12
|
+
const PRICES_PER_MTOK = {
|
|
13
|
+
"claude-haiku-4-5": { input: 1.0, output: 5.0 },
|
|
14
|
+
"claude-sonnet-5": { input: 2.0, output: 10.0 },
|
|
15
|
+
"claude-opus-5": { input: 5.0, output: 25.0 },
|
|
16
|
+
};
|
|
17
|
+
/** Cache reads bill at about a tenth of fresh input; writes at about 1.25x. */
|
|
18
|
+
const CACHE_READ_MULTIPLIER = 0.1;
|
|
19
|
+
const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
20
|
+
/** Cost in USD, or null when this model has no price here. */
|
|
21
|
+
export function priceOf(model, tokens) {
|
|
22
|
+
const rate = PRICES_PER_MTOK[model];
|
|
23
|
+
if (!rate) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const perToken = (perMTok) => perMTok / 1_000_000;
|
|
27
|
+
return (tokens.inputTokens * perToken(rate.input) +
|
|
28
|
+
tokens.outputTokens * perToken(rate.output) +
|
|
29
|
+
(tokens.cacheReadTokens ?? 0) * perToken(rate.input) * CACHE_READ_MULTIPLIER +
|
|
30
|
+
(tokens.cacheWriteTokens ?? 0) * perToken(rate.input) * CACHE_WRITE_MULTIPLIER);
|
|
31
|
+
}
|
|
32
|
+
export function recordAiUsage(input) {
|
|
33
|
+
const at = input.at ?? Math.floor(Date.now() / 1000);
|
|
34
|
+
const costUsd = priceOf(input.model, input);
|
|
35
|
+
const row = { ...input, at, costUsd };
|
|
36
|
+
try {
|
|
37
|
+
getAppDb()
|
|
38
|
+
.prepare(`INSERT INTO ai_usage
|
|
39
|
+
(at, kind, model, source, input_tokens, output_tokens,
|
|
40
|
+
cache_read_tokens, cache_write_tokens, cost_usd)
|
|
41
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
42
|
+
.run(at, input.kind, input.model, input.source, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, costUsd);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Accounting must not take down the answer it was accounting for.
|
|
46
|
+
logger.warn({ err }, "could not record AI usage");
|
|
47
|
+
}
|
|
48
|
+
return row;
|
|
49
|
+
}
|
|
50
|
+
export function aiSpendSince(sinceUnixSeconds) {
|
|
51
|
+
try {
|
|
52
|
+
const row = getAppDb()
|
|
53
|
+
.prepare(`SELECT COUNT(*) AS calls,
|
|
54
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
55
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
56
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
57
|
+
COALESCE(SUM(cost_usd IS NULL), 0) AS unpriced
|
|
58
|
+
FROM ai_usage
|
|
59
|
+
WHERE at >= ?`)
|
|
60
|
+
.get(sinceUnixSeconds);
|
|
61
|
+
return {
|
|
62
|
+
calls: row.calls,
|
|
63
|
+
inputTokens: row.input_tokens,
|
|
64
|
+
outputTokens: row.output_tokens,
|
|
65
|
+
costUsd: row.cost_usd,
|
|
66
|
+
unpricedCalls: row.unpriced,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
logger.warn({ err }, "could not read AI usage");
|
|
71
|
+
return { calls: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, unpricedCalls: 0 };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Spend since the first of the current month, in the user's own local time.
|
|
76
|
+
*
|
|
77
|
+
* A rolling thirty days would be a different and worse number: a monthly cap
|
|
78
|
+
* that resets on a sliding window never resets, and the user compares this
|
|
79
|
+
* against a bill that runs calendar months.
|
|
80
|
+
*/
|
|
81
|
+
export function monthToDateSpend() {
|
|
82
|
+
const start = DateTime.local().startOf("month").toSeconds();
|
|
83
|
+
return aiSpendSince(Math.floor(start));
|
|
84
|
+
}
|
|
85
|
+
export function budgetState() {
|
|
86
|
+
const cap = getProfile().budget.monthlyUsd;
|
|
87
|
+
const spend = monthToDateSpend();
|
|
88
|
+
if (cap === null) {
|
|
89
|
+
return {
|
|
90
|
+
capUsd: null,
|
|
91
|
+
spentUsd: spend.costUsd,
|
|
92
|
+
remainingUsd: null,
|
|
93
|
+
exceeded: false,
|
|
94
|
+
incomplete: spend.unpricedCalls > 0,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const remaining = Math.max(0, cap - spend.costUsd);
|
|
98
|
+
return {
|
|
99
|
+
capUsd: cap,
|
|
100
|
+
spentUsd: spend.costUsd,
|
|
101
|
+
remainingUsd: remaining,
|
|
102
|
+
exceeded: spend.costUsd >= cap,
|
|
103
|
+
incomplete: spend.unpricedCalls > 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Thrown when a request is refused because the user's own cap is reached. */
|
|
107
|
+
export class BudgetExceededError extends Error {
|
|
108
|
+
constructor(state) {
|
|
109
|
+
super(`Monthly AI budget of $${state.capUsd?.toFixed(2)} reached ` +
|
|
110
|
+
`($${state.spentUsd.toFixed(2)} spent). Raise or clear the cap in the dashboard.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Throws when a cap is set and already reached. No cap means never throws. */
|
|
114
|
+
export function assertWithinBudget() {
|
|
115
|
+
const state = budgetState();
|
|
116
|
+
if (state.exceeded) {
|
|
117
|
+
throw new BudgetExceededError(state);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** The most recent calls, newest first — the ledger behind the total. */
|
|
121
|
+
export function recentAiUsage(limit = 20) {
|
|
122
|
+
try {
|
|
123
|
+
const rows = getAppDb()
|
|
124
|
+
.prepare(`SELECT at, kind, model, source, input_tokens, output_tokens, cost_usd
|
|
125
|
+
FROM ai_usage
|
|
126
|
+
ORDER BY at DESC, id DESC
|
|
127
|
+
LIMIT ?`)
|
|
128
|
+
.all(limit);
|
|
129
|
+
return rows.map((row) => ({
|
|
130
|
+
at: row.at,
|
|
131
|
+
kind: row.kind,
|
|
132
|
+
model: row.model,
|
|
133
|
+
source: row.source,
|
|
134
|
+
inputTokens: row.input_tokens,
|
|
135
|
+
outputTokens: row.output_tokens,
|
|
136
|
+
costUsd: row.cost_usd,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger.warn({ err }, "could not read recent AI usage");
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Cost per day for the last `days` days, including the days that cost nothing.
|
|
146
|
+
*
|
|
147
|
+
* The zero days are the point. A chart built only from rows that exist draws a
|
|
148
|
+
* continuous line through a week the user never opened the app, which reads as
|
|
149
|
+
* steady daily spend rather than as the four days it actually was.
|
|
150
|
+
*/
|
|
151
|
+
export function dailyAiSpend(days = 30) {
|
|
152
|
+
const today = DateTime.local().startOf("day");
|
|
153
|
+
const byDay = new Map();
|
|
154
|
+
for (let i = days - 1; i >= 0; i -= 1) {
|
|
155
|
+
const day = today.minus({ days: i }).toISODate();
|
|
156
|
+
if (day) {
|
|
157
|
+
byDay.set(day, { day, costUsd: 0, calls: 0 });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const since = Math.floor(today.minus({ days: days - 1 }).toSeconds());
|
|
162
|
+
const rows = getAppDb()
|
|
163
|
+
.prepare("SELECT at, cost_usd FROM ai_usage WHERE at >= ?")
|
|
164
|
+
.all(since);
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
const day = DateTime.fromSeconds(row.at).toISODate();
|
|
167
|
+
const bucket = day ? byDay.get(day) : undefined;
|
|
168
|
+
if (bucket) {
|
|
169
|
+
bucket.costUsd += row.cost_usd ?? 0;
|
|
170
|
+
bucket.calls += 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
logger.warn({ err }, "could not read daily AI spend");
|
|
176
|
+
}
|
|
177
|
+
return [...byDay.values()];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Count one use of one feature, today.
|
|
181
|
+
*
|
|
182
|
+
* Everything about this is best-effort. It is called from render paths and
|
|
183
|
+
* request handlers that have real work to do, and a counter that can throw is
|
|
184
|
+
* a counter that takes down the feature it was measuring.
|
|
185
|
+
*/
|
|
186
|
+
export function recordFeature(name) {
|
|
187
|
+
try {
|
|
188
|
+
if (!getProfile().analytics.enabled) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const day = DateTime.local().toISODate();
|
|
192
|
+
if (!day) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const trimmed = name.slice(0, 64);
|
|
196
|
+
getAppDb()
|
|
197
|
+
.prepare(`INSERT INTO feature_usage (name, day, count) VALUES (?, ?, 1)
|
|
198
|
+
ON CONFLICT(name, day) DO UPDATE SET count = count + 1`)
|
|
199
|
+
.run(trimmed, day);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
logger.warn({ err }, "could not record feature usage");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export function featureCounts(days = 30) {
|
|
206
|
+
try {
|
|
207
|
+
const since = DateTime.local().minus({ days }).toISODate();
|
|
208
|
+
if (!since) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
return getAppDb()
|
|
212
|
+
.prepare(`SELECT name, SUM(count) AS count
|
|
213
|
+
FROM feature_usage
|
|
214
|
+
WHERE day >= ?
|
|
215
|
+
GROUP BY name
|
|
216
|
+
ORDER BY count DESC, name ASC`)
|
|
217
|
+
.all(since);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
logger.warn({ err }, "could not read feature usage");
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Delete every stored feature counter.
|
|
226
|
+
*
|
|
227
|
+
* Offered because "stop counting" and "forget what you counted" are different
|
|
228
|
+
* requests, and a privacy control that only does the first is the kind that
|
|
229
|
+
* gets described as a privacy control and is not one. AI spend is deliberately
|
|
230
|
+
* NOT cleared here: that is the user's own billing record against a cap they
|
|
231
|
+
* set, and losing it would make the cap wrong for the rest of the month.
|
|
232
|
+
*/
|
|
233
|
+
export function clearFeatureUsage() {
|
|
234
|
+
try {
|
|
235
|
+
getAppDb().prepare("DELETE FROM feature_usage").run();
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
logger.warn({ err }, "could not clear feature usage");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Tests only. */
|
|
242
|
+
export function __clearUsageForTests() {
|
|
243
|
+
const db = getAppDb();
|
|
244
|
+
db.prepare("DELETE FROM ai_usage").run();
|
|
245
|
+
db.prepare("DELETE FROM feature_usage").run();
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AA8D3C;;;;;;GAMG;AACH,MAAM,eAAe,GAAsD;IACzE,kBAAkB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;IAC/C,iBAAiB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAC/C,eAAe,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;CAC9C,CAAC;AAEF,+EAA+E;AAC/E,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAEpC,8DAA8D;AAC9D,MAAM,UAAU,OAAO,CAAC,KAAa,EAAE,MAAmB;IACxD,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,OAAO,GAAG,SAAS,CAAC;IAC1D,OAAO,CACL,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QACzC,MAAM,CAAC,YAAY,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;QAC3C,CAAC,MAAM,CAAC,eAAe,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,qBAAqB;QAC5E,CAAC,MAAM,CAAC,gBAAgB,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,sBAAsB,CAC/E,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAmB;IAC/C,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAE5C,MAAM,GAAG,GAAe,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;IAElD,IAAI,CAAC;QACH,QAAQ,EAAE;aACP,OAAO,CACN;;;4CAGoC,CACrC;aACA,GAAG,CACF,EAAE,EACF,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,MAAM,EACZ,KAAK,CAAC,WAAW,EACjB,KAAK,CAAC,YAAY,EAClB,KAAK,CAAC,eAAe,IAAI,CAAC,EAC1B,KAAK,CAAC,gBAAgB,IAAI,CAAC,EAC3B,OAAO,CACR,CAAC;IACN,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,kEAAkE;QAClE,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,2BAA2B,CAAC,CAAC;IACpD,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,gBAAwB;IACnD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,QAAQ,EAAE;aACnB,OAAO,CACN;;;;;;wBAMgB,CACjB;aACA,GAAG,CAAC,gBAAgB,CAMtB,CAAC;QAEF,OAAO;YACL,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,WAAW,EAAE,GAAG,CAAC,YAAY;YAC7B,YAAY,EAAE,GAAG,CAAC,aAAa;YAC/B,OAAO,EAAE,GAAG,CAAC,QAAQ;YACrB,aAAa,EAAE,GAAG,CAAC,QAAQ;SAC5B,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,yBAAyB,CAAC,CAAC;QAChD,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;IACrF,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC;IAC5D,OAAO,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC;IAC3C,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IAEjC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,KAAK,CAAC,OAAO;YACvB,YAAY,EAAE,IAAI;YAClB,QAAQ,EAAE,KAAK;YACf,UAAU,EAAE,KAAK,CAAC,aAAa,GAAG,CAAC;SACpC,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IACnD,OAAO;QACL,MAAM,EAAE,GAAG;QACX,QAAQ,EAAE,KAAK,CAAC,OAAO;QACvB,YAAY,EAAE,SAAS;QACvB,QAAQ,EAAE,KAAK,CAAC,OAAO,IAAI,GAAG;QAC9B,UAAU,EAAE,KAAK,CAAC,aAAa,GAAG,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,KAAkB;QAC5B,KAAK,CACH,yBAAyB,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,WAAW;YAC1D,KAAK,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,mDAAmD,CACpF,CAAC;IACJ,CAAC;CACF;AAED,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB;IAChC,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;IAC5B,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACnB,MAAM,IAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC;AACH,CAAC;AAYD,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAAC,KAAK,GAAG,EAAE;IACtC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,EAAE;aACpB,OAAO,CACN;;;kBAGU,CACX;aACA,GAAG,CAAC,KAAK,CAQT,CAAC;QAEJ,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACxB,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,WAAW,EAAE,GAAG,CAAC,YAAY;YAC7B,YAAY,EAAE,GAAG,CAAC,aAAa;YAC/B,OAAO,EAAE,GAAG,CAAC,QAAQ;SACtB,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,gCAAgC,CAAC,CAAC;QACvD,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAQD;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAI,GAAG,EAAE;IACpC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAE5C,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;QACjD,IAAI,GAAG,EAAE,CAAC;YACR,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;QACtE,MAAM,IAAI,GAAG,QAAQ,EAAE;aACpB,OAAO,CAAC,iDAAiD,CAAC;aAC1D,GAAG,CAAC,KAAK,CAA8C,CAAC;QAE3D,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;YACrD,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAChD,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,CAAC,OAAO,IAAI,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC;gBACpC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,+BAA+B,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAC7B,CAAC;AAOD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YACpC,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,CAAC;QACzC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClC,QAAQ,EAAE;aACP,OAAO,CACN;gEACwD,CACzD;aACA,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACvB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,gCAAgC,CAAC,CAAC;IACzD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAI,GAAG,EAAE;IACrC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;QAC3D,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,QAAQ,EAAE;aACd,OAAO,CACN;;;;wCAIgC,CACjC;aACA,GAAG,CAAC,KAAK,CAAmB,CAAC;IAClC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,8BAA8B,CAAC,CAAC;QACrD,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC;QACH,QAAQ,EAAE,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC,GAAG,EAAE,CAAC;IACxD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,+BAA+B,CAAC,CAAC;IACxD,CAAC;AACH,CAAC;AAED,kBAAkB;AAClB,MAAM,UAAU,oBAAoB;IAClC,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAC;IACtB,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC,GAAG,EAAE,CAAC;IACzC,EAAE,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC,GAAG,EAAE,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/utils/batch.ts"],"names":[],"mappings":"AAEA,wBAAsB,YAAY,CAAC,CAAC,EAAE,CAAC,EACrC,KAAK,EAAE,CAAC,EAAE,EACV,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAC3B,WAAW,SAAsB,GAChC,OAAO,CAAC,CAAC,EAAE,CAAC,CAkBd"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const DEFAULT_CONCURRENCY = 6;
|
|
2
|
+
export async function mapInBatches(items, fn, concurrency = DEFAULT_CONCURRENCY) {
|
|
3
|
+
if (items.length === 0) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const results = new Array(items.length);
|
|
7
|
+
let index = 0;
|
|
8
|
+
async function worker() {
|
|
9
|
+
while (index < items.length) {
|
|
10
|
+
const current = index++;
|
|
11
|
+
results[current] = await fn(items[current]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
15
|
+
await Promise.all(workers);
|
|
16
|
+
return results;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=batch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.js","sourceRoot":"","sources":["../../src/utils/batch.ts"],"names":[],"mappings":"AAAA,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAU,EACV,EAA2B,EAC3B,WAAW,GAAG,mBAAmB;IAEjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,OAAO,GAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,KAAK,UAAU,MAAM;QACnB,OAAO,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;YACxB,OAAO,CAAC,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,CAAM,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5F,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC3B,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
import type { ActivitySummary } from "../garmin/types.js";
|
|
3
|
+
export type TrendDirection = "improving" | "declining" | "stable" | "insufficient_data";
|
|
4
|
+
export declare function parseIsoDate(value: string): Date;
|
|
5
|
+
export declare function formatIsoDate(date: Date): string;
|
|
6
|
+
export declare function getDateRange(days: number): Date[];
|
|
7
|
+
export declare function getYesterday(): Date;
|
|
8
|
+
export declare function getDatesBetween(startDate: string, endDate: string): Date[];
|
|
9
|
+
export declare function parseActivityLocalDateTime(value: string): DateTime;
|
|
10
|
+
export declare function filterActivitiesByRange(activities: ActivitySummary[], startDate: string, endDate: string): ActivitySummary[];
|
|
11
|
+
export declare function formatDuration(seconds: number | null | undefined): string;
|
|
12
|
+
/**
|
|
13
|
+
* Distance in the units the user thinks in.
|
|
14
|
+
*
|
|
15
|
+
* The unit is a parameter rather than a lookup so these stay pure functions --
|
|
16
|
+
* every caller already sits inside a tool that knows the profile, and reaching
|
|
17
|
+
* for a database from a string formatter is how a formatter becomes untestable.
|
|
18
|
+
*
|
|
19
|
+
* This exists because the setting shipped before the conversion did: the
|
|
20
|
+
* profile accepted `imperial`, the system prompt told the model to answer in
|
|
21
|
+
* miles, and every number it was given was still in kilometres. A setting that
|
|
22
|
+
* claims to change the units and does not is worse than no setting -- the
|
|
23
|
+
* reader has no way to tell which one they are looking at.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatDistanceMeters(meters: number | null | undefined, units?: "metric" | "imperial"): string;
|
|
26
|
+
export declare function formatPaceMetersPerSecond(metersPerSecond: number | null | undefined, units?: "metric" | "imperial"): string;
|
|
27
|
+
export declare function calculateTrend(values: number[], lowerIsBetter?: boolean): TrendDirection;
|
|
28
|
+
export declare function average(values: number[]): number;
|
|
29
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
30
|
+
export declare function hashParams(params: Record<string, unknown>): string;
|
|
31
|
+
export declare function sanitizeErrorMessage(message: string): string;
|
|
32
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,mBAAmB,CAAC;AAuBxF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAMhD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAEhD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,CASjD;AAED,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,CAqB1E;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAYlE;AAED,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,eAAe,EAAE,EAC7B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,eAAe,EAAE,CAYnB;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAkBzE;AAKD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,KAAK,GAAE,QAAQ,GAAG,UAAqB,GACtC,MAAM,CAkBR;AAED,wBAAgB,yBAAyB,CACvC,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC1C,KAAK,GAAE,QAAQ,GAAG,UAAqB,GACtC,MAAM,CAsBR;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,aAAa,UAAQ,GAAG,cAAc,CA6BtF;AAED,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAMhD;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAGlE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK5D"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
// SECTION: Calendar days
|
|
3
|
+
//
|
|
4
|
+
// Every Date in this file is LOCAL midnight for the day it names, not UTC
|
|
5
|
+
// midnight.
|
|
6
|
+
//
|
|
7
|
+
// That is not a style choice. garmin-connect turns a Date into the day it asks
|
|
8
|
+
// Garmin about with `toDateString`, which subtracts getTimezoneOffset() and
|
|
9
|
+
// then reads the UTC date -- a function that round-trips a LOCAL-midnight Date
|
|
10
|
+
// and shifts a UTC-midnight one. Feeding it UTC midnight therefore asked for the
|
|
11
|
+
// wrong calendar day everywhere west of UTC: at UTC-5, `new Date("2026-08-19")`
|
|
12
|
+
// is 2026-08-18T19:00 local, and the library asked Garmin for the 18th while
|
|
13
|
+
// this code stored the answer under the 19th. Every daily metric -- sleep,
|
|
14
|
+
// heart rate, stress, weight -- was off by one day for those users, silently,
|
|
15
|
+
// with plausible numbers under the wrong dates.
|
|
16
|
+
//
|
|
17
|
+
// It never showed up here because the machine this was written on is UTC+4,
|
|
18
|
+
// where the shift lands inside the same day.
|
|
19
|
+
//
|
|
20
|
+
// A calendar day is a local question anyway: "how did I sleep on Tuesday" means
|
|
21
|
+
// the user's Tuesday.
|
|
22
|
+
export function parseIsoDate(value) {
|
|
23
|
+
const parsed = DateTime.fromISO(value);
|
|
24
|
+
if (!parsed.isValid) {
|
|
25
|
+
throw new Error(`Invalid date "${value}". Use ISO 8601 format such as 2026-06-01.`);
|
|
26
|
+
}
|
|
27
|
+
return parsed.toJSDate();
|
|
28
|
+
}
|
|
29
|
+
export function formatIsoDate(date) {
|
|
30
|
+
return DateTime.fromJSDate(date).toISODate() ?? "";
|
|
31
|
+
}
|
|
32
|
+
export function getDateRange(days) {
|
|
33
|
+
const dates = [];
|
|
34
|
+
const today = DateTime.local().startOf("day");
|
|
35
|
+
for (let offset = 0; offset < days; offset += 1) {
|
|
36
|
+
dates.push(today.minus({ days: offset }).toJSDate());
|
|
37
|
+
}
|
|
38
|
+
return dates;
|
|
39
|
+
}
|
|
40
|
+
export function getYesterday() {
|
|
41
|
+
return DateTime.local().minus({ days: 1 }).startOf("day").toJSDate();
|
|
42
|
+
}
|
|
43
|
+
export function getDatesBetween(startDate, endDate) {
|
|
44
|
+
const start = DateTime.fromISO(startDate).startOf("day");
|
|
45
|
+
const end = DateTime.fromISO(endDate).startOf("day");
|
|
46
|
+
if (!start.isValid || !end.isValid) {
|
|
47
|
+
throw new Error("Invalid date range. Use ISO 8601 dates such as 2026-06-01.");
|
|
48
|
+
}
|
|
49
|
+
if (end < start) {
|
|
50
|
+
throw new Error("end_date must be on or after start_date.");
|
|
51
|
+
}
|
|
52
|
+
const dates = [];
|
|
53
|
+
let cursor = start;
|
|
54
|
+
while (cursor <= end) {
|
|
55
|
+
dates.push(cursor.toJSDate());
|
|
56
|
+
cursor = cursor.plus({ days: 1 });
|
|
57
|
+
}
|
|
58
|
+
return dates;
|
|
59
|
+
}
|
|
60
|
+
export function parseActivityLocalDateTime(value) {
|
|
61
|
+
const fromIso = DateTime.fromISO(value, { setZone: true });
|
|
62
|
+
if (fromIso.isValid) {
|
|
63
|
+
return fromIso;
|
|
64
|
+
}
|
|
65
|
+
const fromSql = DateTime.fromSQL(value, { setZone: true });
|
|
66
|
+
if (fromSql.isValid) {
|
|
67
|
+
return fromSql;
|
|
68
|
+
}
|
|
69
|
+
return DateTime.invalid("unparsable");
|
|
70
|
+
}
|
|
71
|
+
export function filterActivitiesByRange(activities, startDate, endDate) {
|
|
72
|
+
const start = DateTime.fromISO(startDate).startOf("day");
|
|
73
|
+
const end = DateTime.fromISO(endDate).startOf("day");
|
|
74
|
+
return activities.filter((activity) => {
|
|
75
|
+
const activityDay = parseActivityLocalDateTime(activity.startTimeLocal).startOf("day");
|
|
76
|
+
if (!activityDay.isValid) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return activityDay.toMillis() >= start.toMillis() && activityDay.toMillis() <= end.toMillis();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function formatDuration(seconds) {
|
|
83
|
+
if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) {
|
|
84
|
+
return "n/a";
|
|
85
|
+
}
|
|
86
|
+
const hours = Math.floor(seconds / 3600);
|
|
87
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
88
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
|
89
|
+
if (hours > 0) {
|
|
90
|
+
return `${hours}h ${minutes}m ${remainingSeconds}s`;
|
|
91
|
+
}
|
|
92
|
+
if (minutes > 0) {
|
|
93
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
94
|
+
}
|
|
95
|
+
return `${remainingSeconds}s`;
|
|
96
|
+
}
|
|
97
|
+
const METRES_PER_MILE = 1609.344;
|
|
98
|
+
const METRES_PER_FOOT = 0.3048;
|
|
99
|
+
/**
|
|
100
|
+
* Distance in the units the user thinks in.
|
|
101
|
+
*
|
|
102
|
+
* The unit is a parameter rather than a lookup so these stay pure functions --
|
|
103
|
+
* every caller already sits inside a tool that knows the profile, and reaching
|
|
104
|
+
* for a database from a string formatter is how a formatter becomes untestable.
|
|
105
|
+
*
|
|
106
|
+
* This exists because the setting shipped before the conversion did: the
|
|
107
|
+
* profile accepted `imperial`, the system prompt told the model to answer in
|
|
108
|
+
* miles, and every number it was given was still in kilometres. A setting that
|
|
109
|
+
* claims to change the units and does not is worse than no setting -- the
|
|
110
|
+
* reader has no way to tell which one they are looking at.
|
|
111
|
+
*/
|
|
112
|
+
export function formatDistanceMeters(meters, units = "metric") {
|
|
113
|
+
if (meters === null || meters === undefined || !Number.isFinite(meters)) {
|
|
114
|
+
return "n/a";
|
|
115
|
+
}
|
|
116
|
+
if (units === "imperial") {
|
|
117
|
+
// A quarter of a mile is the point below which a runner thinks in feet.
|
|
118
|
+
if (meters >= METRES_PER_MILE / 4) {
|
|
119
|
+
return `${(meters / METRES_PER_MILE).toFixed(2)} mi`;
|
|
120
|
+
}
|
|
121
|
+
return `${(meters / METRES_PER_FOOT).toFixed(0)} ft`;
|
|
122
|
+
}
|
|
123
|
+
if (meters >= 1000) {
|
|
124
|
+
return `${(meters / 1000).toFixed(2)} km`;
|
|
125
|
+
}
|
|
126
|
+
return `${meters.toFixed(0)} m`;
|
|
127
|
+
}
|
|
128
|
+
export function formatPaceMetersPerSecond(metersPerSecond, units = "metric") {
|
|
129
|
+
if (metersPerSecond === null ||
|
|
130
|
+
metersPerSecond === undefined ||
|
|
131
|
+
!Number.isFinite(metersPerSecond) ||
|
|
132
|
+
metersPerSecond <= 0) {
|
|
133
|
+
return "n/a";
|
|
134
|
+
}
|
|
135
|
+
const perUnit = units === "imperial" ? METRES_PER_MILE : 1000;
|
|
136
|
+
const secondsPerUnit = perUnit / metersPerSecond;
|
|
137
|
+
const minutes = Math.floor(secondsPerUnit / 60);
|
|
138
|
+
const seconds = Math.round(secondsPerUnit % 60);
|
|
139
|
+
// 4:60 /km is not a pace. Rounding the seconds can carry, and the carry has
|
|
140
|
+
// to reach the minutes or the string is wrong once every sixty paces.
|
|
141
|
+
const carried = seconds === 60;
|
|
142
|
+
const finalMinutes = carried ? minutes + 1 : minutes;
|
|
143
|
+
const finalSeconds = carried ? 0 : seconds;
|
|
144
|
+
return `${finalMinutes}:${finalSeconds.toString().padStart(2, "0")} ${units === "imperial" ? "/mi" : "/km"}`;
|
|
145
|
+
}
|
|
146
|
+
export function calculateTrend(values, lowerIsBetter = false) {
|
|
147
|
+
const filtered = values.filter((value) => Number.isFinite(value));
|
|
148
|
+
if (filtered.length < 2) {
|
|
149
|
+
return "insufficient_data";
|
|
150
|
+
}
|
|
151
|
+
const midpoint = Math.floor(filtered.length / 2);
|
|
152
|
+
const recent = filtered.slice(0, midpoint);
|
|
153
|
+
const older = filtered.slice(midpoint);
|
|
154
|
+
if (recent.length === 0 || older.length === 0) {
|
|
155
|
+
return "insufficient_data";
|
|
156
|
+
}
|
|
157
|
+
const recentAverage = average(recent);
|
|
158
|
+
const olderAverage = average(older);
|
|
159
|
+
const delta = recentAverage - olderAverage;
|
|
160
|
+
const threshold = Math.max(Math.abs(olderAverage) * 0.02, 0.5);
|
|
161
|
+
if (Math.abs(delta) <= threshold) {
|
|
162
|
+
return "stable";
|
|
163
|
+
}
|
|
164
|
+
if (lowerIsBetter) {
|
|
165
|
+
return delta < 0 ? "improving" : "declining";
|
|
166
|
+
}
|
|
167
|
+
return delta > 0 ? "improving" : "declining";
|
|
168
|
+
}
|
|
169
|
+
export function average(values) {
|
|
170
|
+
if (values.length === 0) {
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
174
|
+
}
|
|
175
|
+
export function clamp(value, min, max) {
|
|
176
|
+
return Math.min(max, Math.max(min, value));
|
|
177
|
+
}
|
|
178
|
+
export function hashParams(params) {
|
|
179
|
+
const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right));
|
|
180
|
+
return JSON.stringify(Object.fromEntries(sortedEntries));
|
|
181
|
+
}
|
|
182
|
+
export function sanitizeErrorMessage(message) {
|
|
183
|
+
return message
|
|
184
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, "[email]")
|
|
185
|
+
.replace(/(?:[A-Za-z]:\\|\/)[^\s]+/g, "[path]")
|
|
186
|
+
.replace(/\bpassword[^\s]*/gi, "[redacted]");
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows PowerShell 5.1's `Set-Content -Encoding utf8` writes a BOM, and
|
|
3
|
+
* `JSON.parse` treats a leading U+FEFF as a syntax error. Every JSON file this
|
|
4
|
+
* project reads can have been written or edited by a Windows tool: the watch
|
|
5
|
+
* setup file is written by `scripts/start-watch-stack.ps1`, and Claude Desktop
|
|
6
|
+
* and Cursor both ship BOM'd config on Windows.
|
|
7
|
+
*
|
|
8
|
+
* This was not theoretical. `.trainbud/watch-setup.json` held the correct
|
|
9
|
+
* tunnel URL, and `appConfig.publicUrl` returned "" for it, because the parse
|
|
10
|
+
* threw on the BOM and the getter's `catch` swallowed the reason. `trainbud
|
|
11
|
+
* doctor` then reported "No public URL is configured, so the watch has no
|
|
12
|
+
* address to call" while the tunnel was up and answering 200 — a diagnostic
|
|
13
|
+
* naming the wrong fault, about the one file whose whole job is to name an
|
|
14
|
+
* address.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stripBom(text: string): string;
|
|
17
|
+
/** Read and parse a JSON file, tolerating a byte order mark. Throws like JSON.parse. */
|
|
18
|
+
export declare function readJsonFile<T>(filePath: string): T;
|
|
19
|
+
//# sourceMappingURL=jsonFile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonFile.d.ts","sourceRoot":"","sources":["../../src/utils/jsonFile.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAEnD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Windows PowerShell 5.1's `Set-Content -Encoding utf8` writes a BOM, and
|
|
4
|
+
* `JSON.parse` treats a leading U+FEFF as a syntax error. Every JSON file this
|
|
5
|
+
* project reads can have been written or edited by a Windows tool: the watch
|
|
6
|
+
* setup file is written by `scripts/start-watch-stack.ps1`, and Claude Desktop
|
|
7
|
+
* and Cursor both ship BOM'd config on Windows.
|
|
8
|
+
*
|
|
9
|
+
* This was not theoretical. `.trainbud/watch-setup.json` held the correct
|
|
10
|
+
* tunnel URL, and `appConfig.publicUrl` returned "" for it, because the parse
|
|
11
|
+
* threw on the BOM and the getter's `catch` swallowed the reason. `trainbud
|
|
12
|
+
* doctor` then reported "No public URL is configured, so the watch has no
|
|
13
|
+
* address to call" while the tunnel was up and answering 200 — a diagnostic
|
|
14
|
+
* naming the wrong fault, about the one file whose whole job is to name an
|
|
15
|
+
* address.
|
|
16
|
+
*/
|
|
17
|
+
export function stripBom(text) {
|
|
18
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
19
|
+
}
|
|
20
|
+
/** Read and parse a JSON file, tolerating a byte order mark. Throws like JSON.parse. */
|
|
21
|
+
export function readJsonFile(filePath) {
|
|
22
|
+
return JSON.parse(stripBom(fs.readFileSync(filePath, "utf8")));
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=jsonFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonFile.js","sourceRoot":"","sources":["../../src/utils/jsonFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAI,QAAgB;IAC9C,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAM,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAiGnC,wBAAgB,eAAe,CAAC,OAAO,SAAoB,GAAG,IAAI,CAEjE;AAED,eAAO,MAAM,MAAM,EAAE,MAMnB,CAAC"}
|