Skip to content

feat: Smart pre-trip notifications POC#1892

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/add-smart-pre-trip-notifications
Draft

feat: Smart pre-trip notifications POC#1892
Copilot wants to merge 3 commits intomainfrom
copilot/add-smart-pre-trip-notifications

Conversation

Copy link
Contributor

Copilot AI commented Mar 9, 2026

Original prompt

This section details on the original issue you should resolve

<issue_title>Feature Request: Smart pre-trip notifications - Pack reminders, device charging alerts, and trip readiness checks</issue_title>
<issue_description>Implement intelligent notification system that helps users prepare for upcoming trips by sending timely reminders to pack gear, charge devices, and complete pre-trip tasks.

Core Notification Features:

Packing Reminders

  • "Your trip starts in 3 days - time to start packing!"
  • Reminders for specific gear categories (camping gear, clothing, cooking supplies)
  • Checklist progress notification ("You've packed 60% of your items")
  • Last chance reminders ("Trip tomorrow! Don't forget to pack your sleeping bag")

Device Charging Alerts

  • "Charge your devices before the trip: Phone, Headlamp, Power bank."
  • Battery level recommendations for all electronic gear
  • Night-before charging reminder
  • Solar charger recommendations for off-grid trips

Trip Readiness Checks

  • Weather forecast notifications for trip destination
  • Trail/road condition updates
  • Permit/reservation reminders ("Don't forget your park pass!")
  • Check-in time reminders for accommodations
  • Travel document reminders (ID, reservations, maps)

Additional Smart features: (If possible)

Timely Alerts:

Time Before Trip

  • 7 days
    Notification message:
  • "Trip next week! Review your pack list."

Time Before Trip:

  • 1 day
    Notification message:
  • "Final pack check tomorrow morning!"

Time Before Trip

  • Morning of
    Notification message:
    "Have a great trip! Don't forget your [specific item]!"

Location-Based Triggers

  • Arriving at the trailhead: "Ready to hike? Here's your pack checklist."
  • Near outdoor store: "Need last-minute gear? REI is 2 miles away."
  • Weather alert for destination:." Storm expected at your campsite tonight!"

Group Trip Notifications (if possible)

  • "3 of 4 group members are 100% packed"
  • Group reminder: Meet at the trailhead at 8:00 am Saturday"

Personalized Recommendations

  • Based on past trips: "You usually bring extra batteries - don't forget them!"
  • Weather-based: "Cold front moving in - pack your thermal layers."
  • Activity-based: "Hiking 10 miles? Remember trekking poles and extra socks."</issue_description>

Comments on the Issue (you are @copilot in this section)

@andrew-bierman @copilot Assigned for POC development. Please investigate and create a proof of concept implementation.

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

- Add GET /api/notifications/trip-reminders endpoint that generates
  smart notification messages based on days until trip (7, 3, 1 day
  and day-of) including pack item counts and device charging alerts
- Add useTripNotifications hook in Expo app to fetch trip reminders
- Add TripNotificationsList component with priority-colored cards
- Update notifications settings screen to display trip reminders
- Add i18n translation keys for notification strings

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
Copilot AI changed the title [WIP] Add smart pre-trip notifications for users feat: Smart pre-trip notifications POC Mar 9, 2026
@andrew-bierman
Copy link
Collaborator

@coderabbitai Please review this PR for code quality, best practices, and potential issues.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all new/modified files related to notifications
echo "=== Notification-related files ==="
fd -e ts -e tsx -e js -e json . --exec grep -l "notification" {} \; 2>/dev/null | grep -v node_modules | grep -v .git | head -40

echo ""
echo "=== Trip reminders API route ==="
fd "trip-reminders" --type f 2>/dev/null

echo ""
echo "=== useTripNotifications hook ==="
fd "useTripNotifications" --type f 2>/dev/null

echo ""
echo "=== TripNotificationsList component ==="
fd "TripNotificationsList" --type f 2>/dev/null

echo ""
echo "=== Notifications screen ==="
fd -e ts -e tsx . --exec grep -l "Trip Reminders" {} \; 2>/dev/null | grep -v node_modules | grep -v .git | head -10

Length of output: 749


🏁 Script executed:

#!/bin/bash
echo "=== packages/api/src/routes/notifications/index.ts ==="
cat packages/api/src/routes/notifications/index.ts

