Deep Water Swimming Page is a swim-native personal performance homepage built around a small publish pipeline:
raw swim sessions -> canonical private data -> public generated data -> homepage
Live site: cicistream.github.io/swimming-page
The goal is to make a swimmer's training archive feel deliberate and publishable, instead of looking like a generic fitness export or admin dashboard.
- Vite + React + TypeScript front end
build-datapipeline that normalizes source sessions into private and public artifacts- a publish-friendly homepage with summary metrics, yearly consistency heatmap, stroke mix, and activity archive
- sample config and sample swim sessions so the project runs on first clone
- a design system in DESIGN.md that defines the Deep Pool editorial look
The current app is intentionally narrow:
- one swimmer profile
- one generated public homepage
- sample JSON as the initial input source
- generated data files checked into neither
public/generated/nordata/private/
This is the first working spine, not the final platform. The current repo proves the contract between import, normalization, publish artifacts, and UI.
- Install dependencies.
npm install- Generate the private and public artifacts.
npm run build:data- Start the local app.
npm run dev- Build the production bundle.
npm run buildnpm run build:data: normalize sample/provider input into generated artifactsnpm run probe:fit:swim-fields: scan exported.fitfiles for swimming-related fields such as swolf, stroke, pool length, and lapsnpm run dev: rebuild data first, then start the Vite dev servernpm run build: rebuild data, type-check, and create the production bundlenpm run preview: serve the production build locally
sample-data/ demo input sessions
scripts/build-data.mjs
data/private/ canonical private swim records (generated)
public/generated/ public publish artifacts consumed by the app (generated)
src/ React app shell and Deep Water UI
swim.config.json swimmer profile + display + provider config
DESIGN.md visual system source of truth
scripts/build-data.mjs currently reads:
- provider input selected in
swim.config.json swim.config.json
It then generates:
data/private/canonical-swims.jsonpublic/generated/summary.jsonpublic/generated/activities.jsonpublic/generated/latest.jsonpublic/generated/heatmap.jsonpublic/generated/sync-report.jsonpublic/generated/config.json
The split is deliberate:
data/private/is the fuller canonical layer for future merge/conflict workflowspublic/generated/is the publish-safe layer consumed by the homepage
The first-run configuration lives in swim.config.json.
Current sections:
profile: public-facing swimmer identity and summary copydisplay: privacy and presentation togglesprovider: selected source plus a lightweight capability matrix
Notable display behavior:
startedAtremains a date key in public activity data for stable charting and archive rendering- precise timestamps can still be emitted separately when enabled
- exact location and notes stay out of public artifacts unless explicitly turned on
Each canonical swim record is expected to include:
idsourcesourceActivityIdstartedAttimezonedistanceMetersdurationSecondspacePer100mSecondspoolLengthMeterswhen the provider exposes pool detaillapswhen the provider exposes pool detail
Optional fields currently supported:
strokeswolfcaloriesnoteslocation
Some providers may not expose every swim-specific field. Missing optional fields, and provider-specific gaps such as pool metadata, are preserved as partial-data markers so the UI can expose completeness honestly.
The visual system is intentionally not a generic sports dashboard. The app follows a dark, editorial "Deep Pool" language:
- tonal depth over visible borders
- large typographic anchors over card grids
- restrained aquatic highlights over loud accent colors
- technical swim data presented as calm, premium publishing
See DESIGN.md for the current source of truth.
Implemented now:
- sample JSON import path
- provider adapter scaffold for Huawei Health
- canonical/private to public/publish split
- partial-data labeling
- sync provenance scaffolding
- Deep Water homepage shell
Planned next:
- real Huawei Health auth + sync implementation
- provider selection rubric
- manual override and conflict resolution flow
- stronger onboarding and diagnostics
- richer publish controls
The build pipeline now loads sessions through provider adapters in scripts/providers/.
Available providers:
sample_json: reads the file configured atprovider.sampleJson.pathhuawei_health: reads a raw JSON export configured atprovider.huaweiHealth.rawDataPathand maps it into the canonical swim contractkeep_swim_probe: reads a canonical draft generated from Keep probe responses and is displayed asKeepin the site UI
Huawei Health scaffold notes:
- current mode is a file-based scaffold, not a completed live API sync
- local import helper lives at
npm run sync:huawei - import source path can be passed with
--fromor stored inprovider.huaweiHealth.importPath - expected raw file path defaults to
data/sources/huawei-health/export.json - example import payload lives at
sample-data/huawei-health-export.sample.json - env var names for a future auth flow live under
provider.huaweiHealth.credentialsEnv
Suggested Huawei Health bootstrap:
- Copy
sample-data/huawei-health-export.sample.jsontodata/sources/huawei-health/export.json. - Set
provider.selectedtohuawei_healthinswim.config.json. - Replace the sample payload with your real exported or synced Huawei Health JSON.
- Run
npm run build:data.
Automated local import flow:
- Export your Huawei Health JSON somewhere on your computer.
- Run
npm run sync:huawei -- --from /absolute/path/to/export.json. - The script validates the JSON, copies it into
data/sources/huawei-health/export.json, stores import metadata next to it, and runsbuild:data. - If
provider.selectedis stillsample_json, switch it tohuawei_healthbefore the next build.
There is also a narrow validation script for checking whether Keep's private API exposes swimming records for your account.
Run it with:
npm run probe:keep:swimming -- --phone 13800000000 --password 'your-password'Or probe a batch of likely swimming type names:
npm run probe:keep:swimming -- --phone 13800000000 --password 'your-password' --default-typesTo also brute-force several likely detail endpoints derived from the returned traininglogs schema:
npm run probe:keep:swimming -- --phone 13800000000 --password 'your-password' --default-types --detail-candidatesWhat it does:
- logs into Keep using your provided credentials
- requests one or more candidate activity types such as
swimming,swim,poolSwimming, andindoorSwimming - saves the raw list response under
data/sources/keep-probe/ - if a first record exists, requests its detail payload and saves that too
- saves an aggregate summary file so we can compare candidate types in one place
This is only for feasibility checking. It does not change your website data or import anything into the swim provider pipeline.
If the probe confirms that Keep exposes swim list data, you can draft canonical sessions from the saved probe files:
npm run map:keep:swim-probeThat script reads the latest probe summary, picks the best matching swim sport type, and writes a canonical draft JSON to data/sources/keep-probe/canonical-swims.draft.json.
To run the site directly from that draft, set provider.selected to keep_swim_probe. The checked-in default config still points at sample_json so a fresh clone can build without private probe artifacts.
To refresh the Keep-based swim data in one command:
KEEP_PHONE=13800000000 KEEP_PASSWORD='your-password' npm run sync:keepThat runs the full local chain:
- probe Keep swim list data
- map probe output into canonical draft swims
- rebuild
public/generated/*
Keep sync metadata is persisted under data/sources/keep-probe/sync-meta.json, so the site can show the real last successful sync time, the most recent failed attempt, and whether the current archive has gone stale.
The deployed GitHub Pages site cannot safely hold GitHub credentials, so online sync uses a small relay service.
This repo includes a Cloudflare Worker template at infra/sync-trigger-worker.js.
Wrangler deployment config lives in wrangler.toml.
Current deployed URLs:
- site: https://cicistream.github.io/swimming-page/
- sync trigger: https://swimming-page-sync-trigger.cicistream-c21.workers.dev
Worker env vars:
ALLOWED_ORIGIN: your deployed site origin, for examplehttps://cicistream.github.ioGITHUB_OWNER: GitHub owner or org, for examplecicistreamGITHUB_REPO: repo name, for exampleswimming-pageGITHUB_TOKEN: GitHub token with permission to dispatch repository events
Front-end env var:
VITE_SYNC_TRIGGER_URL: the deployed Worker URL
The relay validates the browser origin, then sends a repository_dispatch event with type sync_keep, which is handled by .github/workflows/keep-sync-pages.yml.
Quick deploy:
npx wrangler login
npx wrangler secret put GITHUB_TOKEN
npm run worker:deployThen set VITE_SYNC_TRIGGER_URL to the deployed Worker URL before building and deploying the site.
The local import modal in development currently supports:
.json: imports directly into the matching JSON-backed provider and rebuilds the page.csv: maps common swim export columns into canonical swim sessions and rebuilds the page.tcx,.xml: parses swim TCX-style activity data into canonical swim sessions and rebuilds the page.gpx: parses swim tracks into approximate canonical swim sessions and rebuilds the page.fit: stores the file underdata/sources/fit-probe/imports/and refreshes the FIT swim field probe report.zip: stores the file underdata/sources/manual-imports/for future mapping work
CSV imports currently recognize common columns such as:
- time:
date,startedAt,startTime,datetime - distance:
distance,distanceMeters,distanceKm - duration:
duration,durationSeconds,durationMinutes,time - optional metadata:
poolLength,laps,stroke,swolf,calories,notes,location
There is a starter template at sample-data/swims.import-template.csv.
There is a scheduled GitHub Actions workflow for Keep-based refresh and GitHub Pages deploy in .github/workflows/keep-sync-pages.yml.
Repository secrets required:
KEEP_PHONEKEEP_PASSWORD
The workflow:
- runs
npm run sync:keepwithSWIM_PROVIDER_OVERRIDE=keep_swim_probe - builds the site with a GitHub Pages base path
- deploys the updated static site to GitHub Pages
Before relying on it, enable GitHub Pages for the repository and allow GitHub Actions to deploy Pages artifacts.
What is confirmed so far:
- Keep swim list probing works for candidate types such as
swim,poolSwimming, andindoorSwimming - the current local site integration uses that list-level data successfully
- the saved Keep list payload includes useful summary fields such as start time, duration, calories, average/max heart rate, vendor info, and a text title that usually contains the distance
Current limits:
- the list payload does not currently expose
poolLengthMeters,laps,swolf, orstroke - the guessed Keep detail endpoints we tried, including
/{sportType}log/{id}and severaltraininglogsvariants, did not return a usable swim detail payload - Keep's swim
distancefield can be0, so the current mapper falls back to parsing distance from titles like游泳池游泳 1025 米
Network debugging notes:
- Charles with plain phone proxy works, so proxying itself is not the problem
- Charles SSL Proxying causes Keep to fail while normal browser HTTPS still works
- in practice, that means ordinary Charles interception is not a reliable path to the Keep swim detail JSON for this app
Recommendation:
- use
npm run sync:keepfor the current stable, list-based swim sync - if you want
poolLengthMeters,laps, orswolf, plan for a stronger reverse-engineering path such as emulator + system certs, rooted device, or Frida, instead of repeating ordinary Charles attempts
This repo is still at the first public spine stage, so the most valuable contributions are:
- provider adapters
- data contract hardening
- visual polish that preserves the Deep Water design language
- publishing and privacy controls
- testing around normalization and generated artifact integrity
If you are changing the UI, keep DESIGN.md in sync with the implementation direction.
