Skip to content

Tracknotes: a Resupply screen of its own - #238

Merged
eamon-b merged 6 commits into
mainfrom
feat/resupply-planner-mobile
Oct 25, 2026
Merged

eamon-b merged 6 commits into
mainfrom
feat/resupply-planner-mobile

Conversation

@eamon-b

@eamon-b eamon-b commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Stacked on #236 (shared lib). Merge #236 first; GitHub will then retarget this to main.

Tracknotes already had resupply planning, but it was hard to find. It sat at the bottom of the Plan screen, below Stops, and you reached its checkbox modal from there. This PR gives resupply planning its own screen.

Changes

  • app/guide/[trailId]/resupply.tsx is now a regular stack screen titled "Resupply", not a modal. The deep link still works.
  • Entry points:
    • A labelled Resupply row near the top of Plan (ResupplyEntryRow), reading e.g. "5 stops · longest 7 days · 2 to decide".
    • An "Open the resupply planner ›" link on resupply waypoints.
  • Screen layout:
    • It reads top to bottom: start → leg → stop → leg → … → end.
    • The summary and an editable inputs line sit on top. Tapping the inputs opens PlanSettingsSheet with hours/pace and food/long-carry; the values stay in plan-inputs-store.
    • A turn-off with several places is a radio list plus "None – carry through". A single place is a Yes/No switch. A turn-off still to choose shows "Not decided yet".
  • What each row shows:
    • Each option: store type, off-trail access or "On trail", whether it accepts boxes, and its description (expandable).
    • Each leg: km with the side-trip and ride lines, ascent and descent, days, food, and a long-carry badge.
    • There are no arrival dates.
  • Zero days: a stepper on each chosen stop, written to setResupplyZeroDays through plans-store.apply.
  • "Plan resupply here" on a waypoint now picks that place at its turn-off with chooseResupplyOption, replacing any other town chosen there. Removing the chosen place makes the turn-off "carry through".
  • Removed: the Plan screen's Resupply section, ResupplyCard, ResupplySelectList, resupply-toggle.ts and the unused toggleResupplyStop / setResupplyStopSelected.

Verified

  • npm run check:mobile: 171 suites and 2156 tests pass; typecheck clean; lint shows 0 errors and the 3 existing warnings.

Not verified on a device

Nobody has run it on a device yet: no emulator or Metro was attached.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
trail-maps Ready Ready Preview Oct 25, 2026 8:46am UTC

@eamon-b eamon-b left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of #238 at f659003 (diff against feat/resupply-planner)

Summary

Resupply planning moves from a section at the foot of Plan, with its checkbox modal, to a stack screen of its own. Each turn-off is now one decision (a radio list, or a switch for a single place), with legs in between, a zero-days stepper, and a Resupply entry row near the top of Plan. "Plan resupply here" on a waypoint is now the same group decision.

The core claims hold up:

  • Edits are built at apply time. chooseResupplyEditor, zeroDaysStepEditor and waypointResupplyEditor all compute from the document apply hands them. apply is serialised per trail (writeChains), so two quick taps build on each other. The legacy fallback is read inside the editor too, so a tap made just after "Clear choices" does not bring the old selection back.
  • Every write goes through plans-store.apply, so it is synced through the outbox. Hiker inputs stay in plan-inputs-store.
  • No calculator is forked. All figures come from @lib/resupply-plan and all wording from @lib/resupply-display.
  • The removed modules have no references left (ResupplyCard, ResupplySelectList, resupply-toggle, toggleResupplyStop, setResupplyStopSelected). The use-planned-resupply read hooks behind the map rings, elevation marks, the List "Planned" chip and the DistanceStrip "Next resupply" are unchanged.
  • The resupply route is still registered as a plain Stack.Screen. The deep link resolves to the same file.
  • SOBO works. The screen and Plan both use usePlanRoute(plan).trail (alternates spliced in, direction applied). The section params are route km on that same trail, and the groups are sorted in walking order.

Checks:

  • npm run check:mobile passed locally: 171 suites and 2156 tests; typecheck clean; lint 0 errors and the 3 existing warnings.
  • Root npx tsc --noEmit is clean.
  • CI is green on f659003, and the PR is MERGEABLE/CLEAN.
  • Overlap with other open PRs: #236 (this PR's base) also edits mobile/src/features/plan/__tests__/ResupplyCard.test.tsx, which this PR deletes. That is expected with a stacked PR, but if #236 changes that file again, the rebase will hit a modify/delete conflict. #237 touches no mobile files.
  • No emulator was attached, so nothing was checked on a device.

Findings (most serious first)