echo ""
echo "=== packages/api/src/routes/index.ts (notifications section) ==="
grep -n -A5 -B5 "notification" packages/api/src/routes/index.ts

echo ""
echo "=== apps/expo/features/trips/hooks/useTripNotifications.ts ==="
cat apps/expo/features/trips/hooks/useTripNotifications.ts

echo ""
echo "=== apps/expo/features/trips/components/TripNotificationsList.tsx ==="
cat apps/expo/features/trips/components/TripNotificationsList.tsx

echo ""
echo "=== apps/expo/app/(app)/(tabs)/profile/notifications.tsx ==="
cat "apps/expo/app/(app)/(tabs)/profile/notifications.tsx"

echo ""
echo "=== en.json notifications keys ==="
python3 -c "
import json
with open('apps/expo/lib/i18n/locales/en.json') as f:
    data = json.load(f)
print(json.dumps(data.get('notifications', {}), indent=2))
"

echo ""
echo "=== i18n types (notifications section) ==="
grep -n -A30 "notifications" apps/expo/lib/i18n/types.ts | head -60

Length of output: 20797


🏁 Script executed:

#!/bin/bash
echo "=== trips schema (deleted field, startDate, userId) ==="
fd "schema" --type f | xargs grep -l "trips" 2>/dev/null | grep -v node_modules | head -5

fd "schema" --type f | xargs grep -l "trips" 2>/dev/null | grep -v node_modules | while read f; do
  echo "--- $f ---"
  grep -n "deleted\|startDate\|userId\|trips" "$f" | head -30
done

echo ""
echo "=== hooks/index.ts barrel export ==="
cat apps/expo/features/trips/hooks/index.ts 2>/dev/null || echo "File not found"

echo ""
echo "=== i18n types.ts — notifications keys ==="
grep -n "notifications\." apps/expo/lib/i18n/types.ts | head -40

echo ""
echo "=== Check if 'refresh' is exposed/used anywhere ==="
rg -n "refresh" apps/expo/features/trips/ --type ts --type tsx -C2 2>/dev/null || \
  rg -n "refresh" apps/expo/features/trips/ -C2

echo ""
echo "=== Check how canSave is computed ==="
grep -n "canSave" "apps/expo/app/(app)/(tabs)/profile/notifications.tsx"

echo ""
echo "=== Check drizzle orderBy usage pattern in other routes ==="
rg -n "orderBy" packages/api/src/routes/ -C2 | head -40

echo ""
echo "=== Check if trips table has 'deleted' boolean field ==="
fd "trips" --type f | xargs grep -l "deleted" 2>/dev/null | grep -v node_modules | head -5 | while read f; do
  echo "--- $f ---"
  grep -n "deleted" "$f" | head -10
done

echo ""
echo "=== Check packItems schema for 'deleted' field ==="
fd "schema" --type f | xargs grep -n "packItems\|pack_items" 2>/dev/null | grep -v node_modules | head -20

echo ""
echo "=== Check if refresh is passed to TripNotificationsList ==="
grep -n "refresh\|useTripNotification" "apps/expo/app/(app)/(tabs)/profile/notifications.tsx"

Length of output: 8511


@andrew-bierman: Here is a detailed review of the PR covering code quality, correctness, and best practices.


Review: Smart Pre-Trip Notifications POC

🔴 Critical Issues


1. morning_of notification never fires — Math.ceil bug

File: packages/api/src/routes/notifications/index.ts

Any trip starting later today (e.g., 6 hours from now) produces diffDays ≈ 0.25. Math.ceil(0.25) is 1, so the === 0 branch is unreachable for any real same-day trip.

// Current (buggy) — Math.ceil(0.25) === 1, not 0
const daysUntilTrip = Math.ceil(diffMs / (1000 * 60 * 60 * 24));

// Suggested fix — compare calendar dates, ignoring time-of-day
const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const tripDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
const daysUntilTrip = Math.round((tripDate.getTime() - nowDate.getTime()) / (1000 * 60 * 60 * 24));

2. N+1 query — pack item counts fetched in a loop

File: packages/api/src/routes/notifications/index.ts

For every trip with a packId, an individual SELECT is issued inside the for loop. With many upcoming trips this degrades linearly.

// Current — N DB round-trips
for (const trip of futureTrips) {
  if (trip.packId) {
    const items = await db.select(...).from(packItems).where(...); // ← inside loop
  }
}

