Skip to content

feat(program): approve a proposed program in the app - #95

Merged
mohammadp1001 merged 1 commit into
mainfrom
feat/81-approve-program
Aug 23, 2026
Merged

feat(program): approve a proposed program in the app#95
mohammadp1001 merged 1 commit into
mainfrom
feat/81-approve-program

Conversation

@mohammadp1001

Copy link
Copy Markdown
Owner

Closes #81the last of the 13.

Stacked on #94. Retarget as the chain merges.

Why

A draft the user never sees is a feature that does nothing. This is where the
proposal from #80 becomes a decision.

It shows what would change, not the YAML

The user is being asked to replace what they are following. The only question
they actually have is which exercises are new, which are gone, and which
moved
. A raw file dump does not answer that.

The diff rules (the actual design problem)

lib/program-diff.ts — pure and unit-tested, because this is the fiddly part:

  • Days pair by name before position. Resequencing a split must not read as
    removing every day and adding it back.
  • Unmatched days then pair by position, so a rename reads as a rename rather
    than a simultaneous add and remove.
  • Exercises compare by exerciseId, never by display name. The lift is the
    durable thing, not the label it happens to carry — renaming an exercise must
    not look like swapping it out.
  • Dropped exercises are listed last within a day: "what am I losing?" only
    makes sense once you can see what you are getting.
  • With no active program, everything is new — the honest reading for a user
    who has none.

The rationale comes before the diff

A list of changes with no reason behind it is not something anyone can judge.

Interaction

  • The banner sits above the program and is server-rendered, so it is
    there on first paint rather than popping in. It is the one thing on that
    screen that needs an answer.
  • Both actions take two taps. Neither replacing a training block nor
    throwing away a proposal should happen on a stray thumb.
  • Approve and discard re-read the pending draft rather than trusting the id
    from the screen — the coach may have replaced it, or the user may have
    answered it in another tab. A stale id gets a 409 saying exactly that.

lib/db/program-proposal.ts assembles the proposal and its diff once, shared by
the page and the API route. Two implementations of a comparison this subtle
would drift.

Verified against the running app

  • The proposal renders with its counts (1 new · 2 dropped · 1 changed) in
    both Persian and English, dir correct, no undefined/null in markup.
  • Approving through the real route makes it active, clears isDraft, and
    retires the previous program rather than deleting it.
  • Replaying the same approval returns 409.

Tests

lib/program-diff.test.ts (11): identical programs, added/removed/changed
exercises, comparison by id rather than name, reordered days not reported as
churn, a rename read as a rename, whole days added and dropped, a renamed
program, and the no-previous-program case.

app/api/programs/draft/route.test.ts (7): null when nothing waits, the
proposal returned with reasoning and diff, names resolved rather than null,
approve activating and clearing, discard leaving the active program alone, a
stale id refused with 409, and a non-draft program refused.

Full suite: 443 passed, 0 failed. tsc --noEmit and eslint clean.

@mohammadp1001
mohammadp1001 force-pushed the feat/80-save-program-draft branch from cd037de to 8df9ee5 Compare August 23, 2026 06:17
@mohammadp1001
mohammadp1001 force-pushed the feat/81-approve-program branch from 0b75a9f to c0e664e Compare August 23, 2026 06:17
@mohammadp1001
mohammadp1001 force-pushed the feat/80-save-program-draft branch from 8df9ee5 to c344526 Compare August 23, 2026 06:22
@mohammadp1001
mohammadp1001 force-pushed the feat/81-approve-program branch from c0e664e to 160a74f Compare August 23, 2026 06:22
@mohammadp1001
mohammadp1001 changed the base branch from feat/80-save-program-draft to main August 23, 2026 06:30
A draft the user never sees is a feature that does nothing. This is the screen
where the proposal from #80 becomes a decision.

It shows what would change, not the YAML. The user is being asked to replace
what they are following, and the only question they actually have is which
exercises are new, which are gone, and which moved. A raw file dump does not
answer that.

`lib/program-diff.ts` holds the comparison, pure and unit-tested, because the
rules are the fiddly part. Days pair by name before position, so resequencing a
split does not read as removing every day and adding it back, and an unmatched
day pairs by position so a rename reads as a rename rather than a simultaneous
add and remove. Exercises compare by `exerciseId`, not by display name - the
lift is the durable thing, not the label it happens to carry. Dropped exercises
are listed after a day's contents, because "what am I losing?" only makes sense
once you can see what you are getting. With no active program every exercise is
reported as new, which is the honest reading for a user who has none.

The rationale is shown above the diff. A list of changes with no reason behind
it is not something anyone can judge.

The banner sits above the program itself and is rendered on the server, so it is
there on first paint rather than popping in - it is the one thing on that screen
that needs an answer. Both actions take two taps: neither replacing a training
block nor throwing away a proposal should happen on a stray thumb.

Approving and discarding both re-read the pending draft rather than trusting the
id from the screen, because the coach may have replaced it or the user may have
answered it in another tab since it loaded. A stale id gets a 409 saying so.

`lib/db/program-proposal.ts` assembles the proposal and its diff once, shared by
the page and the API route - two implementations of a comparison this subtle
would drift.

Verified against the running app: a proposal renders with its counts in both
Persian and English with RTL intact and no nulls in the markup, approving it
through the real route makes it active and retires the previous program, and
replaying the same approval returns 409.
@mohammadp1001
mohammadp1001 force-pushed the feat/81-approve-program branch from 160a74f to 1554507 Compare August 23, 2026 06:30
@mohammadp1001
mohammadp1001 merged commit 46e2def into main Aug 23, 2026
2 checks passed
@mohammadp1001
mohammadp1001 deleted the feat/81-approve-program branch August 23, 2026 06:37
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.

Approve a proposed program in the app

1 participant