1. "Clear choices" leaves zero days behind, and they are still counted but can no longer be seen or changed.
mobile/src/features/plan/use-planned-resupply.ts:108-111 (resetResupplyStops) clears resupplyStops, but resupplyZeroDays is left alone. After the reset, every multi-place group is undecided. For an undecided stop, computeResupplyLegs counts the largest zero-days value among its ticked options (stopZeroDays; src/lib/resupply-plan.ts test at line ~1099 shows {b1: 0.5, b2: 2} giving 2). plannerRows (resupply-planner.ts:146) shows 0 for an undecided group, and ResupplyPlanner.tsx:347 shows the stepper only when one place is chosen.

Failure scenario:

  1. At Monarch Pass, choose Salida and add 2 zero days.
  2. Tap Clear choices.
  3. The header still reads "Days (incl. 2 days off)" and the leg into Monarch Pass still carries the 2 days. No row shows them, and no stepper can remove them.

The same happens when choosing Salida with zero days, then switching to Poncha Springs, then clearing: the hidden Salida days come back into the totals. They also sync to the web.

Fix options:

  • clear resupplyZeroDays in the reset;
  • or prune the entry for any option that is no longer the chosen one when chooseResupplyEditor runs;
  • or at least show the inherited figure on undecided groups.

2. In multi-place groups, the option details and the "More" button can't be reached with a screen reader.
At ResupplyPlanner.tsx:389-403, Radio is a Pressable (accessible by default) with accessibilityLabel={spokenLabel}, and it wraps OptionFacts, which holds its own "More"/"Less" Pressable (lines 423-433). An accessible parent becomes a single focus element: its label replaces the children's text, and the nested button is not separately focusable (always on iOS; on Android TalkBack merges it too).

Failure scenario: a VoiceOver user at Monarch Pass hears "Salida, radio button" and nothing else. They miss "Full resupply · 22.5 km hitch · Accepts boxes", the description, and the expand control, which are the facts the decision depends on. Single-place groups don't have this problem, because OptionFacts sits outside the Switch there.

Fix: put the facts in accessibilityHint or build them into the label, and move "More" outside the radio's Pressable.

3. Several controls share the same spoken label on a 70-group screen.

  • ResupplyPlanner.tsx:353/362: "Fewer zero days" / "More zero days" are identical on every chosen stop.
  • :340: "None – carry through" is identical in every group.

Navigating by control, the hiker can't tell which turn-off they are changing. Fix: add the place or turn-off name, for example "More zero days at Salida" and "Carry through Monarch Pass".

