matrix-workouts-core
v0.8.0
Published
Parser, API client and export format for Matrix / Johnson Fitness workout records
Maintainers
Readme
matrix-workouts-core
Read what a Matrix / Johnson Fitness exercise bike actually recorded.
The dashboard at matrixworkouts.jfit.co shows six tiles per ride: distance, average
incline, average heart rate, calories, duration, average speed. The record behind that
page also carries a sample every 10 seconds with power (watts), console resistance
level and cadence (rpm) — none of which appears anywhere in the site's UI. This
package parses those records, talks to the API that holds the full history, and
writes a lossless export document. Those three fields are why it exists.
It has no runtime dependencies and touches no platform: no DOM, no chrome.*, no
node:, no fetch. A test asserts that on every run. Node-only helpers live behind a
separate entry point, so a browser bundle cannot pull them in by accident.
Install
npm install matrix-workouts-coreNode 20 or newer. ESM only.
Quickstart
import { toWorkout, heartRateStats, fetchWorkoutHistory } from "matrix-workouts-core";
const workout = toWorkout(record); // one raw record -> normalized model
workout.samples[0]?.powerWatts; // the thing the site never shows
heartRateStats(workout.samples); // dropout-filtered, with the count
// The API needs a fetch; it never calls one itself.
const { workouts, truncated, skipped } = await fetchWorkoutHistory(credentials, myFetch);Under Node, the fetch adapter is already written:
import { httpFetch, downloadHistory } from "matrix-workouts-core/node";
import { loginWithXid } from "matrix-workouts-core";
const credentials = await loginWithXid({ xid, pin }, httpFetch);
const result = await downloadHistory(credentials, { outDir: "history", split: true });Download a whole history
The site's own list page holds roughly the current week and errors on anything older. The API has everything, so the package ships a CLI that signs in and takes the lot:
cp .env.example .env # your xid and passcode
npx matrix-workouts-history # -> history/raw-history-<timestamp>.json
npx matrix-workouts-history --split # plus one export document per ride
npx matrix-workouts-history --out data # somewhere other than history/| Variable | Required | Meaning |
|---|---|---|
| MATRIX_XID | yes | the member number printed on the gym tag |
| MATRIX_PIN | yes | the numeric passcode |
Read from .env in the working directory or from the environment; the environment
wins. Used for one sign-in request, never written to the output and never printed.
What it exports
| Entry point | Holds |
|---|---|
| matrix-workouts-core | toWorkout, loadCachedWorkouts, findWorkout, heartRateStats, flagHeartRateDropouts, controlSignature, programMode, targetWattBlocks, sprint8Segments, shareCard, renderShareCardSvg, fetchWorkoutHistory, loginWithXid, readCredentials, workoutExport, workoutTcx, workoutFit, workoutTitle, stravaUpload, bestEffort, bestAmong, beats, shownWatts, powerCurve, personalRecords, rideRecords, and the Workout / Sample types |
| matrix-workouts-core/node | httpFetch, downloadHistory, readRawHistoryFile, loadEnvFile — real fetch, real files |
Units are in the names (powerWatts, cadenceRpm, distanceMeters), because the
upstream names are ambiguous and one of them — averageDistance, which is cumulative
— is actively misleading. Every record keeps its untouched original under raw, so an
export is lossless even where this model has no name for a field.
shareCard(workout) decides what a shareable workout image says — one line and one
big number per console program — and renderShareCardSvg(card, { format: "story" })
lays it out as an SVG string, 1080 × 1920 or 1080 × 1350 ("feed"). Drawing it is
yours: it names Doto and Instrument Sans and does not carry them.
powerCurve(workouts) gives the best mean power across a history for each
duration, from 10 s to an hour, and names the ride each one came from.
personalRecords(workouts, { recentSince }) gives each duration's all-time best
next to its recent best. rideRecords(workout, history) says, for each duration,
whether one ride set a first effort, a new record, or neither against the rides
before it. Windows are whole 10 s samples and never include the console's partial
final sample. Experimental: this API may change before 1.0.
MATRIX_API.md documents the upstream API: endpoints, wire shapes, units, and which parts are verified rather than read out of the site's bundle.
Privacy and scope
Scope: indoor bikes — upright and recumbent. Treadmill and rower records parse, but nothing here is tuned for them.
This library reads one account's own data and sends it nowhere except the API it came
from. The CLI handles a passcode for exactly one sign-in request and never stores,
logs or prints it; downloads are written 0600 because they are the rider's heart
rate. Where you point --out is where that data lives — choose accordingly.
Development
npm test # vitest
npm run typecheck
npm run build # -> dist/All three must pass before committing. See AGENTS.md for the decisions and rules behind the code, and CHANGELOG.md for the release history.
Cutting a release:
npm run release -- patch # bumps, tags, packs; refuses without a CHANGELOG entry
git push origin main:main --follow-tags
npm publish --access publicPart of matrix-workouts
| | | |---|---| | matrix-workouts-core | this package: parser, API client, export format | | matrix-workouts-chrome | the Chrome extension that puts it on screen | | matrix-workouts-mcp | an MCP server, so an AI agent can ask about your rides |
MIT licensed. Unaffiliated with Matrix Fitness or Johnson Health Tech.