// Suggested fix — batch all packIds, then aggregate in memory
const packIds = futureTrips.map((t) => t.packId).filter(Boolean) as string[];
const allItems = packIds.length
  ? await db.select({ packId: packItems.packId, id: packItems.id })
      .from(packItems)
      .where(and(inArray(packItems.packId, packIds), eq(packItems.deleted, false)))
  : [];

const itemCountByPackId = allItems.reduce<Record<string, number>>((acc, item) => {
  acc[item.packId] = (acc[item.packId] ?? 0) + 1;
  return acc;
}, {});

// Then inside the loop: const packItemCount = itemCountByPackId[trip.packId] ?? 0;

3. No DB-level date filter — full table scan per user

File: packages/api/src/routes/notifications/index.ts

findMany fetches all non-deleted trips for the user and filters the date window in JavaScript. For an active user with hundreds of past trips, this is unnecessarily expensive.

import { gte, lte } from 'drizzle-orm';

const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);

const futureTrips = await db.query.trips.findMany({
  where: and(
    eq(trips.userId, auth.userId),
    eq(trips.deleted, false),
    gte(trips.startDate, now),
    lte(trips.startDate, thirtyDaysFromNow),
  ),
  with: { pack: true },
  orderBy: (t, { asc }) => [asc(t.startDate)],
});

This also removes the post-fetch filter call entirely.


4. notifications.* i18n keys not registered in TranslationKey type

File: apps/expo/lib/i18n/types.ts

The grep for notifications. returned zero results in types.ts. Keys like t('notifications.tripReminders'), t('notifications.today'), t('notifications.daysAway', ...) etc. used in TripNotificationsList.tsx and notifications.tsx are not in the union type, meaning they will fail TypeScript type checking. All the new keys need to be added:

// In apps/expo/lib/i18n/types.ts, add to the TranslationKey union:
| 'notifications.tripReminders'
| 'notifications.tripRemindersSubtitle'
| 'notifications.loadingReminders'
| 'notifications.loadError'
| 'notifications.noUpcomingReminders'
| 'notifications.today'
| 'notifications.tomorrow'
| 'notifications.daysAway'

🟡 Medium Issues


5. refresh is never wired to any UI

File: apps/expo/app/(app)/(tabs)/profile/notifications.tsx

The hook exposes a refresh() escape hatch but it is silently dropped:

// Current
const { notifications: tripNotifications, isLoading, error } = useTripNotifications();

// Suggested — wire to pull-to-refresh on the outer ScrollView
const { notifications: tripNotifications, isLoading, error, refresh } = useTripNotifications();

// Then on ScrollView:
<ScrollView
  refreshControl={<RefreshControl refreshing={isLoading} onRefresh={refresh} />}
  ...
>

Also, the error state in TripNotificationsList shows a message but offers no retry action. Consider passing onRetry as a prop.


6. Nested ScrollView with scrollEnabled={false}

File: apps/expo/features/trips/components/TripNotificationsList.tsx

A <ScrollView scrollEnabled={false}> nested inside the outer ScrollView in notifications.tsx can cause touch event propagation issues on Android. Since scrolling is disabled, a plain <View> or <> fragment would be semantically cleaner and avoids the nested-scroll pitfall:

// Instead of:
<ScrollView scrollEnabled={false}>
  {notifications.map(...)}
</ScrollView>

// Use:
<View>
  {notifications.map(...)}
</View>

7. Unused i18n keys in en.json

File: apps/expo/lib/i18n/locales/en.json

The following keys are defined but appear unreferenced in any component or screen:

  • noRemindersTitle
  • noRemindersMessage
  • packingReminders
  • deviceAlerts
  • tripReadiness
  • upcomingTripsCount / upcomingTripsCount_plural
  • viewTrip

Either wire them into the UI or remove them to avoid dead translation strings.


8. orderBy missing explicit direction

File: packages/api/src/routes/notifications/index.ts

orderBy: (t) => t.startDate,  // direction unspecified

The existing codebase pattern (e.g., chat.ts) uses the second operators argument:

orderBy: (t, { asc }) => [asc(t.startDate)],

While Drizzle likely defaults to ASC, being explicit is consistent with the rest of the codebase.


🔵 Low / Minor Issues


9. Unsafe icon name cast in TripNotificationsList

File: apps/expo/features/trips/components/TripNotificationsList.tsx