4. React.memo on the rows has no effect, so every tap re-renders the whole list.
plannerRows (resupply-planner.ts:96-150) builds new row and names objects on every call, and it is re-run whenever stored, zeroDays or legs change, which is every edit. GroupRow (memoised on row) and LegRow (on leg/names) therefore always re-render. On the CDT (about 70 groups plus about 70 legs, with FlatList's default window covering most of them) one radio tap re-renders every mounted row and recomputes optionFacts for each option. That is probably tolerable on a recent phone, but the file header says rows are memoised when in practice they are not.

Fix: reuse unchanged row objects (key them and compare choice/zeroDays/leg fields), or memoise GroupRow on primitive props.

5. With an invalid section, the Resupply row and the screen behind it disagree.
If the hiker sets the start at or after the end on Plan, the row reads "Choose where to resupply" (resupplyEntryText: hasData is false, plan.tsx:509-513), even when a plan is already made. Tapping it opens the screen without params (validSection is false), which shows the whole trail with real figures. The screen's "Choose a start before the end on the Plan screen." caption (ResupplyPlanner.tsx:133) can never show, because the screen always falls back to the whole trail.

Fix: have the row say the section is invalid in that case.

6. Single-place groups read differently in two places before any plan is made (low).
With no selection stored, a single-place group is chosen on the resupply screen (the switch is on). The waypoint detail, through isSelected/plannedResupplyIds(null), says "Plan resupply here" for the same place. Tapping it makes the default selection explicit; nothing is lost. This is consistent with the shared semantics, but it may confuse someone moving between the two screens.

7. Leftover code (nit).

  • saveResupplyStops (use-planned-resupply.ts:95) no longer has any caller.
  • useStoredResupplyStops still returns stored and its doc comment ("a write has to start from the list the hiker is looking at") now describes the opposite of what the editors do.

Verdict

The structure is sound, and the edit-race and sync claims check out. Fix #1 before merging: it silently inflates the hiker's day totals with zero days they can't see or remove. #2 and #3 are accessibility regressions worth fixing in the same pass. #4–#7 can follow. Nothing has been run on a device yet.

🤖 Generated with Claude Code

@eamon-b
eamon-b force-pushed the feat/resupply-planner-mobile branch from f659003 to 74f132e Compare October 25, 2026 05:24
eamon-b added a commit that referenced this pull request Oct 25, 2026
Review of #236 and #238:

- A section that starts at a town no longer walks out of it again: the
  leg arriving there, in the section before, counted the side trip both
  ways, so two sections meeting at a town added up to more than the whole
  trail (0-50 + 50-100 was 109 km against 106). Only a stop at the trail's
  own start, which no leg arrives at, has its walk out on the leg leaving.
- With a camp plan, a leg's arrival comes from that plan alone: its rest
  nights are already in its dates, and adding the resupply zero days too
  counted the same rest twice. The day number is now the calendar day the
  date falls on (walking days plus earlier rest days), so day and date
  agree. Zero days still count in the summary's total days.
- Only a stop decided on one place takes zero days; an undecided one takes
  none rather than the most any of its places holds.
- Zero days follow the selection: setResupplyStops with an explicit list,
  and so chooseResupplyOption, drops those of options no longer ticked
  (carrying through included); absent keeps them. resetResupplyPlan
  clears the selection and the zero days together.
- chooseResupplyOption writes nothing when the group already reads that
  way, so a lone place chosen under the every-option default keeps the
  default.
- The privacy policy lists the zero days a plan holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eamon-b added a commit that referenced this pull request Oct 25, 2026
- Clear choices resets through resetResupplyPlan, so the zero days go
  with the selection; choosing another place or carrying through drops
  the left-out places' zero days in the shared editor.
- A multi-place group's radio speaks the place's full name with its facts
  as the hint; the facts and More/Less sit beside the radio, each its own
  focusable element, instead of inside one Pressable label.
- Repeated controls name their place or turn-off: "Fewer/More zero days
  at Salida", "None - carry through at Monarch Pass (US 50)".
- Rows are memoised by value (sameGroupRow, sameLegRow), since
  plannerRows rebuilds every row object on each edit; a tap now
  re-renders the changed rows only.
- An invalid Plan section: the Resupply row is worked out for the whole
  trail and says "Whole trail · ...", which is what the screen shows.
- The waypoint detail reads the group's choice with resupplyGroupChoice,
  so a lone place under the every-option default shows as planned with
  Remove, as the planner does; a place in an undecided group picks.
- saveResupplyStops is gone, and useStoredResupplyStops says what it is
  for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eamon-b
eamon-b force-pushed the feat/resupply-planner-mobile branch from 74f132e to 09e8ce1 Compare October 25, 2026 05:47
@eamon-b

eamon-b commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Update on the review (09e8ce1, rebased on #236 a69f949, CI green)

Point by point against the earlier COMMENT review:

  1. Clear choices leaves zero days behind. Fixed in the lib (Resupply planner: store type, decision groups, zero days, summary (shared lib) #236): pruning on deselect or switch, the new resetResupplyPlan, and an undecided stop counts no zero days. The screen's Clear choices now uses resetResupplyPlan, and there are screen tests for clearing and for carry-through.
  2. Screen reader can't reach option details. Fixed. The radio holds only the circle and the name. It speaks the full name, with the facts as the hint. The facts and More/Less are now separate focusable elements, and a test checks this.
  3. Ambiguous spoken labels. Fixed: "Fewer/More zero days at " and "None – carry through at ".
  4. Memoisation was a no-op. Fixed. The value comparators sameGroupRow / sameLegRow are in place, with comparator tests and a render-count test.
  5. Invalid section mismatch. Fixed: both the row and the screen use the whole trail, and the row reads "Whole trail · …".
  6. Waypoint detail vs planner before any plan. Fixed: the waypoint detail uses resupplyGroupChoice.
  7. Dead code. saveResupplyStops is removed and the doc comment is corrected.

Also new: single-place turn-offs are headed by the short name plus a "turn-off at …" line, and the full name is kept as the accessibility label.

Fixes are as reported by the implementing agent and covered by new tests. It has still not been run on a device or emulator.

🤖 Generated with Claude Code

eamon-b added a commit that referenced this pull request Oct 25, 2026
Review of #236 and #238:

- A section that starts at a town no longer walks out of it again: the
  leg arriving there, in the section before, counted the side trip both
  ways, so two sections meeting at a town added up to more than the whole
  trail (0-50 + 50-100 was 109 km against 106). Only a stop at the trail's
  own start, which no leg arrives at, has its walk out on the leg leaving.
- With a camp plan, a leg's arrival comes from that plan alone: its rest
  nights are already in its dates, and adding the resupply zero days too
  counted the same rest twice. The day number is now the calendar day the
  date falls on (walking days plus earlier rest days), so day and date
  agree. Zero days still count in the summary's total days.
- Only a stop decided on one place takes zero days; an undecided one takes
  none rather than the most any of its places holds.
- Zero days follow the selection: setResupplyStops with an explicit list,
  and so chooseResupplyOption, drops those of options no longer ticked
  (carrying through included); absent keeps them. resetResupplyPlan
  clears the selection and the zero days together.
- chooseResupplyOption writes nothing when the group already reads that
  way, so a lone place chosen under the every-option default keeps the
  default.
- The privacy policy lists the zero days a plan holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eamon-b
eamon-b changed the base branch from feat/resupply-planner to main October 25, 2026 08:40
eamon-b and others added 6 commits October 25, 2026 19:40
The resupply picker was a modal of checkboxes behind a "Choose stops"
link at the foot of the Plan screen, and the legs a card under it; the
owner could not find either. The planner is now a regular stack screen
(guide/[trailId]/resupply, title "Resupply"), laid out like the planning
spreadsheet: a summary on top (summariseResupplyPlan), the hiker's inputs
in one editable line, then start -> leg -> stop -> ... -> end, each leg
just above the stop it arrives at.

- A decision group is a radio list plus "None - carry through"
  (chooseResupplyOption); a lone place is a Yes/No switch; a group with
  more than one place still ticked says "Not decided yet".
- Each option shows storeLabel, its access or "On trail", accepts boxes
  and the description's first sentence, the rest behind More.
- Each leg shows walked km with legSideTripText, rides, climb, days, food
  and a long-carry flag. No arrival days, and no camp plan is passed to
  computeResupplyLegs, so the plan's rest days and the zero days are
  never counted twice.
- A chosen stop has a half-day zero-days stepper (setResupplyZeroDays,
  through plans-store.apply), passed to the legs as zeroDays.
- Hours/day, pace, food per day and long-carry days are editable from
  the screen (plan-inputs-store, in the existing sheet and cards).

The Plan screen's Resupply section becomes a labelled row near the top
("5 stops · longest 7 days · 2 to decide") that opens the screen with
the section as route params; the waypoint detail's "Plan resupply here"
toggle gains a link to it. ResupplyCard, ResupplySelectList and the
checkbox toggle are removed with their tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With every option ticked by default, "Plan resupply here" on a town at a
turn-off with several towns added a tick, leaving the group undecided on
the resupply screen. It now makes the same decision the screen does
(waypointResupplyEditor): planning picks this place in its group
(chooseResupplyOption), removing the group's chosen place carries the
group through, and removing a tick from a group still undecided takes
only that tick out. A lone place stays Yes/No. The edit reads the
document when it runs, with the device-local selection as the base only
while the document has none.

setResupplyStopSelected had no caller left and goes with its tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Group headers use resupplyGroupName and the rows inside a turn-off
resupplyOptionShortName, so "Salida (access: Monarch Pass (US 50))" under
"Monarch Pass (US 50)" reads "Salida" (the full name stays the
accessibility label). Each leg row carries legEndpointNames, so a leg
into a group still undecided ends at "Monarch Pass (US 50) (not
decided)" rather than every place joined, and its side trip is "off
trail and back" rather than to a list of towns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
groupHeading titles a group of one by the place's short name ("Chama") with
"turn-off at Cumbres Pass (CO 17)" underneath, from the group label; the full
name stays the accessibility label. A decision group is still headed by its
turn-off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Clear choices resets through resetResupplyPlan, so the zero days go
  with the selection; choosing another place or carrying through drops
  the left-out places' zero days in the shared editor.
- A multi-place group's radio speaks the place's full name with its facts
  as the hint; the facts and More/Less sit beside the radio, each its own
  focusable element, instead of inside one Pressable label.
- Repeated controls name their place or turn-off: "Fewer/More zero days
  at Salida", "None - carry through at Monarch Pass (US 50)".
- Rows are memoised by value (sameGroupRow, sameLegRow), since
  plannerRows rebuilds every row object on each edit; a tap now
  re-renders the changed rows only.
- An invalid Plan section: the Resupply row is worked out for the whole
  trail and says "Whole trail · ...", which is what the screen shows.
- The waypoint detail reads the group's choice with resupplyGroupChoice,
  so a lone place under the every-option default shows as planned with
  Remove, as the planner does; a place in an undecided group picks.
- saveResupplyStops is gone, and useStoredResupplyStops says what it is
  for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eamon-b
eamon-b force-pushed the feat/resupply-planner-mobile branch from 09e8ce1 to 83c6df7 Compare October 25, 2026 08:40
@eamon-b
eamon-b merged commit 3121266 into main Oct 25, 2026
6 checks passed

This branch was successfully deployed

1 active deployment
Preview — 83c6df74 Deployed Oct 25, 2026 by vercel[bot]
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.

1 participant