Skip to content

feat(focus): single 'Go' focused trip, pinned on home#58

Merged
JamieRuderman merged 66 commits into
mainfrom
feat/go-focused-trip
Jun 7, 2026
Merged

feat(focus): single 'Go' focused trip, pinned on home#58
JamieRuderman merged 66 commits into
mainfrom
feat/go-focused-trip

Conversation

@JamieRuderman
Copy link
Copy Markdown
Owner

@JamieRuderman JamieRuderman commented May 31, 2026

Summary

Replaces the per-trip departure-reminder array with a single, global, user-indicated focused trip ("Go" — I'm taking this train), pinned at the top of the home screen with live times. The reminder is demoted to an opt-in sub-option of the focused trip.

  • Go control in the trip detail sheet sets the focused trip. At most one is focused; tapping Go on a different trip prompts a "Switch trains?" confirm. The lead-time reminder slider is a sub-option (off by default), with a Stop to un-focus.
  • Pinned FocusedTripCard always shows above the schedule, reconstructed from static schedule via its stored identity and overlaid with live realtime status (delays/cancellations) for the focused leg — independent of the home screen's current from/to. Its duplicate row is hidden from the list when on the matching leg.
  • Auto-clears on arrival (derived from the service date + schedule), so the trip stays focused for the whole ride.
  • source: "user" seam so the deferred riding-detector integration (spec fix(api): allow Capacitor CORS for GTFS-RT endpoints #2) needs no schema change.
  • selectedTripNumber (open-sheet / deep-link state) and focusedTrip are deliberately separate facets on the same provider.
  • Legacy smart-train-departure-reminders array is migrated on first load, then removed. Notification mechanics extracted into notificationScheduler.ts; old departureReminder.ts / useDepartureReminder.ts deleted.

Data model — anchored on identity + service date (not stored times)

The focused trip stores its identity (train number + leg + scheduleType) and a serviceDate anchor; departure/arrival are derived (reconstruct static schedule → resolve against serviceDate → overlay realtime). This is why the pinned card shows live times rather than a frozen snapshot.

We deliberately do not anchor on GTFS trip_id: SMART's trip_ids are synthetic and regenerated per feed build (static t_6153517_b_86615_tn_0 vs realtime t_6043274_b_86583_tn_0), and our data refreshes daily — a stored trip_id would dangle after the next republication. The stable public identity is the train number, which the app already uses. See the spec's "Revision" section for the full rationale.

Design spec: docs/superpowers/specs/2026-05-29-go-focused-trip-design.md
Implementation plan: docs/superpowers/plans/2026-05-29-go-focused-trip.md

Test plan

  • 76 unit tests passing (storage round-trip, schedule-derived arrival expiry, stale-trip clear, legacy migration → serviceDate, trip reconstruction, scheduler capability/permission). npx vitest run
  • Typecheck clean (npx tsc -p tsconfig.app.json --noEmit), npx vite build succeeds.
  • On-device / browser walkthrough (pending): Go pins the trip with live times and removes its list row; set a reminder, reload → persists; tap Go on another trip → "Switch trains?"; change pickers to a different leg → pinned card still shows; Stop → returns to list; card clears after arrival; delayed train shows live delay on the pinned card.
  • Android/iOS: native reminder notification fires; iOS-web App Store CTA still appears where notifications are unsupported.

Known v1 limitations

  • Auto-clear uses the static (scheduled) arrival on the service date; a delayed train's card could clear slightly before the actual late arrival. Live-arrival expiry is a possible refinement.
  • Reminder fire time is rescheduled on observed drift only while the trip-detail sheet is open (parity with the previous reminder behavior); the pinned card's display is always live.

Follow-up

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented May 31, 2026

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

Project Deployment Actions Updated (UTC)
smart-trip Ready Ready Preview, Comment Jun 7, 2026 12:49am

Schedule the focused-trip leave reminder as a real Apple AlarmKit alarm on
iOS 26+ so it breaks through Silent Mode / Focus, instead of a local
notification that's easy to miss. Falls back to the existing notification
everywhere else (Android, web, AlarmKit unavailable/denied, or any
scheduling error). The alarm replaces the notification — never both.

Stacked on the go-focused-trip refactor: the AlarmKit preference lives in
the focused-trip orchestration (useFocusedTrip) plus a thin iOS-only
wrapper; notificationScheduler.ts stays a pure primitive.

- Add @capgo/capacitor-alarm + src/lib/native/leaveAlarm.ts wrapper
  (availability/auth/schedule/cancel + pure decideReminderChannel).
- armAndPersistReminder prefers a Leave Alarm, scheduling the new channel
  before retiring the old one so a failed (re)schedule never leaves the
  user with no reminder; persist the alarm id on FocusedTripReminder.
- Cancel/clear/disarm retire both channels (notification + alarm).
- Label the active pill "Leave alarm" when AlarmKit-backed (en + es).
- Add NSAlarmKitUsageDescription to Info.plist.
- Vitest coverage for the channel decision and alarm scheduling/fallback.

Android keeps the notification: the plugin's Android path only hands off to
the system Clock app (uncancellable), and true break-through alarms require
USE_FULL_SCREEN_INTENT, which Android 14+ won't grant a transit app by
default.
- Date-safety (high): @Capgo's createAlarm only takes a clock time and fires
  at the NEXT occurrence of that HH:MM — it can't target a calendar date. A
  weekend trip focused on a weekday (serviceDate = nextServiceDate) or a
  "tomorrow" departure whose time recurs earlier today would have fired days
  early. scheduleLeaveAlarm now bails (→ dated notification fallback) unless
  fireAt is the next occurrence of its own clock time (alarmFiresOnIntendedDay).
- Stale-focus race: a permission prompt can block long enough for the user to
  Stop / switch trains / the trip to auto-clear. armAndPersistReminder now
  re-reads the focus after scheduling and, if it changed, rolls back the
  freshly scheduled alarm/notification instead of resurrecting the stale trip.
- Permission gate: notification permission is now requested only on the
  notification fallback path, so an iOS user who denied notifications but
  authorized AlarmKit can still set a Leave Alarm.

Adds unit coverage for the day-safety helper and the off-day fallback.
@JamieRuderman JamieRuderman merged commit 396f74c into main Jun 7, 2026
2 checks passed
@JamieRuderman JamieRuderman deleted the feat/go-focused-trip branch June 7, 2026 00:51
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.

2 participants