save_issue created a Todo bug in the EPAC team with priority High, estimate 8, a full root-cause analysis, acceptance criteria, implementation options, risks, manual reproduction steps, and definition of done.
Raw JSONL row 99
{"parentUuid":"bb40bb95-ae1a-4e81-92b6-b99100b64525","isSidechain":false,"message":{"model":"claude-opus-4-7","id":"msg_01TcK2dmxrAHsPC95jHowZy2","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_018ebkRpFnQbfKiMMuVAtoWc","name":"mcp__852c51d3-c3ae-4813-8f86-6e0893d9ef71__save_issue","input":{"title":"Calendar scrolls to Jan 2001 on Parliament tab re-tap; add prominent Today action and de-clutter toolbar","team":"EPAC","state":"Todo","estimate":8,"priority":2,"labels":["Bug"],"description":"## Context / background\n\nOn the Parliament tab's sitting calendar (`SittingCalendarView`), tapping the **Parliament** tab item while it is already the active tab scrolls the calendar all the way up to **January 2001** — the start of the configured visible date range. There is no parliamentary data before 2016 (the year picker's lower bound is hard-coded to 2016 at [SittingCalendarView.swift:204](ios/epac/Views/Calendar/SittingCalendarView.swift:204)), so the user lands in an empty pre-history with no obvious way back.\n\nThere is a hidden \"tap to scroll to today\" affordance on the legend dot at [SittingCalendarView.swift:127-133](ios/epac/Views/Calendar/SittingCalendarView.swift:127), but it reads as a legend swatch, not as a control. The accessibility label `\"Today — tap to scroll to current month\"` confirms it is meant as a button, but visually it is indistinguishable from the \"Upcoming\" legend swatch next to it.\n\nAdjacent concern flagged by user: the Parliament toolbar and the Members toolbar are getting crowded — this issue should also consolidate.\n\n## Root cause analysis\n\n**Confirmed root cause:** standard iOS `UIScrollView.scrollsToTop` behavior, applied to a HorizonCalendar `CalendarScrollView` whose content range begins at 2001-01-01.\n\nEvidence:\n\n1. **HorizonCalendar's scroll view is a `UIScrollView`** with default settings:\n `CalendarScrollView: UIScrollView` at [`ios/.build/SourcePackages/checkouts/HorizonCalendar/Sources/Internal/CalendarScrollView.swift:24`](ios/.build/SourcePackages/checkouts/HorizonCalendar/Sources/Internal/CalendarScrollView.swift:24). It does not override `scrollsToTop`, so it inherits the default `true`.\n2. **iOS tab re-tap triggers `scrollsToTop`.** When the user taps a `TabView`'s currently-active tab item, UIKit walks the active tab's view hierarchy and asks the first eligible scroll view with `scrollsToTop == true` to scroll its `contentOffset` to `.zero`. This is the same mechanism as the legacy status-bar tap.\n3. **`contentOffset.zero` for this calendar is January 2001.** `visibleDates` is configured as\n `ISO8601DateFormatter().date(from: \"2001-01-01T23:59:59Z\")!...ISO8601DateFormatter().date(from: \"2026-12-31T23:59:59Z\")!`\n at [SittingCalendarView.swift:30](ios/epac/Views/Calendar/SittingCalendarView.swift:30) with `monthsLayout: .vertical`. So \"top\" of the scroll view is January 2001 — the earliest month in the range.\n4. **Year picker bound differs from calendar bound.** The chevron year picker is clamped to 2016+ ([SittingCalendarView.swift:194-204](ios/epac/Views/Calendar/SittingCalendarView.swift:194)), so even when the user lands in 2001 the picker can't take them back through chevrons. The only existing escape is the unobvious \"Today\" legend dot.\n5. **Tab re-tap intent is correct; the calendar's response is wrong.** Double-tapping a tab bar item to scroll to top is standard iOS behavior we should not fight system-wide — but for *this* scroll view it produces a non-actionable state. We do not gain anything from preserving \"scroll to start of 25-year range,\" and we lose discoverability.\n\nRejected candidates (with reasons):\n\n- **NavigationStack popping to root** — the calendar is the root of `parliamentStack` ([ContentView.swift:464-481](ios/epac/Views/ContentView.swift:464)); there is no parent to pop to. The popping happens (and is correct) on subsequent taps, but it is not what scrolls the calendar to 2001.\n- **`task(id: viewModel.currentYear)` re-firing** — the initial-load task at [SittingCalendarView.swift:153-160](ios/epac/Views/Calendar/SittingCalendarView.swift:153) does scroll to today, but only `if viewModel.dates.isEmpty`. On subsequent tab visits the dates are cached, so the task body is a no-op. Not the cause.\n- **HorizonCalendar's own scroll restoration** — `CalendarViewProxy` does not register an automatic scroll-to-top hook with the tab bar. The behavior comes from UIKit's traversal, not HorizonCalendar.\n\nConfidence: high. The mechanism is well-documented UIKit behavior, the source file confirms `CalendarScrollView` does not opt out, and the date-range arithmetic explains why \"top\" is January 2001 specifically.\n\n## Acceptance criteria\n\n- [ ] Given the user is on the Parliament tab with the calendar visible at any month, when they tap the Parliament tab bar item (re-selecting the active tab), then the calendar **does not** scroll to January 2001.\n- [ ] Given the user is anywhere in the Parliament calendar's date range, when they tap a prominent \"Today\" affordance, then the calendar scrolls/animates to the month containing today and centers today's date if possible.\n- [ ] The \"Today\" affordance must be visible from the calendar's default state without requiring the user to scroll, open a menu, or recognize the legend dot as a button. Pick **one** placement and remove the legend-dot tap target so there is only one canonical Today action: prefer a toolbar item (label `\"Today\"`, system image `calendar.circle` or similar), OR a sticky bottom-bar pill above the safe-area inset. Author decides; both meet AC.\n- [ ] The \"Today\" control is hidden or disabled when today's date is already inside the currently visible month range (avoid noise when there's nothing to do).\n- [ ] Parliament toolbar is de-cluttered. Today's top-bar contains: year-picker (principal), refresh, calendar-export menu, Order Paper link, Committees link, Ontario Debates link — 6 distinct items plus a 3-item principal cluster. Consolidate by **either** (a) grouping Order Paper / Committees / Ontario Debates into a single `Menu` (ellipsis or `line.3.horizontal`) with one toolbar slot, **or** (b) moving Committees + Ontario Debates into the `Accountability` / `Members` hubs they thematically belong to (see ADR-001 in `CLAUDE.md`). Author decides; document the choice in the PR description.\n- [ ] Members toolbar (`MembersView.swift:45-118`) is also de-cluttered. Five sequential filter menus + a clear button in one `ToolbarItemGroup` is too dense. Consolidate by either (a) combining `Party`, `Province`, `Status`, `Cabinet` into a single \"Filters\" menu that expands into pickers, with the active-filter count rendered as a badge on the filter icon, or (b) moving filters into a sheet/bottom-bar surfaced by one toolbar button. Author decides.\n- [ ] The fix does **not** change scroll-to-top behavior on tabs whose primary content is conventionally oriented (Home, Members list, Accountability, Search). Specifically: do not blanket-disable `scrollsToTop` across the app; scope the change to the calendar scroll view only.\n- [ ] On tab re-tap of Parliament, if the calendar is *not* currently showing today, the system can optionally scroll to today (a reasonable user expectation given \"tab re-tap = go to a sensible home\"). If implemented, document the choice in the PR description; if not, do nothing and rely on the explicit Today button. Either is acceptable.\n- [ ] Existing snapshot / unit tests in `SittingCalendarViewModelTests` continue to pass; new test added: `SittingCalendarView` does not opt the underlying scroll view into `scrollsToTop`.\n- [ ] Manual verification steps below pass on iPhone simulator (iOS 17 baseline, iOS 18+ acceptable) and on iPad.\n\n## Implementation approach (non-prescriptive)\n\nTwo viable mechanisms — pick the one with fewer side effects after spike-checking both:\n\n**Option A — opt the calendar scroll view out of `scrollsToTop`.** HorizonCalendar does not currently expose a public modifier to set `scrollsToTop`. Either:\n- Submit a small upstream PR to `airbnb/HorizonCalendar` adding `.scrollsToTop(_:)` on `CalendarViewRepresentable` (preferred long-term), or\n- Walk the view hierarchy from a `UIViewRepresentable` shim inserted alongside the calendar to find the `CalendarScrollView` and set `scrollsToTop = false`. This is fragile but localized — keep it in one helper file with a comment pointing at this issue.\n\n**Option B — intercept the tab re-tap.** Add a `UITabBarControllerDelegate` (via `UITabBarController.appearance()` is not enough; need a real introspection layer) that, when the user re-selects the Parliament tab, runs `calendarViewProxy.scrollToMonth(containing: .now, …)` instead of letting the system scroll to `.zero`. iOS 18 ships a `tabViewSelection` API that makes this cleaner.\n\n**Recommended:** Option A, scoped to the calendar view only. It's the smaller change and doesn't reach across the app. If Option A is infeasible without an upstream change, Option B is the fallback.\n\n## External validation gates\n\n- [ ] None — this is a pure iOS UI fix verifiable from repo changes, simulator, and a real device. No App Store Connect, legal, or product sign-off needed.\n\n## Out of scope\n\n- Changing the calendar's date range lower bound (the data backstop is 2016, but HorizonCalendar's range stays 2001+ as a buffer). Not touching this here.\n- Removing the legend \"Sitting days / Today / Upcoming\" row entirely. The colors-legend stays; only its dual-purpose tap behavior is removed once the explicit Today control replaces it.\n- Reworking the iPad sidebar / `parliamentStack` navigation flow.\n- Backend or data pipeline work.\n- Any change to `HomeFeedView`, `AccountabilityHubView`, or `SearchView`.\n\n## Inputs / dependencies\n\n- iOS source: `ios/epac/Views/Calendar/SittingCalendarView.swift`, `ios/epac/Views/ContentView.swift`, `ios/epac/Views/Members/MembersView.swift`.\n- HorizonCalendar SDK source (read-only): `ios/.build/SourcePackages/checkouts/HorizonCalendar/Sources/`. Pinned version comes from `ios/epac.xcodeproj` SPM resolution.\n- Existing `CalendarViewProxy` API: `scrollToMonth(containing:scrollPosition:animated:)` already used at [SittingCalendarView.swift:131](ios/epac/Views/Calendar/SittingCalendarView.swift:131) and `:157`.\n- Tab structure: `ContentView.phoneLayout` and `iPadLayout` in [`ios/epac/Views/ContentView.swift`](ios/epac/Views/ContentView.swift:168).\n\n## Risks / notes for implementer\n\n- **Don't blanket-disable `scrollsToTop` app-wide.** Status-bar / tab re-tap scroll-to-top is correct behavior on every other scroll view in the app. Scope strictly to the calendar.\n- **`CalendarScrollView` is internal to HorizonCalendar.** If walking the view hierarchy via `UIViewRepresentable` introspection, do it in a single helper file and add a `// swiftlint:disable:next ... — reason` comment if needed. Add a test that fails loudly if HorizonCalendar's internal class name changes on a future package update.\n- **iPad sidebar selection ≠ tab re-tap.** iPad uses `NavigationSplitView` with `router.selectedTab` bindings ([ContentView.swift:195-228](ios/epac/Views/ContentView.swift:195)). The scroll-to-top bug only manifests on phone (compact). Verify iPad isn't accidentally regressed.\n- **Mac Catalyst.** The `MacCommandCenter` already calls `viewModel.onSelectedDateChanged` on refresh ([ContentView.swift:256-265](ios/epac/Views/ContentView.swift:256)). Nothing to change here, but verify on Catalyst.\n- **Year picker chevrons** independently scroll to the target year's January via `calendarViewProxy.scrollToMonth(containing: jan, …)` ([SittingCalendarView.swift:196-198](ios/epac/Views/Calendar/SittingCalendarView.swift:196)). Don't break this when restructuring the toolbar.\n\n## Definition of Done\n\n- Re-tapping the Parliament tab item no longer scrolls the calendar to January 2001 on iPhone and iPad simulators.\n- A single, visually obvious \"Today\" affordance returns the calendar to today's month from any point in the range.\n- Parliament and Members toolbars are visibly less crowded (count of top-bar items reduced; document the count delta in the PR description).\n- New scroll-behavior test added; existing tests pass; SwiftLint `--strict` passes; `make build` and `make simulator` succeed.\n- PR includes before/after screenshots of:\n - Parliament tab re-tap behavior (recorded as a short GIF or two screenshots).\n - Parliament toolbar before and after.\n - Members toolbar before and after.\n- `Release-Note:` line added (e.g. `Release-Note: Added a Today button to the Parliament calendar and stopped tab re-tap from scrolling to 2001`).\n\n## Architecture Impact\n\n- Affected layer: presentation only (Views/Calendar, Views/Members, possibly a tiny UIKit-bridge helper).\n- Dependency direction unchanged: View → `CalendarViewProxy` (already in place).\n- No new ports, no schema/migration impact, no backend change.\n- Test strategy: ViewModel layer unchanged (no new ViewModel tests). Add one View-layer integration check that asserts the calendar's scroll view has `scrollsToTop == false` after first appearance.\n\n## Mergeability / change ownership\n\n- Single reason to change: fix calendar tab-re-tap scroll + reduce Parliament/Members toolbar density.\n- Primary code owner: `ios/epac/Views/Calendar/SittingCalendarView.swift` (and ViewModels owner per CODEOWNERS).\n- Hot files: `SittingCalendarView.swift`, `MembersView.swift`, possibly a new `Util/CalendarScrollBehavior.swift` helper.\n- Sibling issues likely to touch the same area: none currently. Confirm at pickup by scanning open EPAC PRs touching `Views/Calendar/` or `Views/Members/`.\n- Conflict risk: low — narrow surface, no model or migration changes.\n- Sequencing lane: `parallel`.\n\n## Clean Architecture Shape\n\nN/A — UI affordance + scroll-view configuration change. No new use case, no port/adapter boundary, no domain entity touched. Catalog update: not required.\n\n## Manual reproduction steps\n\n1. `cd ios && SIM_UUID=<booted-sim-uuid> make simulator` (or run from Xcode against an iPhone simulator).\n2. App opens at Home.\n3. Tap **Parliament** tab. Calendar should show current month (May 2026 as of writing).\n4. Tap the **Parliament** tab item again.\n5. Observe: calendar instantly jumps to **January 2001** with no green sitting cells, no obvious affordance to return. (Year picker chevron-left is disabled at 2016; chevron-right works but takes many taps.)\n6. The only escape today is the tiny red \"Today\" legend dot near the bottom — tap it to return.\n\nExpected after fix: re-tapping Parliament does not move the calendar (or, if implementing the optional scroll-to-today on re-tap, scrolls to today's month). A prominent toolbar or bottom-bar \"Today\" action returns the user to today from anywhere.\n\nResolves the user-reported bug filed against the Parliament calendar screen on 2026-05-18."},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":6,"cache_creation_input_tokens":1979,"cache_read_input_tokens":99719,"output_tokens":5867,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":1979,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[{"input_tokens":6,"output_tokens":5867,"cache_read_input_tokens":99719,"cache_creation_input_tokens":1979,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":1979},"type":"message"}],"speed":"standard"},"diagnostics":null},"requestId":"req_011CbAcm1sR7LrtTr812Qact","type":"assistant","uuid":"6b0437a9-a26e-422e-aef9-dbc2fe3e0cba","timestamp":"2026-05-18T19:53:18.252Z","userType":"external","entrypoint":"claude-desktop","cwd":"/Users/sunny/code/epac-worktrees/parliament-tab-scroll-bug/ios","sessionId":"98f4ef34-3d43-48ec-8d4d-3887b9253263","version":"2.1.138","gitBranch":"HEAD"}