Repository navigation
Conversation
Optional follow-up from the audit (not a full schedule editor). The ramp
schedule textarea is taller (8 rows, 10rem) so a two-step schedule fits
without scrolling, and spellcheck is off for the JSON. Invalid JSON now
also leaves a message under the field ("Not valid JSON: <parser
message>", aria-invalid + aria-describedby via showFieldError) and moves
focus there; the existing toast stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ladder config modal's ramp schedule was a raw JSON textarea. It is now
a compact step editor: one row per step with "After (days)" and
"Share (%)" inputs and an icon Remove button, an Add step button, quick
presets, a proportional bar and a live summary ("Total 100% across 3
steps · last purchase on day 60"). "Edit as JSON" reveals the existing
textarea (same id, hint and placeholder); the two stay in sync, and
invalid JSON keeps the rows and shows the parser error under the field.
The textarea is still what the save reads, so the request body is built
exactly as before, and a stored schedule saved untouched is sent back
unchanged. Shares are stored as fractions; a row's exact fraction is kept
until the user edits that share, and typed percents convert by decimal
shift (33.3% -> 0.333, not 33.3 / 100).
Validation mirrors the backend exactly (internal/config/validation.go
validateLadderRampSchedule -> cloud-commitments-go pkg/ladder
RampSchedule.Validate and Go's json decoding): at least one step,
after_days a whole number, 0 or more and strictly increasing, each
fraction in (0, 1], fractions summing to 1 within 1e-9 in float64 order.
Problems show beside the row (aria-invalid + aria-describedby) and in
the summary, and Save refuses them like invalid JSON today: toast, inline
message, focus on the field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 46 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Oct 29, 2026
Collaborator
Author
|
Replaced by #1624: the same branch and commits, reopened from LeanerCloud/cloud-commitments-platform so it can be part of a native GitHub stack (cross-fork stacks aren't supported). Please review there. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1603: merge that first; until then this diff includes it. This PR's own commits: bdf8f04.
In the Commitment Laddering per-account config modal, the ramp schedule was a raw JSON textarea: to change it you edited
{"steps":[{"after_days":0,"fraction":0.5}, ...]}by hand, and a schedule the backend refuses (shares not adding to 1, days out of order) only showed up as a failed save. This PR adds a step editor: one row per step with "After (days)" and "Share (%)", Add/Remove, quick presets, a proportional bar and a live summary ("Total 100% across 3 steps · last purchase on day 60"). Problems show beside the step as you type. "Edit as JSON" still opens the textarea, and the two stay in sync.Part of the UI modernization sweep.
What changed
frontend/src/ladder-ramp-schedule.ts(new): the model. It decodes the JSON the way the backend's Go decoder does, validates with the backend's rules (see Notes for review), converts shares between percent and fraction, and writes the JSON back in the format the modal has always used (JSON.stringify(schedule, null, 2)). Typed percents convert by moving the decimal point, so 33.3% is stored as0.333, not33.3 / 100(0.33299999999999996). Also has the three presets.frontend/src/ladder-ramp-editor.ts(new): the editor's DOM. The rows are a view over the textarea:"after_days": "7"), leaves the rows alone and shows the error under the textarea.stepsare kept when the rows are edited.aria-label. It is disabled while only one step is left.aria-invalidand are linked witharia-describedby. The summary is a polite live region, and the bar isaria-hidden.style="", because the CSP has no'unsafe-inline'for styles.frontend/src/ladder-config-modal.ts: the ramp field is the editor markup. The textarea keepsid="ladder-cfg-ramp-schedule", its label, hint and placeholder inside the "Edit as JSON" panel, and has a static error line,#ladder-cfg-ramp-schedule-error.frontend/src/ladder.ts:{"steps":[{"after_days":0,"fraction":1}]}).JSON.parses the textarea, so the request body is built exactly as before.frontend/src/styles/ladder-form.css: editor styles using--cudly-*tokens, with standard 2.25rem inputs and the 2.25rem icon button on the same row, a gap above Add step, the error colour on invalid inputs, and a red outline on the bar when the total is off. The ui(ladder): taller ramp-schedule field with an inline JSON error #1603 textarea sizing now targets the textarea in its new place.src/__tests__/ladder-ramp-schedule.test.ts(model, one case per backend rule) andsrc/__tests__/ladder-ramp-editor.test.ts(rows and JSON in sync both ways, live errors and their ARIA wiring, add/remove focus, presets, save payload and refusals). The new Playwright spectests-e2e/ladder-ramp-editor.spec.tsedits steps in the real modal and asserts the PUT body.tests-e2e/ladder-account-selector.spec.ts's layout test now checks that the editor spans both columns with compact inputs, then opens "Edit as JSON" before checking the textarea and hint as before.Why
Hand-editing JSON fractions was the only way to set up a ramp, and mistakes came back as a server error after Save. Keeping the textarea as the value the save reads means the editor cannot change what is sent, only how it is entered. The JSON view stays for anyone who prefers it or wants to paste a schedule.
Screenshots
Before = the base branch (#1603, main at 43dd618 plus its commit); after = this branch. Headless Chromium at 1440x900 (mobile: 390x844) against a mocked API. The edited config has steps of 50% on day 0, 30% on day 30 and 20% on day 60.
Verification
Run on this branch's head
bdf8f043(own commitbdf8f043on top of #1603 atcb79608d), fromfrontend/:npm run typecheck: passes.npm run lint: 0 errors (141 warnings, all pre-existing; none in the touched files).npm run build: passes.npx jest --silent --maxWorkers=3): 177 suites, 4650 passed, 1 skipped (4651). New:ladder-ramp-schedule.test.ts(30 tests) andladder-ramp-editor.test.ts(23 tests).e2e-local.sh, whole suite): 278 passed, including the newladder-ramp-editor.spec.ts(2 tests), the updated layout test inladder-account-selector.spec.ts, andladder-config.spec.ts, which checks that an untouched schedule round-trips into the PUT body.git merge-tree) with ui(dialogs): one dialog chrome with a header close button #1598 and ui(settings): laddering and planned purchases section spacing #1607.Notes for review
The editor checks exactly what the backend checks, so it never blocks a schedule the server would accept. Backend references are to this repo at the base commit and to
cloud-commitments-go/pkgat the pinnedv0.0.0-20261026123645-88b95b64b043:internal/config/validation.go:911-922validateLadderRampSchedule: empty input is refused, thenjson.Unmarshalintopkg/ladder.RampSchedule(a decode failure is "ramp_schedule is not valid JSON"), thenrs.Validate().internal/config/store_postgres_ladder.go:514-523marshalRampScheduleonly adds ajson.Validcheck on write.pkg/ladder/types.go:291-310, Goencoding/json): keys match case-insensitively, with the last matching key winning. Missing ornullvalues become zero values (nullor nostepsis an empty list, anullstep is{0, 0}, a missingafter_daysis 0).after_daysmust decode into anintandfractionmust be a number. Unknown keys are ignored.ladder-ramp-schedule.tsdecodeRampScheduledoes the same.RampSchedule.Validate(pkg/ladder/types.go:317-347)::318);after_daysstrictly greater than the previous step's, starting fromprev := -1(:322-324). So the first step can be any day from 0 up, and duplicates are refused;:333);rampSumEpsilon = 1e-9of 1.0 (:280,:344). JavaScript numbers are float64 and the editor sums in the same order, so both sides reach the same verdict on borderline sums.after_daysabove 2^53 is refused here (Go's int is 64-bit), so that JavaScript never rounds it silently.7.0or7e0forafter_dayspasses here, becauseJSON.parsecannot tell it from7, but Go refuses to decode it into an int. The row editor never writes those forms; only hand-typed JSON could reach the server's error.Overlaps
ladder-form.cssinto the shared dialog styles. This PR's CSS sits in a different part of the file and merges cleanly with that branch (checked withgit merge-tree).frontend/src/ladder.ts(the "Per-Account Configurations" heading) and appends toladder-form.css. It merges cleanly with this branch.Note for the UI owner
This changes UI code that @cristim owns. IAmJSD asked for this PR; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code