name={
  (NOTIFICATION_ICON[notification.type] as Parameters<typeof Icon>[0]['name']) ??
  'bell-outline'
}

The as cast bypasses compile-time safety. If any of the icon names (e.g., calendar-clock, package-variant, flag-checkered, check-all) are not valid for @roninoss/icons, it will silently fail at runtime. Validate the icon names against the library's type, or use a safe lookup with a typed constant.


10. No AbortController for fetch cancellation

File: apps/expo/features/trips/hooks/useTripNotifications.ts

The cancelled flag prevents state updates after unmount, but the underlying HTTP request continues. Using AbortController is more robust, especially on slow connections:

useEffect(() => {
  const controller = new AbortController();

  async function fetchReminders() {
    setIsLoading(true);
    setError(null);
    try {
      const res = await axiosInstance.get<TripRemindersResponse>(
        '/api/notifications/trip-reminders',
        { signal: controller.signal },
      );
      setNotifications(res.data.notifications ?? []);
      setUpcomingTripsCount(res.data.upcomingTripsCount ?? 0);
    } catch (err) {
      if (!axios.isCancel(err)) {
        const { message } = handleApiError(err);
        setError(message);
      }
    } finally {
      setIsLoading(false);
    }
  }

  fetchReminders();
  return () => controller.abort();
}, [refreshKey]);

Summary Table

# Severity Location Issue
1 🔴 Critical notifications/index.ts Math.ceilmorning_of branch never fires
2 🔴 Critical notifications/index.ts N+1 query for pack item counts
3 🔴 Critical notifications/index.ts No DB-level date filter
4 🔴 Critical i18n/types.ts notifications.* keys not in TranslationKey
5 🟡 Medium notifications.tsx refresh unconnected to pull-to-refresh / retry
6 🟡 Medium TripNotificationsList.tsx Nested ScrollView with scrollEnabled={false}
7 🟡 Medium en.json Several i18n keys defined but unused
8 🟡 Medium notifications/index.ts orderBy missing explicit direction
9 🔵 Low TripNotificationsList.tsx Unsafe as cast for icon name
10 🔵 Low useTripNotifications.ts cancelled flag instead of AbortController

@andrew-bierman
Copy link
Collaborator

@copilot CodeRabbit has provided review feedback on this PR. Please review and address the comments.

- Fix morning_of notification bug: use calendar-date Math.round instead
  of Math.ceil to correctly detect day-of-trip (fixes same-day trips)
- Fix N+1 query: batch-fetch all pack item counts with single inArray
  query instead of per-trip SELECT inside loop
- Add DB-level date filter (gte/lte) to trips query; remove JS filter
- Add explicit asc() direction to orderBy per codebase convention
- Add notifications.* keys to TranslationKey union type in i18n/types.ts
- Wire refresh() to pull-to-refresh RefreshControl on notifications screen
- Pass onRetry prop to TripNotificationsList for error state retry action
- Replace nested ScrollView scrollEnabled={false} with plain View
- Remove 7 unused i18n keys from en.json
- Type NOTIFICATION_ICON map with MaterialIconName from @roninoss/icons;
  replace 4 invalid icon names (package-variant, flag-checkered,
  check-all, battery-charging) with valid equivalents; fix error/empty
  state icons (alert-circle-outline, bell-check-outline)
- Replace cancelled boolean flag with AbortController + axios.isCancel

Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
@andrew-bierman
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Warning

Rate limit exceeded

@andrew-bierman has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 44 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0534d68-bf42-4c6c-a807-6155accbc515

📥 Commits

Reviewing files that changed from the base of the PR and between 3f94763 and ecf70fb.

📒 Files selected for processing (8)
  • apps/expo/app/(app)/(tabs)/profile/notifications.tsx
  • apps/expo/features/trips/components/TripNotificationsList.tsx
  • apps/expo/features/trips/hooks/index.ts
  • apps/expo/features/trips/hooks/useTripNotifications.ts
  • apps/expo/lib/i18n/locales/en.json
  • apps/expo/lib/i18n/types.ts
  • packages/api/src/routes/index.ts
  • packages/api/src/routes/notifications/index.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/add-smart-pre-trip-notifications

Comment @coderabbitai help to get the list of available commands and usage tips.

@andrew-bierman
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

Feature Request: Smart pre-trip notifications - Pack reminders, device charging alerts, and trip readiness checks

2 participants