matrix-workouts-core 0.1.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/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/MATRIX_API.md +245 -0
- package/README.md +123 -0
- package/dist/api/client.d.ts +63 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +112 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/credentials.d.ts +24 -0
- package/dist/api/credentials.d.ts.map +1 -0
- package/dist/api/credentials.js +32 -0
- package/dist/api/credentials.js.map +1 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +4 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/login.d.ts +43 -0
- package/dist/api/login.d.ts.map +1 -0
- package/dist/api/login.js +97 -0
- package/dist/api/login.js.map +1 -0
- package/dist/export/document.d.ts +108 -0
- package/dist/export/document.d.ts.map +1 -0
- package/dist/export/document.js +97 -0
- package/dist/export/document.js.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +2 -0
- package/dist/export/index.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/node/bin/history.d.ts +3 -0
- package/dist/node/bin/history.d.ts.map +1 -0
- package/dist/node/bin/history.js +80 -0
- package/dist/node/bin/history.js.map +1 -0
- package/dist/node/history.d.ts +60 -0
- package/dist/node/history.d.ts.map +1 -0
- package/dist/node/history.js +126 -0
- package/dist/node/history.js.map +1 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/dist/parse/controlSignature.d.ts +38 -0
- package/dist/parse/controlSignature.d.ts.map +1 -0
- package/dist/parse/controlSignature.js +42 -0
- package/dist/parse/controlSignature.js.map +1 -0
- package/dist/parse/heartRate.d.ts +78 -0
- package/dist/parse/heartRate.d.ts.map +1 -0
- package/dist/parse/heartRate.js +57 -0
- package/dist/parse/heartRate.js.map +1 -0
- package/dist/parse/index.d.ts +30 -0
- package/dist/parse/index.d.ts.map +1 -0
- package/dist/parse/index.js +33 -0
- package/dist/parse/index.js.map +1 -0
- package/dist/parse/machine.d.ts +42 -0
- package/dist/parse/machine.d.ts.map +1 -0
- package/dist/parse/machine.js +65 -0
- package/dist/parse/machine.js.map +1 -0
- package/dist/parse/persist.d.ts +13 -0
- package/dist/parse/persist.d.ts.map +1 -0
- package/dist/parse/persist.js +40 -0
- package/dist/parse/persist.js.map +1 -0
- package/dist/parse/program.d.ts +58 -0
- package/dist/parse/program.d.ts.map +1 -0
- package/dist/parse/program.js +51 -0
- package/dist/parse/program.js.map +1 -0
- package/dist/parse/types.d.ts +127 -0
- package/dist/parse/types.d.ts.map +1 -0
- package/dist/parse/types.js +5 -0
- package/dist/parse/types.js.map +1 -0
- package/dist/parse/workout.d.ts +22 -0
- package/dist/parse/workout.d.ts.map +1 -0
- package/dist/parse/workout.js +130 -0
- package/dist/parse/workout.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type RawWorkout } from "./types.js";
|
|
2
|
+
/** localStorage key the site's redux-persist store lives under. */
|
|
3
|
+
export declare const PERSIST_KEY = "root";
|
|
4
|
+
/**
|
|
5
|
+
* The persisted blob is a JSON object whose *values are themselves JSON strings*.
|
|
6
|
+
* Missing that double encoding is the single easiest way to get this wrong.
|
|
7
|
+
*/
|
|
8
|
+
export type PersistRoot = Record<string, unknown>;
|
|
9
|
+
/** Parse the outer blob and the inner JSON-string slice named by `slice`. */
|
|
10
|
+
export declare function parsePersistSlice(raw: string | null | undefined, slice: string): PersistRoot;
|
|
11
|
+
/** Pull the raw workout array out of the persisted blob. Absent/!array -> []. */
|
|
12
|
+
export declare function extractRawWorkouts(raw: string | null | undefined): RawWorkout[];
|
|
13
|
+
//# sourceMappingURL=persist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/parse/persist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEhE,mEAAmE;AACnE,eAAO,MAAM,WAAW,SAAS,CAAC;AAElC;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAUlD,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAoB5F;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,EAAE,CAO/E"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { WorkoutParseError } from "./types.js";
|
|
2
|
+
/** localStorage key the site's redux-persist store lives under. */
|
|
3
|
+
export const PERSIST_KEY = "root";
|
|
4
|
+
function parseJson(raw, what) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(raw);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
throw new WorkoutParseError(`${what} is not valid JSON.`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Parse the outer blob and the inner JSON-string slice named by `slice`. */
|
|
13
|
+
export function parsePersistSlice(raw, slice) {
|
|
14
|
+
if (raw == null || raw === "") {
|
|
15
|
+
throw new WorkoutParseError(`No workout data found in this browser (localStorage "${PERSIST_KEY}" is empty). Open a workout on the site first.`);
|
|
16
|
+
}
|
|
17
|
+
const outer = parseJson(raw, `localStorage "${PERSIST_KEY}"`);
|
|
18
|
+
if (typeof outer !== "object" || outer === null || Array.isArray(outer)) {
|
|
19
|
+
throw new WorkoutParseError(`localStorage "${PERSIST_KEY}" is not an object.`);
|
|
20
|
+
}
|
|
21
|
+
const value = outer[slice];
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
throw new WorkoutParseError(`"${slice}" is missing from the stored data.`);
|
|
24
|
+
}
|
|
25
|
+
// Slices are normally JSON strings, but tolerate an already-parsed object.
|
|
26
|
+
const inner = typeof value === "string" ? parseJson(value, `"${slice}"`) : value;
|
|
27
|
+
if (typeof inner !== "object" || inner === null || Array.isArray(inner)) {
|
|
28
|
+
throw new WorkoutParseError(`"${slice}" did not contain an object.`);
|
|
29
|
+
}
|
|
30
|
+
return inner;
|
|
31
|
+
}
|
|
32
|
+
/** Pull the raw workout array out of the persisted blob. Absent/!array -> []. */
|
|
33
|
+
export function extractRawWorkouts(raw) {
|
|
34
|
+
const userStore = parsePersistSlice(raw, "userStore");
|
|
35
|
+
const workouts = userStore["workouts"];
|
|
36
|
+
if (!Array.isArray(workouts))
|
|
37
|
+
return [];
|
|
38
|
+
return workouts.filter((w) => typeof w === "object" && w !== null && !Array.isArray(w));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=persist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.js","sourceRoot":"","sources":["../../src/parse/persist.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAmB,MAAM,YAAY,CAAC;AAEhE,mEAAmE;AACnE,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC;AAQlC,SAAS,SAAS,CAAC,GAAW,EAAE,IAAY;IAC1C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,iBAAiB,CAAC,GAAG,IAAI,qBAAqB,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,iBAAiB,CAAC,GAA8B,EAAE,KAAa;IAC7E,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QAC9B,MAAM,IAAI,iBAAiB,CACzB,wDAAwD,WAAW,gDAAgD,CACpH,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,iBAAiB,WAAW,GAAG,CAAC,CAAC;IAC9D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,iBAAiB,CAAC,iBAAiB,WAAW,qBAAqB,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,KAAK,GAAI,KAAiC,CAAC,KAAK,CAAC,CAAC;IACxD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,iBAAiB,CAAC,IAAI,KAAK,oCAAoC,CAAC,CAAC;IAC7E,CAAC;IACD,2EAA2E;IAC3E,MAAM,KAAK,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACjF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,iBAAiB,CAAC,IAAI,KAAK,8BAA8B,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,KAAoB,CAAC;AAC9B,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,kBAAkB,CAAC,GAA8B;IAC/D,MAAM,SAAS,GAAG,iBAAiB,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IACxC,OAAO,QAAQ,CAAC,MAAM,CACpB,CAAC,CAAC,EAAmB,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CACjF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console program ("mode") identification.
|
|
3
|
+
*
|
|
4
|
+
* `programType` is the numeric console program id and is the mode marker. Values
|
|
5
|
+
* observed across one account's 44 workouts: 46 (27x), 18 (7x), 20 (4x), 47 (3x),
|
|
6
|
+
* 0 (2x), 38 (1x). Only the ids the rider has confirmed are named here — the rest
|
|
7
|
+
* are deliberately left unmapped rather than guessed at.
|
|
8
|
+
*/
|
|
9
|
+
export type ProgramMode = "sprint_8" | "target_heart_rate" | "target_watts" | "fitness_test" | "virtual_active" | "unknown";
|
|
10
|
+
export declare const PROGRAM_MODES: Readonly<Record<number, ProgramMode>>;
|
|
11
|
+
/**
|
|
12
|
+
* How these were established, and why 0 is still unknown.
|
|
13
|
+
*
|
|
14
|
+
* The rider keeps a Notion training log with a dated row per session. Matching ride
|
|
15
|
+
* dates and durations against it identifies the modes directly, rather than by
|
|
16
|
+
* inference from the telemetry:
|
|
17
|
+
*
|
|
18
|
+
* - **20 = target watts.** All four program-20 rides are explicitly watt-target
|
|
19
|
+
* sessions in the log ("2x18 min at 145-150 W", "4x4 @ 200 W", "2x18 min @
|
|
20
|
+
* 155 W", "2x20 min @ 155 W"). The 2026-08-12 ride matches its log row exactly:
|
|
21
|
+
* 46.07 min, 22.29 km, 129 W average.
|
|
22
|
+
* - **38 = fitness test.** The single program-38 ride matches the log's
|
|
23
|
+
* "Fitness test / indoor bike" row exactly: 2026-07-20, 902 s, 7419 m, 148 W
|
|
24
|
+
* average against a logged 149 W. The console reported a VO2 estimate and
|
|
25
|
+
* "final stage completed: 7", which is why the series shows eight power stages
|
|
26
|
+
* at a fixed resistance.
|
|
27
|
+
* - **46 = target heart rate**, independently corroborated by log entries that
|
|
28
|
+
* name the mode ("Relaxed Zone 2 ride in Target HR mode", "Target HR was 139").
|
|
29
|
+
* - **47 = Virtual Active**, the console's scenic-route mode, where the video's
|
|
30
|
+
* terrain drives resistance and the rider answers it with cadence. Reported by
|
|
31
|
+
* the rider off the console for the 2026-09-13 ride (2403 s, 20.37 km, 145 W
|
|
32
|
+
* average, 241 samples). That is one confirmed ride naming an id, the same
|
|
33
|
+
* standard that pinned 38, and the other two program-47 rides inherit the name
|
|
34
|
+
* by id without a confirmation of their own. Do not try to corroborate that
|
|
35
|
+
* from the series: both 47 rides open 1, 4, 4, 4, ... which looks like a
|
|
36
|
+
* signature until you check `raw-6aa04566...`, a program 46, which opens
|
|
37
|
+
* 1, 4, 4 as well.
|
|
38
|
+
*
|
|
39
|
+
* Program 0 remains "unknown": its rides are in the log but no entry names a console
|
|
40
|
+
* mode, and the telemetry does not separate it. It is plausibly manual / quick-start
|
|
41
|
+
* (that is the usual console convention for id 0, and both rides are short
|
|
42
|
+
* unstructured efforts) but that is a convention, not evidence.
|
|
43
|
+
*
|
|
44
|
+
* A power-plateau heuristic was tried as a way to detect watt-target rides from the
|
|
45
|
+
* data alone and REJECTED: across all 43 rides, program 20 scores 0.50-0.76 but
|
|
46
|
+
* three program-46 rides score 0.51-0.72, so any threshold misclassifies them.
|
|
47
|
+
* Use programType for this, not a derived signal.
|
|
48
|
+
*/
|
|
49
|
+
export declare function programMode(programType: number | null | undefined): ProgramMode;
|
|
50
|
+
/** The eight per-sprint scores from a Sprint 8 (HIIT) ride, in order 1..8. */
|
|
51
|
+
export interface Sprint8Result {
|
|
52
|
+
/** Sum of `scores` — the console's "sweat score". */
|
|
53
|
+
sweatScore: number;
|
|
54
|
+
/** Exactly eight scores, sprint 1 through 8. */
|
|
55
|
+
scores: number[];
|
|
56
|
+
programLevel: number | null;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=program.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/parse/program.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GACnB,UAAU,GACV,mBAAmB,GACnB,cAAc,GACd,cAAc,GACd,gBAAgB,GAChB,SAAS,CAAC;AAEd,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAM9D,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,WAAW,CAG/E;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const PROGRAM_MODES = Object.freeze({
|
|
2
|
+
18: "sprint_8", // carries sprintScores; confirmed by rider
|
|
3
|
+
46: "target_heart_rate", // confirmed by rider and by their training log
|
|
4
|
+
20: "target_watts", // all 4 rides are watt-target sessions in the log
|
|
5
|
+
38: "fitness_test", // console VO2 / Cooper test; exact match in the log
|
|
6
|
+
47: "virtual_active", // confirmed by rider off the console, 13 Sep 2026 ride
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* How these were established, and why 0 is still unknown.
|
|
10
|
+
*
|
|
11
|
+
* The rider keeps a Notion training log with a dated row per session. Matching ride
|
|
12
|
+
* dates and durations against it identifies the modes directly, rather than by
|
|
13
|
+
* inference from the telemetry:
|
|
14
|
+
*
|
|
15
|
+
* - **20 = target watts.** All four program-20 rides are explicitly watt-target
|
|
16
|
+
* sessions in the log ("2x18 min at 145-150 W", "4x4 @ 200 W", "2x18 min @
|
|
17
|
+
* 155 W", "2x20 min @ 155 W"). The 2026-08-12 ride matches its log row exactly:
|
|
18
|
+
* 46.07 min, 22.29 km, 129 W average.
|
|
19
|
+
* - **38 = fitness test.** The single program-38 ride matches the log's
|
|
20
|
+
* "Fitness test / indoor bike" row exactly: 2026-07-20, 902 s, 7419 m, 148 W
|
|
21
|
+
* average against a logged 149 W. The console reported a VO2 estimate and
|
|
22
|
+
* "final stage completed: 7", which is why the series shows eight power stages
|
|
23
|
+
* at a fixed resistance.
|
|
24
|
+
* - **46 = target heart rate**, independently corroborated by log entries that
|
|
25
|
+
* name the mode ("Relaxed Zone 2 ride in Target HR mode", "Target HR was 139").
|
|
26
|
+
* - **47 = Virtual Active**, the console's scenic-route mode, where the video's
|
|
27
|
+
* terrain drives resistance and the rider answers it with cadence. Reported by
|
|
28
|
+
* the rider off the console for the 2026-09-13 ride (2403 s, 20.37 km, 145 W
|
|
29
|
+
* average, 241 samples). That is one confirmed ride naming an id, the same
|
|
30
|
+
* standard that pinned 38, and the other two program-47 rides inherit the name
|
|
31
|
+
* by id without a confirmation of their own. Do not try to corroborate that
|
|
32
|
+
* from the series: both 47 rides open 1, 4, 4, 4, ... which looks like a
|
|
33
|
+
* signature until you check `raw-6aa04566...`, a program 46, which opens
|
|
34
|
+
* 1, 4, 4 as well.
|
|
35
|
+
*
|
|
36
|
+
* Program 0 remains "unknown": its rides are in the log but no entry names a console
|
|
37
|
+
* mode, and the telemetry does not separate it. It is plausibly manual / quick-start
|
|
38
|
+
* (that is the usual console convention for id 0, and both rides are short
|
|
39
|
+
* unstructured efforts) but that is a convention, not evidence.
|
|
40
|
+
*
|
|
41
|
+
* A power-plateau heuristic was tried as a way to detect watt-target rides from the
|
|
42
|
+
* data alone and REJECTED: across all 43 rides, program 20 scores 0.50-0.76 but
|
|
43
|
+
* three program-46 rides score 0.51-0.72, so any threshold misclassifies them.
|
|
44
|
+
* Use programType for this, not a derived signal.
|
|
45
|
+
*/
|
|
46
|
+
export function programMode(programType) {
|
|
47
|
+
if (programType == null)
|
|
48
|
+
return "unknown";
|
|
49
|
+
return PROGRAM_MODES[programType] ?? "unknown";
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=program.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"program.js","sourceRoot":"","sources":["../../src/parse/program.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAC,MAAM,aAAa,GAA0C,MAAM,CAAC,MAAM,CAAC;IAChF,EAAE,EAAE,UAAU,EAAW,2CAA2C;IACpE,EAAE,EAAE,mBAAmB,EAAE,+CAA+C;IACxE,EAAE,EAAE,cAAc,EAAO,kDAAkD;IAC3E,EAAE,EAAE,cAAc,EAAO,oDAAoD;IAC7E,EAAE,EAAE,gBAAgB,EAAK,uDAAuD;CACjF,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,MAAM,UAAU,WAAW,CAAC,WAAsC;IAChE,IAAI,WAAW,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IAC1C,OAAO,aAAa,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ProgramMode, Sprint8Result } from "./program.js";
|
|
2
|
+
/**
|
|
3
|
+
* Types for the jfit workout record.
|
|
4
|
+
*
|
|
5
|
+
* `Raw*` types mirror the upstream shape byte-for-byte, quirks and all. Everything
|
|
6
|
+
* downstream should consume the normalized types, whose field names carry units —
|
|
7
|
+
* upstream names are ambiguous and in one case (`averageDistance`, which is
|
|
8
|
+
* cumulative) actively misleading.
|
|
9
|
+
*/
|
|
10
|
+
/** Machine types seen in the wild. Unknown values are preserved, not rejected. */
|
|
11
|
+
export type KnownMachineType = "upright_bike" | "recumbent_bike" | "treadmill" | "rower" | "elliptical";
|
|
12
|
+
export type MachineType = KnownMachineType | (string & {});
|
|
13
|
+
/** One sample, emitted every 10 seconds by the console. */
|
|
14
|
+
export interface RawInterval {
|
|
15
|
+
/** Seconds covered by this sample: 10 for every sample except the last, which is 0. */
|
|
16
|
+
duration: number;
|
|
17
|
+
/** Distance covered during this sample, meters. Quantized to 0.01 mile. */
|
|
18
|
+
distance: number;
|
|
19
|
+
/** CUMULATIVE distance to this point, meters. The name is a misnomer. */
|
|
20
|
+
averageDistance: number;
|
|
21
|
+
speed: number;
|
|
22
|
+
rpm: number;
|
|
23
|
+
power: number;
|
|
24
|
+
resistance: number;
|
|
25
|
+
heartRate: number;
|
|
26
|
+
incline: number;
|
|
27
|
+
totalSteps: number;
|
|
28
|
+
}
|
|
29
|
+
export interface RawSprintScores {
|
|
30
|
+
[sprint: string]: number;
|
|
31
|
+
}
|
|
32
|
+
export interface RawWorkout {
|
|
33
|
+
workoutId: string;
|
|
34
|
+
/**
|
|
35
|
+
* The record's own document id, and the segment the site's `/workouts/:id` links
|
|
36
|
+
* use. NOT interchangeable with `workoutId` — see `Workout.routeId`.
|
|
37
|
+
*/
|
|
38
|
+
id?: string;
|
|
39
|
+
workoutTime: string;
|
|
40
|
+
machineType: MachineType;
|
|
41
|
+
exerciseTitle?: string;
|
|
42
|
+
workoutType?: string;
|
|
43
|
+
workoutSource?: string;
|
|
44
|
+
machineId?: string;
|
|
45
|
+
programType?: number;
|
|
46
|
+
duration: number;
|
|
47
|
+
distance: number;
|
|
48
|
+
calories?: number;
|
|
49
|
+
minHeartRate?: number;
|
|
50
|
+
maxHeartRate?: number;
|
|
51
|
+
averageHeartRate?: number;
|
|
52
|
+
archived?: number;
|
|
53
|
+
/** Sprint 8 rides only: the console's sweat score (== sum of sprintScores). */
|
|
54
|
+
totalSweatScore?: number;
|
|
55
|
+
/** Sprint 8 rides only: eight scores keyed "1".."8". Its presence identifies the mode. */
|
|
56
|
+
sprintScores?: RawSprintScores;
|
|
57
|
+
programLevel?: number;
|
|
58
|
+
sprint8ProgramLevel?: number;
|
|
59
|
+
intervals?: RawInterval[];
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
}
|
|
62
|
+
/** A normalized sample. Units are in the names; time is elapsed from workout start. */
|
|
63
|
+
export interface Sample {
|
|
64
|
+
elapsedSeconds: number;
|
|
65
|
+
/** Distance covered during this sample, meters. */
|
|
66
|
+
distanceMeters: number;
|
|
67
|
+
/** Cumulative distance at this sample, meters. */
|
|
68
|
+
cumulativeDistanceMeters: number;
|
|
69
|
+
speedKmh: number;
|
|
70
|
+
cadenceRpm: number;
|
|
71
|
+
powerWatts: number;
|
|
72
|
+
/** Console resistance level. Range is machine- and program-dependent (1–13 observed). */
|
|
73
|
+
resistanceLevel: number;
|
|
74
|
+
heartRateBpm: number;
|
|
75
|
+
inclinePercent: number;
|
|
76
|
+
totalSteps: number;
|
|
77
|
+
}
|
|
78
|
+
export interface Workout {
|
|
79
|
+
/** The record's `workoutId`. Fixture filenames and the export are named by it. */
|
|
80
|
+
id: string;
|
|
81
|
+
/**
|
|
82
|
+
* The id the site's own `/workouts/:id` links carry — the record's `id` field.
|
|
83
|
+
*
|
|
84
|
+
* On every ride recorded before 13 Aug 2026 this is a DIFFERENT value from `id`,
|
|
85
|
+
* so anything resolving a URL must go through `findWorkout`, which accepts either.
|
|
86
|
+
* Falls back to `id` for a record that carries no `id` field at all.
|
|
87
|
+
*/
|
|
88
|
+
routeId: string;
|
|
89
|
+
/** Start of the workout. */
|
|
90
|
+
startedAt: Date;
|
|
91
|
+
machineType: MachineType;
|
|
92
|
+
machineId: string | null;
|
|
93
|
+
/** Numeric console program id, e.g. 46. Raw value, always preserved. */
|
|
94
|
+
programType: number | null;
|
|
95
|
+
/** Named mode for the program id, where known. */
|
|
96
|
+
mode: ProgramMode;
|
|
97
|
+
/** Present only on Sprint 8 rides; null otherwise. */
|
|
98
|
+
sprint8: Sprint8Result | null;
|
|
99
|
+
durationSeconds: number;
|
|
100
|
+
distanceMeters: number;
|
|
101
|
+
calories: number | null;
|
|
102
|
+
/**
|
|
103
|
+
* Summary heart rate as reported by the platform. NOTE: these are NOT derived
|
|
104
|
+
* from `samples` and do not always agree with them — compute your own from the
|
|
105
|
+
* series if you need consistency.
|
|
106
|
+
*/
|
|
107
|
+
reported: {
|
|
108
|
+
minHeartRateBpm: number | null;
|
|
109
|
+
maxHeartRateBpm: number | null;
|
|
110
|
+
averageHeartRateBpm: number | null;
|
|
111
|
+
};
|
|
112
|
+
archived: boolean;
|
|
113
|
+
samples: Sample[];
|
|
114
|
+
/**
|
|
115
|
+
* The record exactly as it arrived, untouched: camelCase from localStorage,
|
|
116
|
+
* snake_case from the API. Kept so an export can be lossless — the upstream
|
|
117
|
+
* shape is undocumented and carries fields this model does not name, and
|
|
118
|
+
* dropping them silently would make the exported file a worse record of the
|
|
119
|
+
* ride than the one the browser already had.
|
|
120
|
+
*/
|
|
121
|
+
raw: Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
/** Every parse failure surfaces as this, with a message safe to show a user. */
|
|
124
|
+
export declare class WorkoutParseError extends Error {
|
|
125
|
+
name: string;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/parse/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/parse/types.ts"],"names":[],"mappings":"AAkIA,gFAAgF;AAChF,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,IAAI,GAAG,mBAAmB,CAAC;CACrC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RawWorkout, Workout } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The console records distance in hundredths of a mile and the API converts to
|
|
4
|
+
* meters, so every per-sample distance is a multiple of this. Useful as a sanity
|
|
5
|
+
* check, and it explains why summed samples drift from the reported total.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DISTANCE_QUANTUM_METERS = 16.09;
|
|
8
|
+
/**
|
|
9
|
+
* Nominal seconds between samples. Every sample reports 10 EXCEPT the final one,
|
|
10
|
+
* whose duration is whatever fraction of a sample the ride ended on — observed as
|
|
11
|
+
* 0, 1, 2, 3, 5, 6, 7, 8, 10 and 11 across one account's history. Never assume the
|
|
12
|
+
* last sample is 0, and never derive elapsed time as `index * 10`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SAMPLE_INTERVAL_SECONDS = 10;
|
|
15
|
+
/**
|
|
16
|
+
* The HTTP API returns snake_case; the persisted localStorage blob returns
|
|
17
|
+
* camelCase. Normalize both to camelCase so one code path handles either source.
|
|
18
|
+
*/
|
|
19
|
+
export declare function camelizeWorkout(raw: Record<string, unknown>): RawWorkout;
|
|
20
|
+
/** Normalize one raw record. Accepts either the API or the localStorage shape. */
|
|
21
|
+
export declare function toWorkout(input: Record<string, unknown>): Workout;
|
|
22
|
+
//# sourceMappingURL=workout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workout.d.ts","sourceRoot":"","sources":["../../src/parse/workout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,UAAU,EAAU,OAAO,EAAE,MAAM,YAAY,CAAC;AAG3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,QAAQ,CAAC;AAE7C;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAY1C;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAmBxE;AAkCD,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAoDjE"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { programMode } from "./program.js";
|
|
2
|
+
import { WorkoutParseError } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The console records distance in hundredths of a mile and the API converts to
|
|
5
|
+
* meters, so every per-sample distance is a multiple of this. Useful as a sanity
|
|
6
|
+
* check, and it explains why summed samples drift from the reported total.
|
|
7
|
+
*/
|
|
8
|
+
export const DISTANCE_QUANTUM_METERS = 16.09;
|
|
9
|
+
/**
|
|
10
|
+
* Nominal seconds between samples. Every sample reports 10 EXCEPT the final one,
|
|
11
|
+
* whose duration is whatever fraction of a sample the ride ended on — observed as
|
|
12
|
+
* 0, 1, 2, 3, 5, 6, 7, 8, 10 and 11 across one account's history. Never assume the
|
|
13
|
+
* last sample is 0, and never derive elapsed time as `index * 10`.
|
|
14
|
+
*/
|
|
15
|
+
export const SAMPLE_INTERVAL_SECONDS = 10;
|
|
16
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
17
|
+
const numOr = (v, fallback) => num(v) ?? fallback;
|
|
18
|
+
/** Convert snake_case keys to camelCase, one level deep plus nested intervals. */
|
|
19
|
+
function camelizeKey(key) {
|
|
20
|
+
return key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The HTTP API returns snake_case; the persisted localStorage blob returns
|
|
24
|
+
* camelCase. Normalize both to camelCase so one code path handles either source.
|
|
25
|
+
*/
|
|
26
|
+
export function camelizeWorkout(raw) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
29
|
+
const key = camelizeKey(k);
|
|
30
|
+
if (key === "intervals" && Array.isArray(v)) {
|
|
31
|
+
out[key] = v.map((iv) => {
|
|
32
|
+
const s = {};
|
|
33
|
+
if (iv && typeof iv === "object") {
|
|
34
|
+
for (const [ik, ivv] of Object.entries(iv)) {
|
|
35
|
+
s[camelizeKey(ik)] = ivv;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return s;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
out[key] = v;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function toSample(raw, elapsedSeconds) {
|
|
48
|
+
return {
|
|
49
|
+
elapsedSeconds,
|
|
50
|
+
distanceMeters: numOr(raw.distance, 0),
|
|
51
|
+
cumulativeDistanceMeters: numOr(raw.averageDistance, 0),
|
|
52
|
+
speedKmh: numOr(raw.speed, 0),
|
|
53
|
+
cadenceRpm: numOr(raw.rpm, 0),
|
|
54
|
+
powerWatts: numOr(raw.power, 0),
|
|
55
|
+
resistanceLevel: numOr(raw.resistance, 0),
|
|
56
|
+
heartRateBpm: numOr(raw.heartRate, 0),
|
|
57
|
+
inclinePercent: numOr(raw.incline, 0),
|
|
58
|
+
totalSteps: numOr(raw.totalSteps, 0),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function toSprint8(raw) {
|
|
62
|
+
const scoresByKey = raw.sprintScores;
|
|
63
|
+
if (!scoresByKey || typeof scoresByKey !== "object")
|
|
64
|
+
return null;
|
|
65
|
+
const scores = [];
|
|
66
|
+
for (let i = 1; i <= 8; i += 1) {
|
|
67
|
+
const v = num(scoresByKey[String(i)]);
|
|
68
|
+
if (v === null)
|
|
69
|
+
return null; // partial score sets are not a Sprint 8 result
|
|
70
|
+
scores.push(v);
|
|
71
|
+
}
|
|
72
|
+
const summed = scores.reduce((a, b) => a + b, 0);
|
|
73
|
+
return {
|
|
74
|
+
scores,
|
|
75
|
+
sweatScore: num(raw.totalSweatScore) ?? summed,
|
|
76
|
+
programLevel: num(raw.sprint8ProgramLevel) ?? num(raw.programLevel),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Normalize one raw record. Accepts either the API or the localStorage shape. */
|
|
80
|
+
export function toWorkout(input) {
|
|
81
|
+
const raw = camelizeWorkout(input);
|
|
82
|
+
// Two identifiers, and they are not interchangeable: `workoutId` names the ride,
|
|
83
|
+
// `id` is what the site's own /workouts/:id links carry. They coincide only on
|
|
84
|
+
// rides recorded from 13 Aug 2026 onwards. Either one alone still identifies a
|
|
85
|
+
// record, so a record carrying only one of them is read rather than rejected.
|
|
86
|
+
const workoutId = typeof raw.workoutId === "string" ? raw.workoutId : null;
|
|
87
|
+
const recordId = typeof raw.id === "string" ? raw.id : null;
|
|
88
|
+
const id = workoutId ?? recordId;
|
|
89
|
+
if (!id)
|
|
90
|
+
throw new WorkoutParseError("Workout record has no workoutId and no id.");
|
|
91
|
+
const startedAt = new Date(String(raw.workoutTime));
|
|
92
|
+
if (Number.isNaN(startedAt.getTime())) {
|
|
93
|
+
throw new WorkoutParseError(`Workout ${id} has an unreadable workoutTime.`);
|
|
94
|
+
}
|
|
95
|
+
const intervals = Array.isArray(raw.intervals) ? raw.intervals : [];
|
|
96
|
+
const programType = num(raw.programType);
|
|
97
|
+
// Elapsed time accumulates each sample's own duration, so a short final sample
|
|
98
|
+
// (or any irregular one) lands at the right place on the clock.
|
|
99
|
+
let elapsed = 0;
|
|
100
|
+
const samples = intervals.map((interval) => {
|
|
101
|
+
const sample = toSample(interval, elapsed);
|
|
102
|
+
elapsed += numOr(interval.duration, SAMPLE_INTERVAL_SECONDS);
|
|
103
|
+
return sample;
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
id,
|
|
107
|
+
routeId: recordId ?? id,
|
|
108
|
+
startedAt,
|
|
109
|
+
machineType: typeof raw.machineType === "string" ? raw.machineType : "unknown",
|
|
110
|
+
machineId: typeof raw.machineId === "string" ? raw.machineId : null,
|
|
111
|
+
programType,
|
|
112
|
+
mode: programMode(programType),
|
|
113
|
+
sprint8: toSprint8(raw),
|
|
114
|
+
durationSeconds: numOr(raw.duration, 0),
|
|
115
|
+
distanceMeters: numOr(raw.distance, 0),
|
|
116
|
+
calories: num(raw.calories),
|
|
117
|
+
reported: {
|
|
118
|
+
minHeartRateBpm: num(raw.minHeartRate),
|
|
119
|
+
maxHeartRateBpm: num(raw.maxHeartRate),
|
|
120
|
+
averageHeartRateBpm: num(raw.averageHeartRate),
|
|
121
|
+
},
|
|
122
|
+
archived: numOr(raw.archived, 0) !== 0,
|
|
123
|
+
samples,
|
|
124
|
+
// `input`, not `raw`: the camelized copy has already dropped the distinction
|
|
125
|
+
// between the two upstream shapes, and a fixture captured through the export
|
|
126
|
+
// has to come back out in the shape it went in.
|
|
127
|
+
raw: input,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=workout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workout.js","sourceRoot":"","sources":["../../src/parse/workout.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "matrix-workouts-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Parser, API client and export format for Matrix / Johnson Fitness workout records",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Christoph Halang",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"matrix-fitness",
|
|
10
|
+
"johnson-fitness",
|
|
11
|
+
"jfit",
|
|
12
|
+
"workout",
|
|
13
|
+
"cycling",
|
|
14
|
+
"power",
|
|
15
|
+
"cadence",
|
|
16
|
+
"heart-rate",
|
|
17
|
+
"telemetry"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./node": {
|
|
25
|
+
"types": "./dist/node/index.d.ts",
|
|
26
|
+
"import": "./dist/node/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"matrix-workouts-history": "dist/node/bin/history.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"MATRIX_API.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/bestimmaa/matrix-workouts-core.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/bestimmaa/matrix-workouts-core/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/bestimmaa/matrix-workouts-core#readme",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=20"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/node/bin/history.js",
|
|
55
|
+
"prepare": "npm run build",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:watch": "vitest",
|
|
59
|
+
"history": "vite-node src/node/bin/history.ts --",
|
|
60
|
+
"release": "node scripts/release.js"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^22.20.2",
|
|
64
|
+
"typescript": "^5.7.2",
|
|
65
|
+
"vite-node": "^2.1.8",
|
|
66
|
+
"vitest": "^2.1.8"
|
|
67
|
+
}
|
|
68
|
+
}
|