Skip to content

feat: shared plan registry — publish a week, browse and import others' - #21

Open
bwndapp wants to merge 1 commit into
arvids-unavailable:mainfrom
bwndapp:feat/shared-plan-registry
Open

feat: shared plan registry — publish a week, browse and import others'#21
bwndapp wants to merge 1 commit into
arvids-unavailable:mainfrom
bwndapp:feat/shared-plan-registry

Conversation

@bwndapp

@bwndapp bwndapp commented Aug 24, 2026

Copy link
Copy Markdown

What

Publish your weekly plan to the openGym instance you already run, browse what others published, and import one in a tap.

openGym already has the hard part. plan-share.js defines a portable bundle — routines, the week schedule and any custom exercises they reference, deliberately carrying no workouts, weigh-ins or settings — and mergePlan() adds it non-destructively. Sharing it still means handing someone a file. This adds storage and discovery around that same format, so it's not a second import path.

Backend — api/server.js, no new dependencies

GET  /api/plans          browse, ?q= &sort=popular|new &mine=true
GET  /api/plan?slug=     one plan, with its bundle
POST /api/plan/publish   author taken from the session, never the body
POST /api/plan/imported  popularity counter, best-effort
POST /api/plan/delete    author or admin

One JSON file per plan under DATA/plans, written through the existing atomicWrite. At this scale a directory beats a table: a plan is readable without a query and a bad one is removable with rm. Route keys and query strings follow the existing table's style.

Some things I was deliberate about:

  • Publishing is validated with the same rules parsePlan applies client-side, so a plan the app would refuse to import can't land on disk.
  • Slugs are constrained to [a-z0-9-] before they ever reach the filesystem — a slug is a filename, so it isn't trusted just because we generated the ones already there.
  • Rate limited per author (10/hour, 50 total) so one account can't fill the directory.
  • Browse metadata is derived from the bundle, never from what the client claims.

A note on the exercise catalogue

The natural way to label a plan is by equipment and body part, which needs exercises-data.js. The api image copies server.js alone, and pushing 800 kB of frontend dataset into the backend to render a few chips seemed like a bad trade against "keep it dependency-light".

So the server derives only what the bundle itself knows (routines, days, exercise count, customs), and PlanDetail derives the equipment chips client-side, where the catalogue already lives. The consequence is that server-side search matches name, description and author, not equipment — the search placeholder says so. Happy to move it either way if you'd rather.

Frontend

Plans.jsx (browse, search, sort) and PlanDetail.jsx (full week, muscle map, import) as new views, a thin plans-api.js client, and entry points from the Plan tab, Settings and the existing share sheet. Everything is gated behind registryOn(), which is false in the mobile and demo builds since they have no backend to talk to.

Testing

  • npm test198 passing, including 6 new cases over the registry client (query building, slug encoding, error surfacing, and that the popularity counter can never fail an import that already happened).
  • Exercised every endpoint against a running server: empty list, missing plan 404, unauthenticated publish 401, invalid bundle 400, publish, list, search hit and miss, mine while signed out, the counter on a real and a missing slug, path traversal via ?slug=../../db (404, not a file read), delete by a non-owner (403) and by the owner (200).
  • Built the frontend and clicked through browse → detail → import and the publish sheet.

Open questions

Happy to adjust any of this — in particular whether the registry should sit behind an env flag (like INVITE_ONLY) for instances whose operator doesn't want user-published content, and whether POST /api/plan/delete should be DELETE instead. I matched the existing table, which uses POST for mutations like /api/admin/user/disable.

openGym already has the hard part: plan-share.js defines a portable bundle (routines,
week schedule, referenced customs — never workouts, weigh-ins or settings) and
mergePlan() adds it non-destructively. Sharing it still means handing someone a file.

This adds storage and discovery around that same format, so a plan can be published to
the instance you already run and imported in a tap.

Backend (api/server.js, no new dependencies):
  GET  /api/plans          browse, ?q= &sort=popular|new &mine=true
  GET  /api/plan?slug=     one plan, with its bundle
  POST /api/plan/publish   author comes from the session, never the body
  POST /api/plan/imported  popularity counter, best-effort
  POST /api/plan/delete    author or admin

One JSON file per plan under DATA/plans, written atomically via the existing helper.
Publishing is validated with the same rules parsePlan applies client-side, so a plan the
app would refuse to import can't land on disk. Slugs are constrained to [a-z0-9-] before
they ever reach the filesystem. Rate limited per author (10/hour, 50 total).

Browse metadata is derived from the bundle, never from what the client claims. It
deliberately excludes anything needing the exercise catalogue — the api image ships
server.js alone, and PlanDetail derives the equipment chips client-side where the
catalogue already lives.

The UI hides itself in the mobile and demo builds, which have no backend to talk to.

Tests: 6 cases over the registry client, alongside the existing suite (198 passing).
@mangeshraut712

Copy link
Copy Markdown

Not landing in #28. A shared plan registry is a new public write surface on the API (publish/browse/import counters) and needs a dedicated security review (authz on delete, slug uniqueness, size limits, invite-only instances). Please keep this PR open for that review rather than bundling it with the secret/media hygiene fix.

@SunriseDuarte

Copy link
Copy Markdown

Heads up: openGym has moved to GitLab.
-> GitLab

The GitHub repo is no longer maintained (the account was suspended, so it's out of my hands). Please re-open this issue/PR on GitLab so it doesn't get lost, I'll pick it up there. Questions, help and roadmap discussion now happen in Discord:
-> Discord

Sorry for the noise, and thanks for sticking around.
more infos here: openGym

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants