diff --git a/.env.example b/.env.example index 42a7990..f239ac3 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,21 @@ # Server port PORT=3001 +# Public deployment URL used for redirects and generated links +DEPLOYMENT_URL=https://timetoleave.app + # Γ–BB HAFAS API HAFAS_URL=https://fahrplan.oebb.at/bin/mgate.exe +HAFAS_TIMEOUT_MS=10000 +HAFAS_VER=1.36 +HAFAS_LANG=eng +HAFAS_AID=hf7mcf9bv3nv8g5f +HAFAS_CLIENT_ID=OEBB +HAFAS_CLIENT_VER=6020700 +HAFAS_CLIENT_NAME=oebbApp + +# Optional Γ–BB GTFS enrichment +OEBB_GTFS_URL=https://static.web.oebb.at/open-data/soll-fahrplan-gtfs/GTFS_Fahrplan_2026.zip # Nominatim geocoding (OpenStreetMap) NOMINATIM_URL=https://nominatim.openstreetmap.org @@ -19,5 +32,17 @@ WIENER_LINIEN_API_URL=https://api.wienerlinien.at/darwin-v2 # CORS Configuration CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,https://timetoleave.app -# Deployment URL -DEPLOYMENT_URL=https://timetoleave.app +# API rate limiting +API_RATE_LIMIT_MAX_REQUESTS=120 +API_RATE_LIMIT_WINDOW_MS=60000 + +# Google Calendar OAuth (required for web Google Calendar sync) +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI=http://localhost:3000/api/auth/google/callback + +# Version returned by /api/health +APP_VERSION=0.1.0 + +# Mobile app backend URL for physical device builds +EXPO_PUBLIC_API_BASE_URL=http://localhost:3000 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e306a1..4ffce9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,32 +4,41 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -### πŸŒ™ Mobile Application +### Mobile Application - Default to dark theme across the mobile app - Filter native calendar events by location to exclude empty ones - Async station selection with error handling and alerts - Improved notification settings UI +- Native calendar source selection with CalDAV/DAVx, Apple, Google, Exchange, subscribed, local, CardDAV, ActiveSync, and other source labels +- Event detail sections split into reusable header, journey, bike, and nearby-stop components -### πŸ“… Calendar Integration +### Calendar Integration - **Google Calendar sync** via full OAuth 2.0 flow (token exchange, refresh, and status checks) - UI for connecting, syncing, and disconnecting Google accounts in the Calendar panel - Batch edit panel for managing event destinations on the web calendar - Edit support in AddEventModal for modifying existing events -### πŸš‰ Public Transport +### Public Transport - HAFAS LocMatch method in API client for finding nearest station to current location - Improved station selection with async search and error handling - WienerLinien departures hook improvements +- HAFAS response enrichment through the Γ–BB GTFS fallback when available +- Arrive-by journey search with fallback search window -### πŸ”§ API Client +### API Client -- Added `fetchGoogleCalendarEvents` method for Google Calendar sync -- Added `findNearestStation` method for geolocation-based station lookup +- Added `findStationByExtId` for resolving a saved station ID +- Added `findNearestStationByCoords` for geolocation-based station lookup +- Added base URL failover for backend availability issues - Enhanced HAFAS request handling +### Documentation + +- Updated README, development, architecture, API, user, privacy, and testing documentation to match the current route tree and app behavior + --- ## [1.0.0] β€” 2025-01-27 @@ -68,7 +77,7 @@ your calendar with real-time public transport data to tell you exactly when to l ### Routing -- Bike route calculation from origin to departure station via OSRM +- Bike route calculation and final walking route calculation via OSRM - Geocoding API integration for station lookups - Fallback and caching logic for API failures diff --git a/CHECKLIST.md b/CHECKLIST.md index fa0862d..ef63803 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -33,7 +33,7 @@ | # | Step | βœ… | βœ”οΈ | |---|---|----|-| | 19 | Add local notifications (expo-notifications) | [x] | [x] | -| 20 | Add native calendar import (post-MVP) | [~] | [~] | +| 20 | Add native calendar import with calendar selection | [x] | [x] | | 21 | Add mobile tests (core + API + store + screens) | [x] | [x] | | 22 | Prepare deployment (web backend + EAS mobile) | [x] | [x] | | 23 | Release MVP (verify acceptance criteria) | [x] | [x] | diff --git a/FEATURES_CHECKLIST.md b/FEATURES_CHECKLIST.md index f4daa17..42e9ea3 100644 --- a/FEATURES_CHECKLIST.md +++ b/FEATURES_CHECKLIST.md @@ -1,54 +1,57 @@ -# TimeToLeave β€” Features Implementation Checklist +# TimeToLeave - Features Implementation Checklist -## Phase 1 β€” New Settings Infrastructure (Steps 1-3) +This checklist tracks the route-planning and settings features currently implemented across the web and shared packages. -| # | Step | βœ… | βœ”οΈ | -|---|---|----|----| -| 1 | Extend ReminderSettings type with 3 new fields | [x] | [x] | -| 2 | Update useReminderSettings hook with defaults + setters | [x] | [x] | -| 3 | Update ReminderSettingsPanel UI (slider + 2 toggles) | [x] | [x] | +## Settings Infrastructure -## Phase 2 β€” Walk Routing Infrastructure (Steps 4-6) +| # | Step | Done | Verified | +| --- | --- | --- | --- | +| 1 | Extend `ReminderSettings` with arrival buffer, walking visibility, and bike visibility | [x] | [x] | +| 2 | Persist reminder settings in web `localStorage` and mobile `AsyncStorage` | [x] | [x] | +| 3 | Add settings UI for reminder buffer, arrival buffer, notifications, walking, and bike options | [x] | [x] | -| # | Step | βœ… | βœ”οΈ | -|---|---|----|----| -| 4 | Create WalkRoutingClient (OSRM foot profile) | [x] | [x] | -| 5 | Create /api/walk-route endpoint | [x] | [x] | -| 6 | Add getWalkRoute to api-client package | [x] | [x] | +## Routing Infrastructure -## Phase 3 β€” Departure Time Calculation (Steps 7-8) +| # | Step | Done | Verified | +| --- | --- | --- | --- | +| 4 | Add OSRM walking client and `/api/walk-route` endpoint | [x] | [x] | +| 5 | Add OSRM bike client and `/api/bike-route` endpoint | [x] | [x] | +| 6 | Add `getWalkRoute()` and `getBikeRoute()` to `@timetoleave/api-client` | [x] | [x] | +| 7 | Add web/mobile hooks for walking and bike routes | [x] | [x] | -| # | Step | βœ… | βœ”οΈ | -|---|---|----|----| -| 7 | Create useDepartureTime hook | [x] | [x] | -| 8 | Update useClock to accept departureTime override | [x] | [x] | +## Departure Calculation -## Phase 4 β€” Mode Selector & EventCard Updates (Steps 9-11) +| # | Step | Done | Verified | +| --- | --- | --- | --- | +| 8 | Calculate leave-by time from selected transport mode | [x] | [x] | +| 9 | Account for final walking time before choosing train journeys | [x] | [x] | +| 10 | Support HAFAS arrive-by journey search with fallback window | [x] | [x] | +| 11 | Update countdown logic to use computed departure time | [x] | [x] | -| # | Step | βœ… | βœ”οΈ | -|---|---|----|----| -| 9 | Create useWalkRoute hook | [x] | [x] | -| 10 | Create WalkingOption component | [x] | [x] | -| 11 | Update EventCard with mode selector + conditional rendering | [x] | [x] | +## Calendar and Event Management -## Phase 5 β€” TrainSection & JourneyList Updates (Steps 12-13) +| # | Step | Done | Verified | +| --- | --- | --- | --- | +| 12 | Import web calendars from URL and local ICS files | [x] | [x] | +| 13 | Add Google Calendar OAuth sync on web | [x] | [x] | +| 14 | Add batch destination review/editing for imported web events | [x] | [x] | +| 15 | Add mobile native calendar sync with calendar selection | [x] | [x] | +| 16 | Add event editing on web and mobile | [x] | [x] | -| # | Step | βœ… | βœ”οΈ | -|---|---|----|----| -| 12 | Update TrainSection props (arrival buffer + walk option) | [x] | [x] | -| 13 | Update JourneyList with arrival buffer filtering | [x] | [x] | +## Transit Integrations -## Phase 6 β€” Verification & Testing (Steps 14-16) +| # | Step | Done | Verified | +| --- | --- | --- | --- | +| 17 | Add HAFAS station search and nearest-station lookup | [x] | [x] | +| 18 | Add HAFAS journey parsing with real-time delay/cancellation support | [x] | [x] | +| 19 | Add optional Γ–BB GTFS train metadata enrichment | [x] | [x] | +| 20 | Add Wiener Linien nearby stops and monitor departures | [x] | [x] | -| # | Step | βœ… | βœ”οΈ | -|---|---|----|----| -| 14 | Integration verification (manual testing) | [ ] | [ ] | -| 15 | Build verification (typecheck, lint, test, build) | [x] | [x] | -| 16 | Update api-client exports | [x] | [x] | +## Verification ---- - -**Legend:** -- βœ… = Done (code written) -- βœ”οΈ = Verified (tests/builds pass) -- `[~]` = Optional or deferred (never blocks phase advancement) +| # | Step | Done | Verified | +| --- | --- | --- | --- | +| 21 | Web unit and route tests | [x] | [x] | +| 22 | Mobile store, calendar, notification, and screen tests | [x] | [x] | +| 23 | Root lint/typecheck/test scripts documented | [x] | [x] | +| 24 | Manual integration checklist updated | [x] | [x] | diff --git a/MANUAL_TESTING_CHECKLIST.md b/MANUAL_TESTING_CHECKLIST.md index 0918056..6f982bf 100644 --- a/MANUAL_TESTING_CHECKLIST.md +++ b/MANUAL_TESTING_CHECKLIST.md @@ -1,216 +1,145 @@ -# TimeToLeave - Manual Integration Testing Checklist +# TimeToLeave - Manual Testing Checklist -## Overview -This checklist guides you through manual testing of the TimeToLeave application to ensure all features work correctly in the browser. +Use this checklist for browser, mobile, and integration testing before release. ## Prerequisites -- [ ] Application is running locally or deployed -- [ ] All required environment variables are set -- [ ] Network connection is available for external API calls ---- +- [ ] `npm install` has been run. +- [ ] Required environment variables are configured. +- [ ] Web app is running locally or deployed. +- [ ] Mobile app has a reachable `EXPO_PUBLIC_API_BASE_URL` when tested on a device. +- [ ] Network access is available for HAFAS, Nominatim, OSRM, Wiener Linien, and calendar providers. -## 1. Settings Infrastructure Testing +## Web Dashboard -### Arrival Buffer Settings -- [ ] Navigate to Settings panel -- [ ] Set arrival buffer to 10 minutes -- [ ] Verify buffer value is displayed correctly -- [ ] Test different buffer values (0, 5, 15, 30 minutes) -- [ ] Verify buffer value persists after page refresh +- [ ] Open `/`. +- [ ] Verify the departure desk loads without console errors. +- [ ] Add or import at least two future events. +- [ ] Verify the dashboard shows the next upcoming event. +- [ ] Verify edit and remove actions work from the event card. +- [ ] Verify event data persists after browser refresh. -### Walking Option Toggle -- [ ] Enable "Show walking option" toggle -- [ ] Verify toggle state is saved -- [ ] Disable "Show walking option" toggle -- [ ] Verify toggle state persists after page refresh +## Web Calendar Import -### Bike Option Toggle -- [ ] Enable "Show bike option" toggle -- [ ] Verify toggle state is saved -- [ ] Disable "Show bike option" toggle -- [ ] Verify toggle state persists after page refresh +- [ ] Open `/calendar`. +- [ ] Import a valid allowed ICS URL. +- [ ] Upload a local `.ics` file. +- [ ] Verify imported events with locations merge into the local event store. +- [ ] Verify duplicate imports do not create unusable duplicate records. +- [ ] Use batch destination editing and confirm edited destinations are retained. ---- +## Google Calendar Web Sync -## 2. Walk Routing Testing +- [ ] Confirm `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI`, and `DEPLOYMENT_URL` are configured. +- [ ] Open the Google tab on `/calendar`. +- [ ] Connect Google Calendar through OAuth. +- [ ] Verify sync returns upcoming events with locations. +- [ ] Disconnect Google Calendar. +- [ ] Verify status returns to disconnected. -### Walk Route API -- [ ] Open Developer Tools (F12) β†’ Network tab -- [ ] Trigger a walk route calculation (e.g., by loading an event with walk mode) -- [ ] Verify `/api/walk-route` request appears in network log -- [ ] Check request contains correct query parameters (fromLat, fromLng, toLat, toLng) -- [ ] Verify response contains distance, duration, and steps array -- [ ] Test with different coordinate pairs +## Settings and Reminders -### Walk Route Display -- [ ] Enable walking option in settings -- [ ] Load an event that should show walk route -- [ ] Verify walk duration appears under train section -- [ ] Verify walk distance is displayed -- [ ] Verify step-by-step instructions are shown -- [ ] Test with events at different locations +- [ ] Enable browser notifications when prompted. +- [ ] Change reminder buffer and arrival buffer. +- [ ] Toggle walking option off and on. +- [ ] Toggle bike option off and on. +- [ ] Refresh the page and verify settings persist. +- [ ] Verify disabling bike hides or disables bike mode. +- [ ] Verify disabling walking removes the final-walk adjustment from train mode. ---- +## Train Mode -## 3. Departure Time Calculation Testing +- [ ] Use an event with a real destination and origin station. +- [ ] Verify destination geocoding completes. +- [ ] Verify destination station lookup completes. +- [ ] Verify `/api/hafas` is called with `TripSearch`. +- [ ] Verify journey rows show departure, arrival, platform, train labels, delay, changes, and cancellations when present. +- [ ] Verify leave-by time is based on a journey that arrives before the event minus arrival buffer and final walk. +- [ ] Increase arrival buffer and verify leave-by can move earlier. -### Countdown Badge -- [ ] Set arrival buffer to 10 minutes -- [ ] Verify countdown badge shows earlier departure time than event time -- [ ] Test with different event times (now, in 1 hour, in 3 hours) -- [ ] Verify countdown updates in real-time +## Bike and Walking Routes -### Departure Time Override -- [ ] Switch between transport modes (train, bike, walk) -- [ ] Verify countdown updates to reflect selected mode -- [ ] Test mode switching multiple times -- [ ] Verify departure time calculation is consistent +- [ ] Switch to bike mode. +- [ ] Verify `/api/bike-route` is called with four coordinate parameters. +- [ ] Verify bike duration, distance, and steps are displayed. +- [ ] Switch back to train mode with walking enabled. +- [ ] Verify `/api/walk-route` is called for station-to-destination walking. +- [ ] Verify walking duration and distance appear in the train section. +- [ ] Test route error handling with invalid or very distant coordinates. ---- +## Wiener Linien -## 4. Mode Selector Testing +- [ ] Use a destination near Vienna public transport. +- [ ] Verify `/api/wienerlinien/stops` returns nearby stops. +- [ ] Verify `/api/wienerlinien/monitor` returns live departures for selected stops. +- [ ] Verify loading, empty, and error states are readable. -### Transport Mode Selection -- [ ] Verify "Train" mode is selected by default -- [ ] Click "Bike" mode button -- [ ] Verify "Bike" mode is now active -- [ ] Click "Walk" mode button -- [ ] Verify "Walk" mode is now active -- [ ] Test switching between all modes multiple times +## API Guards -### Conditional Rendering -- [ ] With walking option disabled: verify walk section is hidden -- [ ] With walking option enabled: verify walk section appears -- [ ] With bike option disabled: verify bike section is hidden -- [ ] With bike option enabled: verify bike section appears -- [ ] Test all combinations of toggle states +- [ ] Verify remote calendar URLs from unsupported hosts are rejected. +- [ ] Verify private or localhost calendar URLs are rejected. +- [ ] Verify overly large HAFAS POST bodies are rejected. +- [ ] Verify invalid coordinates return client errors. +- [ ] Verify CORS allows only configured origins. +- [ ] Verify rate limiting returns `429` after the configured threshold. ---- +## Mobile Event Flow -## 5. JourneyList Filtering Testing +- [ ] Start the Expo app. +- [ ] Add a manual event. +- [ ] Edit the event. +- [ ] Delete the event. +- [ ] Restart the app and verify stored events persist. +- [ ] Open event detail and verify train, bike, walking, and nearby-stop sections load when data is available. -### Arrival Buffer Filtering -- [ ] Set arrival buffer to 5 minutes -- [ ] Load multiple journeys with different arrival times -- [ ] Verify journeys arriving too late are filtered out -- [ ] Increase arrival buffer to 15 minutes -- [ ] Verify previously filtered journeys now appear -- [ ] Test filtering with real-world journey data +## Mobile Calendar Import ---- +- [ ] Import an ICS URL. +- [ ] Grant calendar permission. +- [ ] Verify native calendars are listed. +- [ ] Select and deselect individual calendars. +- [ ] Use select all and deselect all. +- [ ] Sync native calendars for the next 30 days. +- [ ] Verify events without locations are excluded. +- [ ] Verify CalDAV/DAVx, Apple, Google, Exchange, subscribed, local, and other source labels render correctly when available on the device. -## 6. Cross-Feature Integration Testing +## Mobile Settings and Notifications -### Complete Workflow -- [ ] Open settings and set arrival buffer to 10 minutes -- [ ] Enable walking option -- [ ] Enable bike option -- [ ] Load an event with multiple journey options -- [ ] Verify countdown badge shows earlier departure time -- [ ] Switch to bike mode and verify countdown updates -- [ ] Verify walk duration appears under train section -- [ ] Disable bike option and verify bike section disappears -- [ ] Re-enable bike option and verify bike section reappears -- [ ] Test complete workflow with different events +- [ ] Search for an origin station. +- [ ] Use current location to find nearest origin station. +- [ ] Change reminder buffer and arrival buffer. +- [ ] Toggle walking and bike options. +- [ ] Toggle notifications. +- [ ] Verify notification settings persist after app restart. +- [ ] Verify scheduled notifications are recreated when settings change. +- [ ] Toggle dark/light theme and verify it persists. ---- +## Offline and Failure States -## 7. Edge Cases Testing +- [ ] Disable network and open web event detail data. +- [ ] Verify geocoding, HAFAS, route, and Wiener Linien errors are visible and non-blocking. +- [ ] Re-enable network and verify retry/refresh paths work. +- [ ] Test mobile with the backend URL unavailable and verify errors are understandable. -### Empty States -- [ ] Test with no walk route available (remote location) -- [ ] Verify appropriate error message is displayed -- [ ] Test with missing coordinates -- [ ] Verify graceful handling of missing data +## Accessibility and Layout -### Network Errors -- [ ] Disable network connection (offline mode in DevTools) -- [ ] Attempt to load walk route -- [ ] Verify error state is displayed -- [ ] Re-enable network and verify retry works - -### Invalid Data -- [ ] Test with invalid coordinate values -- [ ] Test with zero or negative buffer times -- [ ] Verify application handles invalid data gracefully - ---- - -## 8. Accessibility Testing - -### Keyboard Navigation -- [ ] Tab through all settings controls -- [ ] Verify all buttons and toggles are keyboard accessible -- [ ] Test mode selector with keyboard only - -### Screen Reader Compatibility -- [ ] Use Chrome's accessibility inspector or a screen reader -- [ ] Verify all settings have proper labels -- [ ] Verify all interactive elements are announced correctly - -### High Contrast Mode -- [ ] Enable high contrast mode in OS settings -- [ ] Verify all UI elements remain visible and readable - ---- - -## 9. Performance Testing - -### Loading Times -- [ ] Measure time to load walk route for nearby location (< 5km) -- [ ] Measure time to load walk route for farther location (10-20km) -- [ ] Verify loading spinner appears during API calls -- [ ] Verify loading spinner disappears when complete - -### Memory Usage -- [ ] Open Developer Tools β†’ Memory tab -- [ ] Perform multiple walk route calculations -- [ ] Verify no memory leaks (memory usage should stabilize) - ---- - -## 10. Responsive Design Testing - -### Mobile -- [ ] Test on mobile device (iPhone/Android) -- [ ] Verify settings panel is usable on small screens - -### Tablet -- [ ] Test on tablet device -- [ ] Verify all controls are properly sized - -### Desktop -- [ ] Test on various desktop screen sizes -- [ ] Verify layout does not break - ---- - -## Reporting Issues - -When you encounter an issue during testing: - -1. Note the exact steps to reproduce -2. Record browser/device information -3. Capture any error messages or console logs -4. Take screenshots if UI is affected -5. Test with latest code after reporting - ---- +- [ ] Navigate web controls with keyboard only. +- [ ] Verify modal focus and close behavior. +- [ ] Verify buttons and interactive controls have accessible labels or readable text. +- [ ] Test narrow mobile browser width, tablet width, and desktop width. +- [ ] Verify mobile screens do not clip primary controls. ## Sign-Off -- [ ] All required tests passed successfully -- [ ] No critical bugs found -- [ ] Application ready for production deployment +- [ ] Web smoke test passed. +- [ ] Mobile smoke test passed. +- [ ] Calendar import tested. +- [ ] Live transit integration tested. +- [ ] Notifications tested. +- [ ] No critical bugs remain. -**Tested by:** ________________________ -**Date:** ________________________ -**Browser/Device:** ________________________ -**Build Version:** ________________________ +Tested by: ---- +Date: -## Additional Notes - -_Add any observations, workarounds, or special test conditions here._ +Build/version: diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md index b8fd143..3f390db 100644 --- a/PRIVACY_POLICY.md +++ b/PRIVACY_POLICY.md @@ -1,27 +1,45 @@ # Privacy Policy -## Information We Collect +TimeToLeave is designed to keep user data local where possible. The app does not include third-party analytics or advertising trackers. -We do not collect any personal information or data from users. All data is stored locally on your device. +## Data Stored Locally -## Data Usage +- Web events and reminder settings are stored in browser `localStorage`. +- Mobile events, origin station, notification settings, theme, and selected native calendars are stored in `AsyncStorage`. +- Mobile notifications are scheduled locally through Expo notifications. -- **Location Data**: We use your device's location to find nearby stations and calculate travel times. This data is only used for the app's functionality and is not stored or transmitted. -- **Calendar Data**: If you choose to import calendar events, we only read the events from your calendar and do not store or transmit them. -- **Notifications**: We use local notifications to remind you about events, which are stored locally on your device. +## Data Sent to External Services -## Data Storage +Some features require network calls to calculate routes or import calendars: -All data is stored locally on your device and never leaves your device. We do not use any third-party analytics or tracking services. +| Data | Sent to | Purpose | +| --- | --- | --- | +| Destination text or address | Nominatim | Convert a place into coordinates. | +| Coordinates | OSRM | Calculate bike and walking routes. | +| Station IDs, dates, and times | Γ–BB HAFAS | Search stations and live public-transport journeys. | +| Coordinates or stop IDs | Wiener Linien | Find nearby stops and live departures. | +| Calendar URL | TimeToLeave backend, then the calendar host | Fetch and parse remote ICS feeds. | +| Google Calendar authorization code and tokens | Google and the TimeToLeave backend | Connect and sync Google Calendar on web. | +| Device calendar event fields | Local mobile app process | Import native calendar events with locations. | -## Third-Party Services +Remote ICS imports are restricted by server-side URL validation. Private and reserved hosts are blocked. -We do not use any third-party services that might collect or process your data. All processing happens locally on your device. +## Google Calendar -## Changes to This Privacy Policy +Google Calendar sync is optional. When connected on the web app, OAuth tokens are stored in HTTP-only cookies and used only to fetch calendar events. Disconnecting Google Calendar deletes the token cookie. -We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. +## Location -## Contact Us +Location access is optional and used to find nearby stations or calculate routes. Coordinates may be sent to route, geocoding, or transit APIs only when the corresponding feature is used. -If you have any questions about this Privacy Policy, please contact us at [contact email]. +## Calendar Data + +Only events with locations are useful to TimeToLeave. Imported events are normalized to title, destination, event time, source, and ID. The app stores those normalized events locally. + +## Data Retention + +Local data remains until the user clears app/browser storage, deletes events, disconnects Google Calendar, or uninstalls the app. Server-side proxy routes are intended for request handling and do not provide application-level persistent event storage. + +## Changes + +This policy may be updated as the app changes. Updates are made in this repository. diff --git a/README.md b/README.md index f60a4e6..0558940 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,128 @@ -# ⏱️ TimeToLeave +# TimeToLeave + +TimeToLeave is a departure planner for calendar-driven travel. It imports events with locations, resolves the closest public-transport station, checks live Γ–BB HAFAS and Wiener Linien data, and shows when to leave by train or bike. ![TimeToLeave Logo](apps/web/public/timetoleave_logo.png) -> **TimeToLeave** is a smart departure planner that tells you exactly when to leave home to catch your public transport for upcoming appointments. It syncs with your personal calendar (`.ics` files or Google Calendar), checks real-time train/bus departures (HAFAS & WienerLinien), and provides a live "Leave Status" based on real-time delays. +![Platform](https://img.shields.io/badge/platform-Web_%26_Mobile-blue) ![Next.js](https://img.shields.io/badge/Next.js-16.2-green) ![React Native](https://img.shields.io/badge/React%20Native-0.81-blue) ![Expo](https://img.shields.io/badge/Expo-54-black) ![TypeScript](https://img.shields.io/badge/TypeScript-5-blue) -![Platform](https://img.shields.io/badge/platform-Web_%26_Mobile-blue) ![Next.js](https://img.shields.io/badge/Next.js-16.2-green) ![React Native](https://img.shields.io/badge/React%20Native-0.81-blue) ![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue) +## What It Does -## πŸš€ How It Works +1. Imports events from ICS URLs, ICS files, Google Calendar on web, or native device calendars on mobile. +2. Stores events locally in browser `localStorage` or mobile `AsyncStorage`. +3. Uses browser or device geolocation, a saved station, or the default MΓΆdling origin. +4. Geocodes event destinations and resolves nearby stations through HAFAS `LocMatch`. +5. Searches Γ–BB HAFAS journeys, enriches train data with the Γ–BB GTFS fallback when available, and shows real-time delays and cancellations. +6. Calculates bike and final walking routes through OSRM. +7. Shows a live leave-by countdown and optional browser/mobile notifications. -1. **Sync Your Calendar:** Import your `.ics` file, provide a calendar URL, or connect your Google Calendar via OAuth 2.0. The app extracts your upcoming events and destinations. -2. **Set Your Origin:** Define your home station or let the app use your current geolocation. The app can also find the nearest station to your location via HAFAS LocMatch. -3. **Journey Calculation:** The app queries the HAFAS protocol and WienerLinien APIs to find the best public transport connections to your event destination. -4. **Real-Time Monitoring:** It monitors your train's real-time departure time, accounts for delays, and adds your local travel time (e.g., biking or walking to the station) to calculate a dynamic countdown. -5. **Leave Status:** You get a clear status: `Leave now`, `On time`, `Delayed +X min`, or `Departure missed`. +## Repository Layout -## 🧱 Project Structure +| Path | Purpose | +| --- | --- | +| `apps/web/` | Next.js 16 App Router web UI plus backend proxy routes for HAFAS, geocoding, routing, calendar parsing, Google Calendar, and Wiener Linien. | +| `apps/mobile/` | Expo 54 / React Native 0.81 mobile app with native calendar, location, notification, and local storage integrations. | +| `packages/core/` | Shared types, defaults, HAFAS time parsing, journey parsing/scoring, countdown, formatting, and status utilities. | +| `packages/api-client/` | Shared client for calling the web app's `/api/*` backend routes from web hooks and the mobile app. | +| `docs/` | Architecture, development, API, user, and codebase reference documentation. | -This project uses a monorepo setup (npm workspaces) to manage multiple, interconnected parts: +## Prerequisites -| Directory | Description | -| :--- | :--- | -| `apps/web/` | The main web dashboard built with **Next.js 16**, React 19, and Tailwind CSS 4. | -| `apps/mobile/` | The on-the-go mobile client built with **React Native 0.81** and **Expo 54**. | -| `packages/core/` | Shared domain logic, types (`Event`, `Journey`, `Station`), countdown utilities, and status calculators. | -| `packages/api-client/` | A lightweight client that handles API proxies for HAFAS requests, calendar parsing, geocoding, bike routing, and Google Calendar sync. | +- Node.js 20 or newer +- npm 9 or newer +- For mobile native builds: Expo/EAS prerequisites plus Android Studio or Xcode as needed -## πŸ›  Development & Running the Application +## Setup -### Prerequisites - -* Node.js (version 20.x or higher) -* npm (version 9.x or higher) - -### Installation - -1. **Clone the repository:** - ```bash - git clone - cd TimeToLeave - ``` - -2. **Install dependencies:** - ```bash - npm install - ``` - -3. **Environment variables:** - * For `apps/web` and `apps/mobile`, copy `.env.example` to `.env` in each app directory and update the backend API URL and any required keys. - * Google Calendar integration requires `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and `GOOGLE_REDIRECT_URI` environment variables. - -### Available Scripts - -| Script | Command | Description | -| :--- | :--- | :--- | -| `dev` | `npm run dev` | Starts the Next.js development server for the Web dashboard. | -| `dev:mobile` | `npm run dev:mobile` | Starts the Expo development server for the Mobile client. | -| `build` | `npm run build` | Builds the production bundle for the Web application. | -| `test` | `npm run test` | Runs Vitest for the Web app and Jest for the Mobile app. | -| `lint` | `npm run lint` | Runs ESLint across both web and mobile clients. | -| `typecheck` | `npm run typecheck` | Runs TypeScript type checking across all workspaces. | - -## πŸ“ Key Features & Tech Stack - -### Web Application (`apps/web`) -* **Framework:** Next.js 16.2.6 (App Router) -* **UI:** React 19.1.0 with Tailwind CSS 4 -* **State Management:** React Context (via `EventsProvider` and `ReminderSettingsProvider`) -* **Routing:** Next.js built-in routing for `/` (event list) and `/calendar` views. -* **Calendar Integration:** - * Import `.ics` files or provide calendar URLs - * **Google Calendar sync** via full OAuth 2.0 flow (token exchange, refresh, and status checks) - * Batch edit panel for managing event destinations - * Edit support in the AddEventModal for modifying existing events -* **Dark/Light Theme:** Built-in theme toggle - -### Mobile Application (`apps/mobile`) -* **Framework:** React Native 0.81 via Expo 54 -* **Navigation:** React Navigation 7 (Native Stack) -* **Theme:** Dark theme by default -* **Device APIs:** - * `expo-location`: For geocoding your current position. - * `expo-calendar`: For native calendar event integration. - * `expo-notifications`: For native push notifications when it's time to leave. - * `@react-native-async-storage/async-storage`: For persisting settings and local state. -* **Station Selection:** Async station search with error handling and nearest-station detection via HAFAS LocMatch. - -### Core Logic (`packages/core`) -* **Countdown Utilities:** Calculates time-deltas and assigns color codes (Red/Orange/Yellow/Green/Blue) based on urgency. -* **HAFAS Time Parsing:** Highly accurate timezone-aware parsing for HAFAS timestamps, specifically handling `Europe/Vienna` (CET/CEST) and DST transitions. -* **WienerLinien Support:** Native types and handling for Vienna public transport departures. -* **Leave Status:** Derives human-readable statuses (`Leave now`, `Delayed +10 min`, etc.) by comparing the best non-cancelled journey's real departure time against the current time. - -## πŸ“„ API Client Usage - -The `@timetoleave/api-client` package provides a clean interface to interact with your backend proxy, which handles the heavy lifting of HAFAS protocol communication, calendar parsing, and Google Calendar sync. - -```typescript -import { ApiClient } from "@timetoleave/api-client"; - -// Initialize with your backend URL -const api = new ApiClient("http://localhost:3000"); - -// 1. Sync your calendar (via .ics URL) -const events = await api.fetchCalendar("https://example.com/calendar.ics", 7); - -// 2. Sync Google Calendar (after OAuth flow) -const googleEvents = await api.fetchGoogleCalendarEvents(); - -// 3. Search for a station via the HAFAS LocMatch endpoint -const stationResult = await api.hafasRequest({ - svcReqL: [ - { - meth: "LocMatch", - req: { searchTxt: "Wien Mitte", maxMatches: 5 }, - }, - ], -}); -const stations = stationResult?.svcReqL?.[0]?.res?.locL ?? []; - -// 4. Find the nearest station to your current location -const nearestStation = await api.findNearestStation(48.2082, 16.3738); - -// 5. Find journeys between stations for a specific date -const journeys = await api.searchJourneys( - stations[0].extId, // From - "dest:extId", // To - new Date() // Date -); - -// 6. Get a bike route from your current location to the station -const bikeRoute = await api.getBikeRoute( - 48.2082, 16.3738, // From lat/lng - 48.1850, 16.3780 // To lat/lng -); +```bash +npm install +cp .env.example .env ``` -## πŸ›‘οΈ Testing & Quality Assurance +The web app reads environment variables from the workspace process. For deployment, configure the same values in the hosting environment. -The project provides comprehensive scripts for maintaining code quality: +Important variables: -* **Linting:** Use `npm run lint` to catch stylistic and structural errors via ESLint 9. -* **Type Checking:** Use `npm run typecheck` to ensure strict type safety across the codebase via TypeScript 5. -* **Testing:** - * The web application uses **Vitest** (v4.1.5) with **jsdom** and **@testing-library/react**. - * The mobile application uses **Jest** (v29.7.0) with **jest-expo** and **react-test-renderer**. +| Variable | Purpose | +| --- | --- | +| `HAFAS_URL` | Γ–BB HAFAS endpoint. Defaults to `https://fahrplan.oebb.at/bin/mgate.exe`. | +| `NOMINATIM_URL` and `NOMINATIM_USER_AGENT` | Geocoding endpoint and required user agent. | +| `OSRM_URL` | Routing endpoint used for bike and foot profiles. | +| `WIENER_LINIEN_API_URL` | Wiener Linien live data base URL. | +| `OEBB_GTFS_URL` | Optional Γ–BB GTFS ZIP used to enrich HAFAS train metadata. | +| `CORS_ALLOWED_ORIGINS` | Comma-separated origins allowed to call `/api/*`. | +| `DEPLOYMENT_URL` | Public base URL used by Google OAuth redirects. | +| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI` | Required for Google Calendar sync on web. | +| `EXPO_PUBLIC_API_BASE_URL` | Mobile backend URL. Set this for device builds so the app can reach the deployed web backend. | -## πŸ“‚ File Structure +## Development -```text -β”œβ”€β”€ apps/ -β”‚ β”œβ”€β”€ mobile/ # Mobile application using React Native and Expo -β”‚ └── web/ # Web application using Next.js and Tailwind CSS -β”œβ”€β”€ packages/ -β”‚ β”œβ”€β”€ api-client/ # API client for HAFAS, Calendar, Google Calendar, and Routing proxies -β”‚ └── core/ # Shared domain types, countdowns, and HAFAS time utilities -β”œβ”€β”€ node_modules/ # Third-party dependencies -└── README.md # The file you're reading now -``` +| Command | Description | +| --- | --- | +| `npm run dev` | Start the Next.js web app on `http://localhost:3000`. | +| `npm run dev:mobile` | Start the Expo development server. | +| `npm run build` | Build the web app. | +| `npm run start` | Start the built web app. | +| `npm run test` | Run web Vitest and mobile Jest suites. | +| `npm run lint` | Run ESLint across web, mobile, core, and api-client workspaces. | +| `npm run typecheck` | Run TypeScript checks across all workspaces. | ---- -*Built for developers who bike to the train and hate missing their connections.* +## Web App + +Current user-facing routes: + +| Route | Description | +| --- | --- | +| `/` | Departure desk. Shows the next upcoming event, leave-by status, transport mode selector, train journeys, bike route, final walk, and nearby Wiener Linien departures. | +| `/calendar` | Calendar import and management view with URL, file, and Google Calendar tabs plus batch destination editing. | + +The add/edit event UI is a modal component, not a standalone page route. + +## Backend Proxy Routes + +All backend routes live under `apps/web/src/app/api/` and are protected by strict CORS plus per-IP rate limiting in `apps/web/src/proxy.ts`. + +| Endpoint | Methods | Purpose | +| --- | --- | --- | +| `/api/health` | `GET` | Returns `{ ok, ts, version }`. | +| `/api/hafas` | `GET`, `POST` | Convenience journey search or validated HAFAS relay for `TripSearch` and `LocMatch`. | +| `/api/geocode` | `GET` | Forward geocoding through Nominatim. | +| `/api/bike-route` | `GET` | OSRM bicycle route between two coordinates. | +| `/api/walk-route` | `GET` | OSRM foot route between two coordinates. | +| `/api/calendar` | `GET` | Fetch and parse an allowed remote ICS URL. | +| `/api/calendar/parse` | `POST` | Parse uploaded/raw ICS text. | +| `/api/calendar/google` | `GET` | Fetch Google Calendar events using OAuth cookies. | +| `/api/auth/google` | `GET` | Start Google OAuth. | +| `/api/auth/google/callback` | `GET` | Complete Google OAuth and store token cookie. | +| `/api/auth/google/status` | `GET` | Report Google configuration and connection state. | +| `/api/auth/google/disconnect` | `POST` | Delete the Google token cookie. | +| `/api/wienerlinien/stops` | `GET` | Find nearby Wiener Linien stops. | +| `/api/wienerlinien/monitor` | `GET` | Fetch and flatten live stop departures. | + +## Mobile App + +The mobile app includes event list, add/edit event, event detail, calendar import, and settings screens. It supports: + +- Native calendar sync for the next 30 days. +- Calendar-source selection, including CalDAV/DAVx, Apple, Google, Exchange, subscribed, and local calendars when exposed by the device. +- Saved origin station with current-location lookup. +- Train, bike, walking, and Wiener Linien live sections on event detail. +- Local notifications scheduled from stored event/settings data. +- Dark/light theme toggle. + +## Documentation + +Start with [docs/README.md](docs/README.md), then use: + +- [Architecture](docs/ARCHITECTURE.md) +- [Development Guide](docs/DEVELOPMENT.md) +- [Core & API Client Reference](docs/API_REFERENCE.md) +- [User Guide](docs/USER_GUIDE.md) +- [Codebase Function Guide](docs/CODEBASE_FUNCTION_GUIDE.md) + +## Important Implementation Notes + +- HAFAS date/time values are Vienna-local strings. Use `parseHafasTime()` and `hafasDateTime()` from `@timetoleave/core`; avoid ad hoc `Date` parsing for HAFAS payloads. +- Remote calendar URLs are restricted to known calendar providers and private/reserved hosts are blocked. +- Mobile devices must use a reachable `EXPO_PUBLIC_API_BASE_URL`; same-origin empty base URLs only work in the web app. +- This repo uses Next.js 16. Before changing Next.js routing, middleware/proxy, or framework conventions, read the relevant guide in `node_modules/next/dist/docs/`. diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 84a6cdc..4ba790b 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -1,149 +1,157 @@ # Core & API Client Reference -This document provides a detailed reference for the internal packages: `@timetoleave/core` and `@timetoleave/api-client`. +This reference covers the two shared packages: `@timetoleave/core` and `@timetoleave/api-client`. -## πŸ“¦ `@timetoleave/core` +## `@timetoleave/core` -The core package contains all domain types, status calculation logic, HAFAS time parsing, and formatting utilities. It has zero runtime dependencies outside of the standard library. +The core package contains shared types, defaults, pure utilities, HAFAS parsing, and journey scoring. It has no first-party dependency on either app. -### Types (`types.ts`) +### Main Types -| Interface | Description | -| :--- | :--- | -| `Event` | Represents a calendar event with an assigned destination and event time. | -| `CalendarEvent` | Raw calendar event data (typically `string` dates before conversion to `Event`). | -| `Station` | A transport station with a name, HAFAS `extId`, and optional coordinates. | -| `Journey` | A public transport connection including scheduled/real departure/arrival, delay, platform, and cancellation status. | -| `BikeRoute` / `WalkRoute` | Routing data including total distance, duration, and step-by-step instructions. | -| `CountdownInfo` | Output of the countdown utility containing a label, color code, and urgency flag. | -| `ReminderSettings` | User preferences for buffers, walking/bike options, and reminder toggles. | -| `WienerLinienDeparture` | Specific types for WienerLinien (Vienna public transport) monitor responses. | +| Type | Description | +| --- | --- | +| `Event` | Locally stored app event with `eventTime: Date`. | +| `CalendarEvent` | API-safe calendar event with `eventTime: string`. | +| `Station` | HAFAS station identity with optional coordinates. | +| `Journey` | Parsed transit journey with scheduled/real departure and arrival, delay, platform, changes, train labels, and cancellation state. | +| `BikeRoute`, `BikeStep` | OSRM bicycle route summary and step data. | +| `WalkRoute`, `WalkStep` | OSRM foot route summary and step data. | +| `CountdownInfo` | Countdown label, color key, and urgency flag. | +| `ReminderSettings` | Buffer, notification, walking, and bike visibility settings. | +| `GeocodeResult` | Nominatim-style coordinate result. | +| `NearbyStop`, `WienerLinien*` | Vienna stop and departure response shapes. | +| `CalendarAccountType`, `CalendarSourceInfo`, `SelectableCalendar` | Mobile native-calendar selection metadata. | -### HAFAS Time Utilities (`hafas-time.ts`) +### Defaults -HAFAS timestamps are strictly tied to the `Europe/Vienna` timezone (CET/CEST). These utilities handle the bi-directional conversion between UTC JavaScript `Date` objects and HAFAS strings, correctly handling Daylight Saving Time (DST) transitions. +`packages/core/src/defaults.ts` exports the shared fallback origin: + +- `DEFAULT_ORIGIN_ADDRESS`: `Goethegasse 36, 2340 Moedling` +- `DEFAULT_ORIGIN_LAT`: `48.0806926` +- `DEFAULT_ORIGIN_LNG`: `16.2908052` +- `DEFAULT_ORIGIN_STATION_NAME`: `MΓΆdling Bahnhof` +- `DEFAULT_ORIGIN_STATION_EXT_ID`: `1231701` +- `DEFAULT_ORIGIN_STATION`: assembled `Station` + +### HAFAS Time Utilities + +HAFAS date/time strings are Vienna-local values. Use these helpers for every HAFAS request/response conversion. ```typescript -import { parseHafasTime, hafasDateTime } from '@timetoleave/core'; +import { hafasDateTime, parseHafasTime } from "@timetoleave/core"; -// Parse HAFAS date ("YYYYMMDD") and time ("HHMMSS") into a UTC Date object -const dateObj = parseHafasTime("20231027", "143000"); - -// Convert a UTC Date object back to HAFAS date and time strings -const { date, time } = hafasDateTime(dateObj); -// Returns: { date: "20231027", time: "143000" } +const parsed = parseHafasTime("20260518", "143000"); +const outbound = hafasDateTime(new Date()); ``` -> **Note:** `getTimezoneOffsetMinutes` is designed exclusively for full-hour offsets like `Europe/Vienna`. It will produce incorrect results for fractional timezones (e.g., India +05:30). +| Function | Description | +| --- | --- | +| `parseHafasTime(dateStr, timeStr)` | Converts HAFAS `YYYYMMDD` and `HHMMSS` strings into a UTC `Date`, including CET/CEST transition handling. | +| `hafasDateTime(date)` | Converts a JavaScript `Date` into HAFAS date/time strings in `Europe/Vienna`. | +| `getTimezoneOffsetMinutes(instant, tz)` | Internal helper intended for whole-hour zones such as `Europe/Vienna`. | +| `getDateTimeParts(instant, tz)` | Extracts timezone-local date parts through `Intl.DateTimeFormat`. | -### Countdown & Status Utilities +### Journey Parsing and Scoring -#### `calculateCountdown(targetDate: Date): CountdownInfo` -Calculates the time delta between `now` and `targetDate`, returning a human-readable label, a color code, and an urgency boolean. +| Function | Description | +| --- | --- | +| `parseHafasJourneys(json, hafasDate, queryDate)` | Converts HAFAS `outConL` responses into `Journey[]`, including real-time delay, platform, trains, changes, and cancellations. | +| `rankJourneys(journeys, targetArrivalTime, finalLegDurationMs?)` | Scores journeys by arrival fit, transfer count, duration, and cancellation penalty. | -| Time Delta | Label | Color | Urgent | -| :--- | :--- | :--- | :--- | -| `<= 0 min` | `Now` | `red` | `true` | -| `<= 10 min` | `[X]min` | `orange` | `true` | -| `<= 30 min` | `[X]min` | `yellow` | `false` | -| `<= 60 min` | `[X]min` | `green` | `false` | -| `> 60 min` | `[X]h [Y]min` | `blue` | `false` | +### Countdown, Status, and Formatting -#### `getLeaveStatus(event: Event, journeys: Journey[]): string` -Derives a human-readable leave-by status by finding the earliest non-cancelled journey and comparing its real departure time (`rD`) against the current time. +| Function | Description | +| --- | --- | +| `calculateCountdown(targetDate)` | Returns a countdown label and color: red for now/past, orange within 10 minutes, yellow within 30, green within 60, blue beyond 60. | +| `getLeaveStatus(event, journeys)` | Returns `No journey data`, `All journeys cancelled`, `Departure missed`, `Delayed +N min`, `Leave now`, or `On time`. | +| `StatusUtils.checkServerStatus(url)` | Performs a timeout-bound `HEAD` request and returns boolean availability. | +| `formatTime(date)` | Austrian local `HH:mm`. | +| `formatDate(date)` | Austrian local date with weekday. | +| `formatDateTime(date)` | Austrian local date and time. | +| `formatDuration(seconds)` | Human-readable duration such as `1h 05min` or `45min`. | +| `formatDistance(meters)` | Meters below 1 km, one-decimal kilometers above. | -**Possible Returns:** -- `"No journey data"` -- `"All journeys cancelled"` -- `"Departure missed"` (if `rD` is in the past) -- `"Delayed +[X] min"` (if delay exceeds 10 minutes) -- `"Leave now"` (if departure is within 15 minutes) -- `"On time"` +## `@timetoleave/api-client` -### Formatting Utilities (`formatting.ts`) - -All formatting functions default to the `de-AT` locale to match the primary target region. - -- `formatTime(date: Date)` -> `"14:30"` -- `formatDate(date: Date)` -> `"Mi., 27. Oktober 2023"` -- `formatDateTime(date: Date)` -> `"27. Oktober 2023, 14:30"` -- `formatDuration(seconds: number)` -> `"1h 23min"` or `"45min"` -- `formatDistance(meters: number)` -> `"1.2km"` or `"800m"` - ---- - -## 🌐 `@timetoleave/api-client` - -The API client is a lightweight wrapper around the Web App's Next.js API routes. It handles URL construction, query parameters, and JSON serialization. - -### Initialization +`ApiClient` is a small client for the web backend proxy. In the web app, an empty base URL means same-origin. In mobile, set `EXPO_PUBLIC_API_BASE_URL` or pass a deployed backend URL. ```typescript -import { ApiClient } from '@timetoleave/api-client'; +import { ApiClient } from "@timetoleave/api-client"; -// Initialize with the base URL of the backend proxy -const api = new ApiClient('http://localhost:3000'); +const api = new ApiClient("https://timetoleave.app"); ``` -### Methods +The constructor accepts either a string or a string array. When an array is provided, the client tries the next base URL for network failures and unavailable statuses such as 408, 429, 502, 503, and 504. + +### Health -#### `getHealth()` ```typescript -getHealth(): Promise<{ status: 'ok'; uptime: number }> +getHealth(): Promise<{ status: "ok"; uptime: number }> ``` -Checks the `/api/health` endpoint to verify backend availability. -#### `geocode(name: string, countrycodes?: string)` +Calls `/api/health`. The current route returns `{ ok, ts, version }`, so callers should keep this method's legacy type in mind until it is aligned with the route payload. + +### Geocoding + ```typescript geocode(name: string, countrycodes?: string): Promise -``` -Performs forward geocoding via `/api/geocode`. - -#### `reverseGeocode(lat: number, lng: number)` -```typescript reverseGeocode(lat: number, lng: number): Promise ``` -Performs reverse geocoding via `/api/geocode/reverse`. -#### `fetchCalendar(url: string, days?: number)` +`geocode()` calls `/api/geocode` and wraps the first result in an array. `reverseGeocode()` calls `/api/geocode/reverse`; the current web app does not define that route, so it returns `null` for non-OK responses. + +### Calendar + ```typescript fetchCalendar(url: string, days?: number): Promise -``` -Fetches and parses a remote `.ics` file via `/api/calendar`. The optional `days` parameter limits the fetch to upcoming events. - -#### `parseCalendarIcs(content: string)` -```typescript parseCalendarIcs(content: string): Promise ``` -Parses raw `.ics` string content via `/api/calendar/parse`. -#### `searchStation(query: string)` +`fetchCalendar()` imports a remote allowed ICS URL through `/api/calendar`. `parseCalendarIcs()` posts raw ICS text to `/api/calendar/parse`. + +Google Calendar sync is currently implemented in the web UI and backend routes, not as a dedicated `ApiClient` method. + +### HAFAS + ```typescript +hafasRequest>(body: unknown): Promise searchStation(query: string): Promise +findStationByExtId(extId: string): Promise +findNearestStationByCoords(lat: number, lng: number): Promise +searchJourneys( + fromStationExtId: string, + toStationExtId: string, + date: Date, + options?: { arriveBy?: boolean }, +): Promise ``` -Searches for stations by name using the HAFAS `LocMatch` method. Returns up to 5 matches with valid `extId`s. -#### `searchJourneys(fromStationExtId: string, toStationExtId: string, date: Date)` -```typescript -searchJourneys(from: string, to: string, date: Date): Promise -``` -Sends a HAFAS `TripSearch` request to find public transport connections between two stations. It automatically converts the `Date` to HAFAS-compatible strings. +`hafasRequest()` posts a validated HAFAS body to `/api/hafas`. The server only allows `TripSearch` and `LocMatch`. -#### `getBikeRoute(fromLat, fromLng, toLat, toLng)` & `getWalkRoute(fromLat, fromLng, toLat, toLng)` -```typescript -getBikeRoute(...): Promise -getWalkRoute(...): Promise -``` -Retrieves routing data for the "first mile / last mile" segment (e.g., biking from home to the train station). +`searchJourneys()` builds a `TripSearch`, converts the requested date with `hafasDateTime()`, and parses the response with `parseHafasJourneys()`. When `arriveBy` is true and no journeys are returned, it retries with a two-hour backward fallback window. -#### `findNearbyStops(lat: number, lng: number, radius?: number)` -```typescript -findNearbyStops(lat: number, lng: number, radius: number = 1000): Promise -``` -Finds public transport stops within a specific radius using the WienerLinien API (`/api/wienerlinien/stops`). +### Routing -#### `hafasRequest(body: unknown)` ```typescript -hafasRequest(body: unknown): Promise +getBikeRoute(fromLat, fromLng, toLat, toLng): Promise +getWalkRoute(fromLat, fromLng, toLat, toLng): Promise ``` -A generic method to send arbitrary HAFAS protocol bodies to `/api/hafas`. Useful for advanced use-cases not covered by the wrapper methods. + +Both methods call OSRM-backed proxy routes and return route summaries plus turn-by-turn steps. + +### Wiener Linien + +```typescript +findNearbyStops(lat: number, lng: number, radius?: number): Promise +monitorStops(stopIds: string[]): Promise +``` + +`findNearbyStops()` calls `/api/wienerlinien/stops`. `monitorStops()` sends repeated `stopIds` query parameters to `/api/wienerlinien/monitor` and returns flattened departure rows. + +## Backend Contract Notes + +- `/api/health` currently returns `{ ok, ts, version }`. +- `/api/geocode` returns a single `GeocodeResult`, while `ApiClient.geocode()` wraps it in an array for existing callers. +- `/api/hafas` POST caps `TripSearch.numF` to 5 in the current implementation. +- `/api/calendar/parse` has a small body limit intended for direct uploaded text parsing. +- Remote calendar URL import is allow-list based and rejects redirects. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e9ead95..ee3b566 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,64 +1,82 @@ # TimeToLeave Architecture -This document outlines the architectural decisions, directory structure, and data flow of the TimeToLeave application. +TimeToLeave is an npm workspaces monorepo. The web app is both the browser UI and the backend proxy for external services; the mobile app calls that backend through the shared API client; shared domain logic lives in `packages/core`. -## πŸ—οΈ Monorepo Structure - -TimeToLeave uses an **npm Workspaces Monorepo** to manage its interconnected components. This allows for seamless code sharing and dependency management between the Web dashboard, the Mobile client, and the shared packages. +## Monorepo Structure ```text TimeToLeave/ β”œβ”€β”€ apps/ -β”‚ β”œβ”€β”€ web/ # Next.js 16 Web Dashboard & Backend Proxy -β”‚ └── mobile/ # React Native 0.81 / Expo 54 Mobile Client +β”‚ β”œβ”€β”€ web/ # Next.js 16 App Router UI and backend proxy +β”‚ └── mobile/ # Expo 54 / React Native 0.81 app β”œβ”€β”€ packages/ -β”‚ β”œβ”€β”€ api-client/ # Unified API Client for Backend Proxies -β”‚ └── core/ # Shared Domain Types, Logic, and Utilities -β”œβ”€β”€ docs/ # Comprehensive Documentation -└── package.json # Root Workspace Configuration +β”‚ β”œβ”€β”€ api-client/ # Shared client for /api/* backend routes +β”‚ └── core/ # Shared types, defaults, parsing, scoring, formatting +β”œβ”€β”€ docs/ # Documentation +└── package.json # Workspace scripts ``` -## 🧩 Component Overview +## Components -### 1. Web Application (`apps/web`) -- **Role:** Primary dashboard for desktop users and the **Backend Proxy** for HAFAS/Geocoding APIs. -- **Framework:** Next.js 16 (App Router) with React 19. -- **Styling:** Tailwind CSS 4. -- **State Management:** React Context (`EventsProvider`, `ReminderSettingsProvider`). -- **Backend Proxy:** The Next.js API routes (`/api/*`) act as a server-side proxy. This is crucial because the HAFAS protocol and various geocoding APIs require server-side execution to protect API keys, handle CORS, and manage protocol-specific payloads. +### Web App: `apps/web` -### 2. Mobile Application (`apps/mobile`) -- **Role:** On-the-go companion app for real-time status checks and native device integration. -- **Framework:** React Native 0.81 via Expo 54. -- **Navigation:** React Navigation 7 (Native Stack Navigator). -- **Native APIs:** - - `expo-location`: Geolocation for finding nearby stations and calculating local transit (bike/walk) to the station. - - `expo-calendar`: Native integration to read local calendar events directly on the device. - - `expo-notifications`: Push notifications to alert the user when it's time to leave. - - `@react-native-async-storage/async-storage`: Persisting user settings (buffers, toggles) and cached events locally. +- Next.js 16 App Router with React 19 and Tailwind CSS 4. +- User-facing routes are `/` and `/calendar`. +- Route handlers under `apps/web/src/app/api/**/route.ts` proxy HAFAS, Nominatim, OSRM, Google Calendar, remote ICS, and Wiener Linien calls. +- `apps/web/src/proxy.ts` applies strict CORS and per-IP rate limiting to `/api/*`. +- Client state is kept in React context and persisted to `localStorage` through `useEventsStore` and `useReminderSettings`. -### 3. Core Package (`packages/core`) -- **Role:** The single source of truth for domain logic and TypeScript types across the entire monorepo. -- **Key Modules:** - - **Types:** Defines `Event`, `Journey`, `Station`, `BikeRoute`, `CountdownInfo`, and `WienerLinien` specific types. - - **HAFAS Time Parsing:** Specialized utilities to parse Vienna-centric (CET/CEST) timestamps. Handles Daylight Saving Time (DST) transitions accurately using `Intl.DateTimeFormat`. - - **Countdown & Status Logic:** Algorithms to translate raw journey data into human-readable statuses like *"Leave now"*, *"On time"*, or *"Delayed +12 min"*. - - **Formatting:** Standardized formatting for dates, times, distances, and durations. +### Mobile App: `apps/mobile` -### 4. API Client Package (`packages/api-client`) -- **Role:** A lightweight HTTP client that wraps the Web App's API routes. -- **Usage:** Used by both the Web frontend (for server/client data sync) and the Mobile client to communicate with the backend proxy. -- **Features:** Handles URL building, query parameters, and JSON serialization for HAFAS requests, calendar fetching, and routing requests. +- Expo 54 / React Native 0.81 with React Navigation 7. +- Screens cover event list, add/edit event, event detail, settings, and calendar import. +- Uses native APIs through `expo-calendar`, `expo-location`, `expo-notifications`, and `AsyncStorage`. +- Calls the web backend through `@timetoleave/api-client`; device builds should set `EXPO_PUBLIC_API_BASE_URL`. -## πŸ”„ Data Flow +### Core Package: `packages/core` -1. **Calendar Sync:** The user provides an `.ics` URL or uploads a file. The `ApiClient` sends this to the Web App's `/api/calendar` route, which parses the events and returns standardized `CalendarEvent` objects. -2. **Station Search:** The user searches for a station. The `ApiClient` triggers a HAFAS `LocMatch` request via `/api/hafas`. -3. **Journey Calculation:** Using the station `extId` and the event time, the `ApiClient` sends a `TripSearch` request to `/api/hafas`. The backend returns real-time journey data (`Journey[]`). -4. **Local Routing:** Using `expo-location` (mobile) or browser geolocation (web), the app calculates the bike/walk route from the user's home/location to the departure station via `/api/bike-route` or `/api/walk-route`. -5. **Real-Time Status:** The `packages/core` logic continuously compares the `Journey.rD` (real departure) against the current time and local travel duration to update the Leave Status dynamically. +- Owns the shared TypeScript model: events, calendar events, stations, journeys, routes, reminder settings, geocoding, and Wiener Linien types. +- Provides Vienna-aware HAFAS time conversion through `parseHafasTime()` and `hafasDateTime()`. +- Parses HAFAS journey responses, ranks journeys, formats dates/durations/distances, and computes countdown/status labels. +- Exports default origin constants for the MΓΆdling fallback origin. -## ⚠️ Technical Constraints & Guidelines +### API Client Package: `packages/api-client` -- **Next.js Version:** The project uses **Next.js 16.2+**, which includes breaking changes compared to previous versions. Always refer to `node_modules/next/dist/docs/` when modifying Web routing or API conventions. -- **HAFAS Timezone:** HAFAS timestamps are strictly tied to `Europe/Vienna`. The `hafas-time.ts` module handles the bi-directional conversion between UTC `Date` objects and Vienna-local HAFAS strings. Never use standard `Date` methods for HAFAS times; always use `parseHafasTime()` and `hafasDateTime()`. +- Wraps the web backend routes from browser and mobile code. +- Supports base URL failover by accepting either one base URL or an array of base URLs. +- Builds HAFAS `TripSearch` and `LocMatch` requests, parses journey responses with `@timetoleave/core`, and includes an arrive-by fallback search window. + +## Data Flow + +1. Events enter through manual entry, remote ICS URL import, local ICS file parsing, Google Calendar on web, or native device calendars on mobile. +2. Events are normalized to `Event` or `CalendarEvent` objects and stored locally in the client. +3. The app resolves an origin station from saved settings, geolocation, or the shared default origin. +4. The event destination is geocoded with Nominatim. +5. HAFAS `LocMatch` resolves the nearest destination station. +6. HAFAS `TripSearch` fetches live journeys, with HAFAS time conversion handled by `packages/core`. +7. OSRM provides optional bike and final-walk routes. +8. Wiener Linien endpoints provide nearby stops and live departures around the destination. +9. Countdown and leave-by calculations combine event time, selected transport mode, journey arrival, final walk duration, bike route duration, and reminder buffers. + +## External Integrations + +| Service | Used for | Access path | +| --- | --- | --- | +| Γ–BB HAFAS | Station lookup and journey search | `/api/hafas` | +| Γ–BB GTFS ZIP | Optional train metadata enrichment | `apps/web/src/lib/oebb-gtfs.ts` | +| Nominatim | Destination geocoding | `/api/geocode` | +| OSRM | Bike and foot routes | `/api/bike-route`, `/api/walk-route` | +| Wiener Linien Darwin | Nearby stops and live monitor data | `/api/wienerlinien/*` | +| Google Calendar | Web OAuth calendar import | `/api/auth/google/*`, `/api/calendar/google` | +| Remote ICS providers | Calendar URL import | `/api/calendar` | + +## Security and Operational Constraints + +- Calendar URL imports validate provider domains, block private/reserved hosts, reject redirects, validate content type, and cap response size. +- HAFAS POST bodies are size-limited and only `TripSearch` and `LocMatch` are allowed. +- Coordinate APIs validate ranges and reject unrealistically distant route requests. +- API routes are rate-limited per client IP. +- CORS is allow-list based through `CORS_ALLOWED_ORIGINS`. +- Google OAuth tokens are stored in HTTP-only cookies. +- HAFAS time values must be treated as `Europe/Vienna` local strings. +- This project uses Next.js 16. Before changing framework behavior, routing, route handlers, or proxy/middleware code, read the relevant guide in `node_modules/next/dist/docs/`. diff --git a/docs/CODEBASE_FUNCTION_GUIDE.md b/docs/CODEBASE_FUNCTION_GUIDE.md index 9fb1f71..35c4e3b 100644 --- a/docs/CODEBASE_FUNCTION_GUIDE.md +++ b/docs/CODEBASE_FUNCTION_GUIDE.md @@ -87,6 +87,20 @@ HAFAS timestamps are Vienna-local strings, not UTC timestamps. Use these helpers | --- | --- | | `parseHafasJourneys(json, hafasDate, queryDate)` | Shared HAFAS trip parser that converts raw `outConL` connections into `Journey[]`. It parses scheduled and realtime departure/arrival strings with `parseHafasTime`, computes delay, platform, train labels, change count, and cancellation state. | +### `packages/core/src/journey-scoring.ts` + +| Function | What it does | +| --- | --- | +| `rankJourneys(journeys, targetArrivalTime, finalLegDurationMs?)` | Scores journeys by arrival fit, transfer count, duration, and cancellation penalty. It favors arrivals close to the target, direct connections, and shorter journeys, while strongly penalizing late or cancelled journeys. | + +### `packages/core/src/defaults.ts` + +| Constant | What it does | +| --- | --- | +| `DEFAULT_ORIGIN_ADDRESS`, `DEFAULT_ORIGIN_LAT`, `DEFAULT_ORIGIN_LNG` | Shared fallback origin address and coordinates for Goethegasse 36, 2340 Moedling. | +| `DEFAULT_ORIGIN_STATION_NAME`, `DEFAULT_ORIGIN_STATION_EXT_ID` | Fallback station name and HAFAS station ID for MΓΆdling Bahnhof. | +| `DEFAULT_ORIGIN_STATION` | Pre-assembled `Station` object used when geolocation or saved origin settings are unavailable. | + ### `packages/core/src/index.ts` Barrel file that re-exports the core types, countdown utilities, formatting utilities, status utilities, HAFAS time helpers, and the shared HAFAS parser. @@ -112,8 +126,10 @@ This client talks to the web app's backend proxy routes. `baseUrl` defaults to a | `ApiClient.reverseGeocode(lat, lng)` | Calls `/api/geocode/reverse`. Note: the current web app does not define this route, so callers should tolerate `null` or failures. | | `ApiClient.findNearbyStops(lat, lng, radius?)` | Calls `/api/wienerlinien/stops` and returns nearby stops. | | `ApiClient.searchStation(query)` | Uses HAFAS `LocMatch` through `/api/hafas` to search station names. | +| `ApiClient.findStationByExtId(extId)` | Uses HAFAS `LocMatch` to resolve one station by external station ID. | | `ApiClient.findNearestStationByCoords(lat, lng)` | Uses HAFAS coordinate `LocMatch` to find the closest station to GPS coordinates. | | `ApiClient.monitorStops(stopIds)` | Calls `/api/wienerlinien/monitor` and returns flattened departure rows. | +| `ApiClient.fetchApi(path, init?, params?)` | Private helper that tries configured base URLs and falls through on network errors or unavailable status codes. | ### `packages/api-client/src/index.ts` @@ -133,6 +149,7 @@ Defines environment-backed service URLs and defaults: | `WIENER_LINIEN_API_URL` | Wiener Linien API base. | | `DEFAULT_DAYS` | Default calendar import horizon. | | `DEFAULT_STATION`, `DEFAULT_STATION_NAME`, `DEFAULT_STATION_EXT_ID` | Web fallback origin. The default origin is Goethegasse 36, 2340 Moedling, using MΓΆdling Bahnhof as the nearest transit station fallback. | +| `OEBB_GTFS_URL` | Γ–BB GTFS ZIP source used by the HAFAS route to enrich train metadata when possible. | | `APP_VERSION` | Health endpoint version string. | ### `apps/web/src/lib/api-service.ts` diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 7d738c8..987cc7c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,101 +1,113 @@ # Development Guide -This guide covers setting up the development environment, running the applications, and maintaining code quality. +## Prerequisites -## πŸ›  Prerequisites +- Node.js 20 or newer +- npm 9 or newer +- Android Studio or Xcode for native mobile builds +- Expo/EAS tooling when building or submitting mobile apps -- **Node.js:** Version 20.x or higher. -- **npm:** Version 9.x or higher. +## Install -## πŸ“₯ Installation - -1. **Clone the repository:** - ```bash - git clone - cd TimeToLeave - ``` - -2. **Install dependencies:** - ```bash - npm install - ``` - *This installs dependencies for the root workspace as well as all apps and packages.* - -3. **Environment Variables:** - - Web App: Copy `apps/web/.env.example` to `apps/web/.env` and configure your HAFAS/Geocoding API keys. - - Mobile App: Copy `apps/mobile/.env.example` to `apps/mobile/.env` and set the backend API URL. - -## ▢️ Running the Applications - -| Script | Command | Description | -| :--- | :--- | :--- | -| `dev` | `npm run dev` | Starts the Next.js development server (Web) on `http://localhost:3000`. | -| `dev:mobile` | `npm run dev:mobile` | Starts the Expo development server (Mobile) and opens the Expo Go simulator. | -| `build` | `npm run build` | Builds the production bundle for the Web application. | - -## πŸ§ͺ Testing & Quality Assurance - -The project enforces strict typing and linting across all workspaces. - -| Script | Command | Description | -| :--- | :--- | :--- | -| `test` | `npm run test` | Runs **Vitest** for the Web app (using jsdom & React Testing Library) and **Jest** for the Mobile app (using jest-expo). | -| `lint` | `npm run lint` | Runs **ESLint 9** across the entire monorepo. | -| `typecheck` | `npm run typecheck` | Runs **TypeScript 5** type checking across all workspaces. | - -### Testing Specific Packages -If you want to run tests for a specific workspace: ```bash -cd apps/web && npm test -cd apps/mobile && npm test -cd packages/core && npm run typecheck +npm install +cp .env.example .env ``` -## 🐳 Docker Support (Web App) +The root workspace owns dependency installation. Avoid installing separately inside workspaces unless you are intentionally changing that workspace's dependency list. -The web application includes a `Dockerfile` and `docker-compose.yml` for containerized development or deployment. +## Environment + +| Variable | Default | Purpose | +| --- | --- | --- | +| `PORT` | `3000` through Next dev defaults | Web server port when the runtime honors it. | +| `HAFAS_URL` | `https://fahrplan.oebb.at/bin/mgate.exe` | Γ–BB HAFAS endpoint. | +| `HAFAS_TIMEOUT_MS` | `10000` | HAFAS request timeout. | +| `HAFAS_VER`, `HAFAS_LANG`, `HAFAS_AID`, `HAFAS_CLIENT_*` | Γ–BB app-compatible defaults | HAFAS request envelope metadata. | +| `NOMINATIM_URL` | `https://nominatim.openstreetmap.org` | Geocoding endpoint. | +| `NOMINATIM_USER_AGENT` | `TimeToLeave/2.0` | Required Nominatim user agent. | +| `OSRM_URL` | `https://router.project-osrm.org` | Bike and foot routing base URL. | +| `WIENER_LINIEN_API_URL` | `https://api.wienerlinien.at/darwin-v2` | Wiener Linien live data base URL. | +| `OEBB_GTFS_URL` | Γ–BB 2026 GTFS ZIP | Optional HAFAS response enrichment source. | +| `CORS_ALLOWED_ORIGINS` | `http://localhost:3000` fallback | Allowed browser origins for `/api/*`. | +| `API_RATE_LIMIT_MAX_REQUESTS` | `120` | Per-IP API requests per window. | +| `API_RATE_LIMIT_WINDOW_MS` | `60000` | Rate-limit window length. | +| `DEPLOYMENT_URL` | local fallback in OAuth code | Public app URL for Google OAuth redirects. | +| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI` | none | Enables Google Calendar sync. | +| `APP_VERSION` | `0.1.0` | Version returned by `/api/health`. | +| `EXPO_PUBLIC_API_BASE_URL` | empty | Mobile backend URL. Required for physical devices unless the empty base URL is intentionally proxied. | + +## Commands + +| Command | Description | +| --- | --- | +| `npm run dev` | Start the web app with `next dev`. | +| `npm run build` | Build the web app. | +| `npm run start` | Start the built web app. | +| `npm run dev:mobile` | Start the Expo dev server. | +| `npm run android` | Run the Expo app on Android from the root script. | +| `npm run ios` | Run the Expo app on iOS from the root script. | +| `npm run lint` | Run ESLint across web, mobile, core, and api-client. | +| `npm run typecheck` | Run TypeScript checks across all workspaces. | +| `npm run test` | Run web Vitest tests and mobile Jest tests. | + +Workspace-specific examples: + +```bash +npm run test -w apps/web +npm run test -w apps/mobile +npm run typecheck -w packages/core +npm run typecheck -w packages/api-client +``` + +## Web Routes + +| Route | Description | +| --- | --- | +| `/` | Departure desk with next upcoming event, train/bike mode selector, live journeys, route sections, reminders, and nearby Wiener Linien departures. | +| `/calendar` | Calendar import/management with URL, file, Google OAuth, and batch destination editing. | + +The web add/edit event flow is implemented by `apps/web/src/app/add-event/AddEventModal.tsx`, not a standalone route. + +## API Routes + +| Endpoint | Methods | Notes | +| --- | --- | --- | +| `/api/health` | `GET` | Returns `{ ok, ts, version }`. | +| `/api/hafas` | `GET`, `POST` | GET is a simple trip search. POST validates and forwards `TripSearch` or `LocMatch`. | +| `/api/geocode` | `GET` | Requires `name`; optional `countrycodes`. Returns the first result. | +| `/api/bike-route` | `GET` | Requires `fromLat`, `fromLng`, `toLat`, `toLng`. | +| `/api/walk-route` | `GET` | Same coordinate contract as bike route, using OSRM foot profile. | +| `/api/calendar` | `GET` | Requires an allowed remote ICS `url`; optional `days`. | +| `/api/calendar/parse` | `POST` | Parses raw ICS text from the request body. | +| `/api/calendar/google` | `GET` | Requires Google OAuth token cookie; optional day horizon. | +| `/api/auth/google` | `GET` | Starts OAuth. | +| `/api/auth/google/callback` | `GET` | Completes OAuth. | +| `/api/auth/google/status` | `GET` | Reports configuration and connection state. | +| `/api/auth/google/disconnect` | `POST` | Clears OAuth token cookie. | +| `/api/wienerlinien/stops` | `GET` | Requires `lat`, `lng`; optional `radius`, capped server-side. | +| `/api/wienerlinien/monitor` | `GET` | Accepts repeated `stopIds`, capped server-side. | + +## Docker + +The web app includes Docker support: ```bash cd apps/web docker-compose up --build ``` -## πŸ“ Web App Routing (Next.js 16) +## Testing Notes -The web app uses the Next.js App Router. Key routes include: +- Web tests use Vitest, jsdom, and Testing Library. +- Mobile tests use Jest, jest-expo, and React Native Testing Library. +- Route, geocoding, HAFAS, calendar, routing, reminder, and UI component tests are present across the repo. +- Run `npm run typecheck` after changing shared types because both apps consume `packages/core`. -| Route | Description | -| :--- | :--- | -| `/` | **Dashboard:** Lists upcoming events and their real-time leave status. | -| `/calendar` | **Calendar Sync:** Interface to import `.ics` files or paste calendar URLs. | -| `/add-event` | **Manual Entry:** Add a new event manually without a calendar source. | -| `/event/[id]` | **Event Details:** Deep dive into a specific event, showing journey options, delays, and local routing. | +## Development Rules -### Backend API Routes -The web app acts as a proxy for external APIs. These are located in `apps/web/src/app/api/`: - -| Endpoint | Method | Description | -| :--- | :--- | :--- | -| `/api/health` | `GET` | Health check for the backend proxy. | -| `/api/calendar` | `GET` | Fetch and parse remote `.ics` files. | -| `/api/calendar/parse` | `POST` | Parse raw `.ics` content. | -| `/api/hafas` | `POST` | Generic HAFAS protocol endpoint (TripSearch, LocMatch). | -| `/api/geocode` | `GET` | Forward geocoding (Nominatim). | -| `/api/geocode/reverse`| `GET` | Reverse geocoding. | -| `/api/bike-route` | `GET` | Bicycle routing between coordinates. | -| `/api/walk-route` | `GET` | Walking routing between coordinates. | -| `/api/wienerlinien/stops`| `GET` | Find nearby WienerLinien stops. | - -## πŸ“± Mobile App Structure - -The mobile app is organized into the following directories: - -- `src/screens/`: UI screens (`EventListScreen`, `EventDetailScreen`, `AddEventScreen`, `CalendarImportScreen`, `SettingsScreen`). -- `src/navigation/`: React Navigation configuration (`AppNavigator.tsx`). -- `src/services/`: API integration and data fetching services. -- `src/store/`: Local state management and AsyncStorage integration. - -## ⚠️ Development Notes - -1. **Next.js 16 Breaking Changes:** The web app runs on Next.js 16.2+, which introduces breaking changes in routing and API conventions. Always check `node_modules/next/dist/docs/` if you encounter unexpected behavior. -2. **HAFAS Timezones:** Never parse HAFAS times using standard `Date` methods. Use `@timetoleave/core` utilities (`parseHafasTime`, `hafasDateTime`) to ensure accurate CET/CEST and DST handling. +- Use `@timetoleave/core` for shared behavior. +- Use `@timetoleave/api-client` for client-to-backend calls that are shared by web and mobile. +- Keep backend validation close to route handlers, then delegate external service behavior to `apps/web/src/lib`. +- Never parse HAFAS date/time values with ad hoc `Date` logic; use `parseHafasTime()` and `hafasDateTime()`. +- This repo uses Next.js 16. Read the relevant `node_modules/next/dist/docs/` material before changing Next.js framework code. diff --git a/docs/README.md b/docs/README.md index 2ec0a42..d8d529e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,59 +1,47 @@ -# πŸ“š TimeToLeave Documentation +# TimeToLeave Documentation -Welcome to the official documentation for TimeToLeave, the smart departure planner that syncs with your calendar and monitors real-time public transport to tell you exactly when to leave home. +This directory documents the current TimeToLeave monorepo: the Next.js web app and backend proxy, the Expo mobile app, and the shared TypeScript packages. -## πŸ—ΊοΈ Table of Contents +## Contents -### 1. [Architecture](./ARCHITECTURE.md) -Understand the monorepo structure, tech stack, data flow, and component relationships between the Web Dashboard, Mobile Client, and shared packages. +| Document | Use it for | +| --- | --- | +| [Architecture](./ARCHITECTURE.md) | System overview, package responsibilities, data flow, integrations, and operational constraints. | +| [Development](./DEVELOPMENT.md) | Setup, environment variables, local commands, route map, quality checks, Docker, and mobile development notes. | +| [Core & API Client Reference](./API_REFERENCE.md) | Shared package exports, HAFAS helpers, countdown/status logic, and `ApiClient` methods. | +| [User Guide](./USER_GUIDE.md) | How to use the web and mobile apps, import calendars, read leave-by statuses, and manage settings. | +| [Codebase Function Guide](./CODEBASE_FUNCTION_GUIDE.md) | File-by-file map of first-party source modules, functions, hooks, and components. | -### 2. [Core & API Reference](./API_REFERENCE.md) -Detailed reference for the internal packages: -- **`@timetoleave/core`**: Domain types, HAFAS time parsing, countdown/status logic, and formatting utilities. -- **`@timetoleave/api-client`**: The unified HTTP client that wraps the backend proxy routes. - -### 3. [Development Guide](./DEVELOPMENT.md) -Everything a developer needs to get started: -- Prerequisites and installation instructions. -- Running the Web and Mobile apps in development mode. -- Testing (Vitest/Jest), Linting (ESLint), and Type Checking (TypeScript). -- Backend API endpoints and Next.js 16 routing conventions. - -### 4. [User Guide](./USER_GUIDE.md) -A guide for end-users explaining how to sync calendars, view event details, interpret the "Leave Status" color codes, and configure mobile notifications. - ---- - -## ⚑ Quick Start +## Quick Start ```bash -# Clone the repository -git clone -cd TimeToLeave - -# Install dependencies for the entire monorepo npm install - -# Start the Web development server +cp .env.example .env npm run dev +``` -# Start the Mobile development server +For mobile development: + +```bash npm run dev:mobile ``` -## πŸ›‘οΈ Code Quality +Set `EXPO_PUBLIC_API_BASE_URL` for device builds so the app can reach the web backend. + +## Quality Checks ```bash -# Run all tests (Web Vitest + Mobile Jest) -npm run test - -# Run ESLint across the monorepo npm run lint - -# Run TypeScript type checking npm run typecheck +npm run test +npm run build ``` ---- +## Current User-Facing Routes -*Built for developers who bike to the train and hate missing their connections.* +| Route | Purpose | +| --- | --- | +| `/` | Departure desk showing the next upcoming event and live route/departure data. | +| `/calendar` | Calendar import, Google Calendar sync, and batch destination review. | + +Add/edit event actions are handled in modal and native-screen flows, not by standalone web page routes. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index a175621..12a0bfb 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -1,93 +1,131 @@ # User Guide -Welcome to the TimeToLeave User Guide! This document explains how to use the TimeToLeave web dashboard and mobile application to plan your departures seamlessly. +TimeToLeave helps you answer one practical question: when do I need to leave for my next appointment? -## πŸš€ How TimeToLeave Works +The app imports events with locations, finds the nearest usable transport station, checks live train and local transit data, calculates walking or biking time, and shows a live leave-by countdown. -TimeToLeave acts as your personal departure planner. Instead of manually checking train schedules, you simply sync your calendar. The app calculates the best public transport connections to your upcoming appointments, monitors real-time delays, and tells you exactly when to leave home. +## Web Dashboard -### Core Workflow -1. **Sync Your Calendar:** Import your `.ics` file or provide a calendar URL. -2. **Set Your Origin:** Define your home station or let the app use your current location. -3. **Automatic Planning:** The app queries real-time public transport data (via HAFAS and WienerLinien) to find the best connections. -4. **Leave Status:** The dashboard displays a clear status: `Leave now`, `On time`, `Delayed +X min`, or `Departure missed`. +### Departure Desk ---- +Open `/` to see the next upcoming event. The card shows: -## 🌐 Web Dashboard +- Event title and appointment time. +- Destination address or place. +- A live countdown badge. +- Leave-by, arrive-by, and buffer times. +- Train and bike mode selector. +- Train journeys with delay, platform, cancellation, arrival, and transfer information. +- Optional final walking route from the arrival station to the destination. +- Optional door-to-door bike route. +- Nearby Wiener Linien stops and live departures when destination coordinates are available. -The web dashboard is designed for planning and monitoring your upcoming events from a desktop or laptop. +The dashboard currently focuses on the next upcoming event. Imported and manually added events are stored locally in the browser. -### Main Dashboard (`/`) -This is the default view when you open the application. It displays a list of your upcoming events sorted by date. Each event card shows: -- **Event Title & Time:** The name of the appointment and when it starts. -- **Destination Station:** The nearest station to your event. -- **Leave Status:** A color-coded indicator of your current standing: - - πŸ”΄ **Red:** Time is up ("Now") or you're extremely close to departure. - - 🟠 **Orange:** Urgent! You need to leave within 10 minutes. - - 🟑 **Yellow:** Moderate urgency (within 30 minutes). - - 🟒 **Green:** Relaxed (within 60 minutes). - - πŸ”΅ **Blue:** Plenty of time remaining (over 1 hour). +### Add or Edit Events -### Calendar Sync (`/calendar`) -Use this page to connect your personal calendar. -- **URL Import:** Paste a public `.ics` calendar URL. The app will fetch upcoming events automatically. -- **File Import:** Upload a `.ics` file directly from your computer. +Use the add/edit event modal from the web interface to create or change local events. Editing is modal-based; there is no separate `/add-event` page. -### Event Details (`/event/[id]`) -Click on any event from the dashboard to view detailed planning information: -- **Journey Options:** A list of available trains/buses with scheduled vs. real departure times. -- **Delay Information:** Real-time delays are highlighted. Cancelled journeys are clearly marked. -- **Local Routing:** See how long it takes to bike or walk from your home to the departure station. +### Calendar Page -### Manual Event Entry (`/add-event`) -If you don't have a calendar synced, or you have a one-off appointment, you can manually add an event by specifying the title, destination, and time. +Open `/calendar` to import and review events. ---- +Available import sources: -## πŸ“± Mobile Application +| Source | Description | +| --- | --- | +| URL | Paste an allowed public ICS URL. The backend fetches and parses future events with locations. | +| File | Upload a local `.ics` file. The backend parses the file content. | +| Google | Connect Google Calendar through OAuth, sync events, and disconnect when needed. | -The mobile app is perfect for on-the-go checks, leveraging your phone's native capabilities. +Imported events are merged into local storage. The calendar view also includes batch destination editing so locations can be corrected before using them for route planning. -### Event List Screen -The home screen mirrors the web dashboard, showing your upcoming events and their real-time status. You can pull-to-refresh to get the latest transit data. +## Mobile App -### Event Detail Screen -Tap on an event to see: -- The best journey options and real-time platform information. -- A countdown timer to your departure. -- Step-by-step bike/walk directions to the station. +The mobile app includes: -### Calendar Import Screen -Import your calendar directly on the device. The mobile app can read your device's native calendar apps (via `expo-calendar`) if you prefer not to use a remote `.ics` URL. +- Event list. +- Add/edit event screen. +- Event detail screen. +- Calendar import screen. +- Settings screen. -### Settings Screen -Customize your experience: -- **Buffer Time:** Set a default buffer (e.g., arrive 5 minutes early). -- **Departure Buffer:** Add extra time for the actual transit journey. -- **Toggle Options:** Enable/disable the walking or biking route suggestions based on your preference. -- **Notifications:** Configure push notifications so you get an alert exactly when it's time to leave. +### Calendar Import ---- +The mobile calendar import screen supports: -## ⏱️ Understanding "Leave Status" +- ICS URL import through the configured backend. +- Native device-calendar sync for the next 30 days. +- Calendar selection before native sync. +- Source grouping for CalDAV/DAVx, Apple, Google, Exchange, subscribed, local, CardDAV, ActiveSync, and other calendars when the device reports that metadata. -The "Leave Status" is the heart of TimeToLeave. It is calculated dynamically by comparing the **real departure time** of your best non-cancelled journey against the **current time**. +If no native calendar selection is saved, the sync uses all available calendars. + +### Event Detail + +Tap an event to see: + +- Leave-by and arrive-by times. +- Train journeys from the saved origin station to the destination station. +- Optional walking route for the final leg. +- Optional bike route. +- Nearby destination stops and live Wiener Linien departures. + +### Settings + +Use settings to configure: + +- Origin station. +- Current-location origin lookup. +- Reminder buffer. +- Arrival buffer. +- Walking option visibility. +- Bike option visibility. +- Notifications. +- Dark/light theme. + +## Leave-By Status + +The countdown and leave-by time are calculated from the selected transport mode. + +For train mode, the app looks for a non-cancelled journey that arrives early enough after accounting for the final walk and arrival buffer. For bike mode, it subtracts the bike route duration from the target arrival time. + +Countdown colors: + +| Color | Meaning | +| --- | --- | +| Red | Leave time is now or already passed. | +| Orange | Leave time is within 10 minutes. | +| Yellow | Leave time is within 30 minutes. | +| Green | Leave time is within 60 minutes. | +| Blue | More than 60 minutes remain. | + +Text statuses can include: | Status | Meaning | -| :--- | :--- | -| **Leave now** | Your train departs within 15 minutes. Head out! | -| **On time** | Everything is running smoothly, and you have a comfortable window. | -| **Delayed +X min** | Your train is delayed. You can stay home a bit longer! | -| **Departure missed** | The best available journey has already departed. A new search may be required. | -| **All journeys cancelled** | Unfortunately, all connections for this time slot are cancelled. | +| --- | --- | +| `Leave now` | The selected departure is close enough that you should go. | +| `On time` | The selected connection is currently usable. | +| `Delayed +N min` | The selected journey is delayed by more than 10 minutes. | +| `Departure missed` | The selected departure has already left. | +| `All journeys cancelled` | Every returned journey is cancelled. | +| `No journey data` | No usable journey data is available. | -## πŸ”’ Privacy & Data +## Notifications -TimeToLeave is designed with privacy in mind: -- Calendar data is processed server-side solely for the purpose of event extraction and is not permanently stored beyond the active session. -- Geolocation data is used exclusively for calculating routes and finding nearby stations. It is never shared with third parties. +Web reminders use browser notifications when permission is granted. Mobile reminders use local Expo notifications. ---- +Mobile notifications are scheduled from stored events and settings. Because the mobile store does not retain live journey data, scheduled notification times use a conservative fallback based on event time, arrival buffer, and reminder buffer. -*Happy traveling! Built for developers who bike to the train and hate missing their connections.* +## Data and Privacy + +Event data is stored locally in the browser or on the device. Some features send the minimum required request data to external services: + +- Destination text is sent to Nominatim for geocoding. +- Coordinates are sent to OSRM for bike/walk route calculation. +- Station IDs and journey times are sent to Γ–BB HAFAS. +- Coordinates or stop IDs are sent to Wiener Linien for nearby stops and departures. +- Google Calendar sync uses Google OAuth and server-side token cookies. +- Remote ICS imports fetch the provided calendar URL through the backend. + +See the root [Privacy Policy](../PRIVACY_POLICY.md) for more detail.