Files
time_to_leave/REWRITE_PLAN.md
T
fegger b131e3c69a Rewrite to Node.js
Phases 1 and 2.
2026-05-09 11:00:21 +02:00

19 KiB

ÖBB Planner — Next.js Rewrite Plan

Status: Planned Created: 2024 Scope: Full rewrite from CRA + Express to Next.js App Router with new features


1. Current State

Layer Technology Files
Backend Express.js + CORS + node-ical server/index.js (~140 lines)
Frontend Create React App (React 19) oebb-planner.jsx (~700 lines monolithic component)
Tests Jest + Supertest server/__tests__/*.test.js
Build Separate npm start for server + react-scripts start for frontend Two independent processes

Problems with current architecture

  • Two separate processes to manage
  • Monolithic component (~700 lines) — no component breakdown
  • CRA is deprecated; react-scripts is unmaintained
  • No TypeScript
  • Inline styles make theming/maintenance painful
  • No proper state management pattern
  • Duplication between oebb-planner.jsx and oebb-planner-app/src/App.js

2. New Features (Beyond Rewrite)

2.1 Calendar View (/calendar)

A browsable month view that lets you navigate between months, see events as badges on dates, and click into a day to see event + train details.

How it works:

  • User imports calendar via the existing ICS flow
  • Events are stored in React state / localStorage
  • A month grid shows event dots on dates that have events
  • Clicking a day shows that day's events with their train info
  • Clicking an event scrolls to / expands the full event card

UI wireframe:

┌──────────────────────┬──────────────────────┐
│  ←  November 2024  → │ [Day] [Week] [Month] │
├──────────────────────┼──────────────────────┤
│ Su Mo Tu We Th Fr Sa │  Thursday, 14 Nov    │
│  1  2  3  4  5  6  7 │                      │
│  8  9 10 11 12 13 14 │  ● 14:00 Graz Hbf    │
│          ●       ●   │     🚂 Leave 13:10   │
│ 15 16 17 18 19 20 21 │     🚲 Leave 11:07   │
│            ●       ● │                      │
│ ...                  │  ● 17:00 Linz Hbf    │
│                      │     🚂 Leave 15:30   │
│                      │     🚲 Leave 15:45   │
└──────────────────────┴──────────────────────┘

2.2 Bicycle Routing

API choice: OSRM Public Demo Server (router.project-osrm.org)

  • Free, no API key required, open source
  • Supports bicycle profile
  • Returns distance, duration, turn-by-turn steps
  • For production: self-host or switch to OpenRouteService

Geocoding: Nominatim (OpenStreetMap) to convert station/event location names to coordinates for routing.

Data flow:

Origin station (name) → already have coords from geolocation
Destination station (name) → geocode via Nominatim → get coordinates
                            → OSRM bicycle route → distance + duration
                            → cache in TripData alongside train journeys

2.3 Train vs Bicycle Comparison

Each event card shows both travel modes side by side:

┌───────────────────────────────────────────────────┐
│ 🏔 Meeting in Graz                               │
│ 📍 Graz Hbf · 14:00 · Thu, 14 Nov                │
├───────────────────────────────────────────────────┤
│  🚂 NEXT BEST TRAIN            🚲 BYCYCLE         │
│  RJX 5234 on time                42 km            │
│  DEP 13:22  ARR 14:35          2h 15min          │
│  PLAT 3                          0 emissions      │
│  Leave by 13:10                 Leave by 11:07    │
└───────────────────────────────────────────────────┘

3. Target Architecture

oebb_planner/
├── package.json
├── next.config.js
├── tsconfig.json
├── tailwind.config.ts
├── postcss.config.mjs
├── vitest.config.ts
├── src/
│   ├── app/
│   │   ├── layout.tsx                    ← root layout (fonts, providers, navbar)
│   │   ├── page.tsx                      ← main planner page (event cards)
│   │   ├── globals.css
│   │   ├── calendar/
│   │   │   └── page.tsx                  ← browsable calendar view (NEW)
│   │   ├── api/
│   │   │   ├── hafas/
│   │   │   │   └── route.ts              ← POST → proxy to ÖBB HAFAS
│   │   │   ├── calendar/
│   │   │   │   ├── route.ts              ← GET → fetch remote ICS calendar
│   │   │   │   └── parse/
│   │   │   │       └── route.ts          ← POST → parse raw ICS body
│   │   │   ├── geocode/
│   │   │   │   └── route.ts              ← GET → Nominatim geocoding (NEW)
│   │   │   ├── bike-route/
│   │   │   │   └── route.ts              ← GET → OSRM bicycle routing (NEW)
│   │   │   └── health/
│   │   │       └── route.ts              ← GET → liveness check
│   │   └── not-found.tsx
│   ├── components/
│   │   ├── layout/
│   │   │   ├── Header.tsx                ← logo, server status, clock
│   │   │   └── Navbar.tsx                ← navigation between pages (NEW)
│   │   ├── calendar/
│   │   │   ├── CalendarPanel.tsx          ← collapsible import panel
│   │   │   ├── CalendarView.tsx           ← month grid component (NEW)
│   │   │   ├── DayEvents.tsx             ← events for a selected day (NEW)
│   │   │   ├── UrlTab.tsx
│   │   │   └── FileTab.tsx
│   │   ├── event/
│   │   │   ├── EventCard.tsx             ← single event card (with train+bike)
│   │   │   ├── TrainSection.tsx          ← train data in event card
│   │   │   ├── BikeSection.tsx           ← bicycle data in event card (NEW)
│   │   │   ├── JourneyList.tsx           ← all departures table
│   │   │   └── LeaveByBadge.tsx
│   │   ├── add-event/
│   │   │   └── AddEventModal.tsx
│   │   └── ui/
│   │       ├── Chip.tsx
│   │       ├── Button.tsx
│   │       └── LoadingSpinner.tsx
│   ├── hooks/
│   │   ├── useServerHealth.ts             ← polls /api/health every 30s
│   │   ├── useClock.ts                    ← ticking clock (every 10s)
│   │   ├── useGeolocation.ts              ← GPS positioning
│   │   ├── useOriginStation.ts            ← finds nearest ÖBB station
│   │   ├── useJourneys.ts                 ← fetches + caches train data
│   │   ├── useBikeRoute.ts               ← fetches bicycle route (NEW)
│   │   ├── useCalendar.ts                 ← calendar import logic
│   │   └── useEventsStore.ts             ← shared events state (NEW)
│   ├── lib/
│   │   ├── hafas.ts                       ← HAFAS API client + types
│   │   ├── calendar.ts                    ← ICS helpers (extractEvents, cleanLocation)
│   │   ├── countdown.ts                   ← leaveBy, countdownInfo
│   │   ├── formatting.ts                  ← fmtTime, delayColor
│   │   ├── demo.ts                        ← demo journey generator
│   │   ├── constants.ts                   ← WALK_MINS, RED, SAMPLE_EVENTS, HAFAS_BASE
│   │   ├── geocode.ts                     ← Nominatim client (NEW)
│   │   └── bike-route.ts                  ← OSRM client (NEW)
│   └── types/
│       └── index.ts                       ← all TypeScript interfaces
├── __tests__/
│   ├── lib/
│   │   ├── calendar.test.ts
│   │   ├── countdown.test.ts
│   │   └── bike-route.test.ts             ← NEW
│   └── api/
│       ├── hafas.test.ts
│       ├── calendar.test.ts
│       ├── geocode.test.ts                ← NEW
│       ├── bike-route.test.ts             ← NEW
│       └── health.test.ts
├── public/
│   ├── favicon.ico
│   └── robots.txt
├── .env.example
└── README.md

4. TypeScript Types

src/types/index.ts

// ── HAFAS / Train ───────────────────────────────────────────

interface Journey {
  id: string;
  sD: Date;          // scheduled departure
  rD: Date;          // real departure
  sA: Date;          // scheduled arrival
  rA: Date;          // real arrival
  delay: number;     // minutes
  platform: string;
  changes: number;
  trains: string[];
  cancelled: boolean;
}

interface Station {
  name: string;
  extId: string;
}

// ── Events ───────────────────────────────────────────────────

interface Event {
  id: string;
  title: string;
  destination: string;
  eventTime: Date;
  source: "manual" | "calendar";
}

// ── Trip Data (per event) ────────────────────────────────────

interface TripDataEntry {
  journeys: Journey[];
  destName: string;
  demo: boolean;
  loading: boolean;
  bikeRoute?: BikeRoute | null;       // NEW
  bikeLoading?: boolean;              // NEW
  bikeError?: string | null;          // NEW
  destCoords?: { lat: number; lng: number }; // NEW (cached geocode)
}

// ── Bicycle Routing (NEW) ────────────────────────────────────

interface BikeRoute {
  distance: number;       // meters
  duration: number;       // seconds
  steps?: BikeStep[];
}

interface BikeStep {
  name: string;
  distance: number;
  duration: number;
  instruction: string;
}

// ── Geocoding (NEW) ──────────────────────────────────────────

interface GeocodeResult {
  lat: number;
  lng: number;
  display_name: string;
}

// ── Calendar Import ──────────────────────────────────────────

interface CalendarEvent {
  id: string;
  title: string;
  destination: string;
  eventTime: string;    // ISO string from API
  source: "calendar";
}

// ── UI Helpers ───────────────────────────────────────────────

interface CountdownInfo {
  label: string;
  color: string;
  urgent: boolean;
}

type ServerStatus = null | true | false;
type LiveStatus   = null | true | false;
type LocState     = "pending" | "granted" | "denied";
type CalStatus    = null | "loading" | "ok" | "error";

5. New API Routes

POST /api/hafas

  • Purpose: Proxy to fahrplan.oebb.at/bin/mgate.exe
  • Body: { svcReqL: [...] } — HAFAS service requests
  • Returns: HAFAS JSON response
  • Timeout: 12s

GET /api/calendar?url=<ics_url>&days=14

  • Purpose: Fetch and parse a remote ICS calendar
  • Returns: CalendarEvent[]
  • Supports: https://, webcal://

POST /api/calendar/parse

  • Purpose: Parse raw ICS content sent in request body
  • Body: Raw ICS text
  • Returns: CalendarEvent[]

GET /api/geocode?name=Graz+Hbf&countrycodes=at (NEW)

  • Purpose: Proxy to Nominatim for address→coordinates
  • Upstream: nominatim.openstreetmap.org/search?q={name}&format=json&limit=1&countrycodes=at
  • Returns: { lat, lng, display_name }
  • Caching: In-memory cache with TTL to avoid rate limits

GET /api/bike-route?fromLat=...&fromLng=...&toLat=...&toLng=... (NEW)

  • Purpose: Proxy to OSRM for bicycle routing
  • Upstream: router.project-osrm.org/route/v1/bicycle/{lon1},{lat1};{lon2},{lat2}?overview=false
  • Returns: { distance, duration, steps: [...] }

GET /api/health

  • Purpose: Liveness check
  • Returns: { ok: true, ts: ISOString, version: "2.0.0" }

6. Key Technical Decisions

Decision Choice Rationale
Framework Next.js 15 App Router Modern standard; API routes are serverless-compatible
Language TypeScript (strict) Type safety across frontend and backend
Styling Tailwind CSS Replaces 700 lines of inline styles; consistent theming
State React hooks + Context No Redux needed for this scale
API proxy Next.js Route Handlers Same logic as Express, no Express dependency
ICS parsing Keep node-ical Already works, well-tested
Bike routing OSRM public demo Free, no API key, good enough for dev
Geocoding Nominatim Free, open source, good Austria coverage
Testing Vitest + Testing Library Faster than Jest for TS, better DX
Deployment Vercel (recommended) or any Node host Single artifact

7. Migration Phases

Phase 1 — Scaffold Next.js Project (~30 min)

  1. Initialize Next.js 15 with App Router, TypeScript, Tailwind CSS
  2. Set up tsconfig.json with strict mode
  3. Create .env.example with PORT, HAFAS_URL, NOMINATIM_URL, OSRM_URL
  4. Configure next.config.js (rewrites if needed)
  5. Configure vitest.config.ts
  6. Set up postcss.config.mjs

Phase 2 — Types + Library Layer (~2 hours)

  1. Define TypeScript types in src/types/index.ts
  2. Port lib/hafas.ts — HAFAS API client functions with proper types
  3. Port lib/calendar.tsextractEvents + cleanLocation (reusable in API routes AND tests)
  4. Port lib/countdown.ts, lib/formatting.ts, lib/constants.ts, lib/demo.ts
  5. Create lib/geocode.ts — Nominatim client (NEW)
  6. Create lib/bike-route.ts — OSRM client (NEW)

Phase 3 — API Routes (~45 min)

  1. src/app/api/hafas/route.ts — POST handler, same logic as Express
  2. src/app/api/calendar/route.ts — GET handler for remote ICS
  3. src/app/api/calendar/parse/route.ts — POST handler for ICS body
  4. src/app/api/geocode/route.ts — GET handler for Nominatim (NEW)
  5. src/app/api/bike-route/route.ts — GET handler for OSRM (NEW)
  6. src/app/api/health/route.ts — health check

Phase 4 — Custom Hooks (~2.5 hours)

  1. useServerHealth.ts — polls /api/health every 30s
  2. useClock.ts — interval that updates now every 10s
  3. useGeolocation.ts — wraps navigator.geolocation
  4. useOriginStation.ts — finds nearest station from geolocation
  5. useJourneys.ts — the complex fetchAll logic, per-event journey fetching
  6. useBikeRoute.ts — fetches bicycle route for an event (NEW)
  7. useCalendar.ts — URL/file import with merge logic
  8. useEventsStore.ts — shared events state via Context (NEW)

Phase 5 — UI Components (~3.5 hours)

  1. ui/Chip.tsx — small badge component
  2. ui/Button.tsx — styled button
  3. ui/LoadingSpinner.tsx — loading indicator
  4. event/LeaveByBadge.tsx — countdown badge
  5. event/JourneyList.tsx — departure rows
  6. event/TrainSection.tsx — train data in event card
  7. event/BikeSection.tsx — bicycle data in event card (NEW)
  8. event/EventCard.tsx — composes train + bike sections
  9. calendar/UrlTab.tsx
  10. calendar/FileTab.tsx
  11. calendar/CalendarPanel.tsx
  12. add-event/AddEventModal.tsx
  13. layout/Header.tsx
  14. layout/Navbar.tsx (NEW)

Phase 6 — Calendar Page (~1 hour)

  1. calendar/CalendarView.tsx — month grid component (NEW)
  2. calendar/DayEvents.tsx — events for a selected day (NEW)
  3. app/calendar/page.tsx — calendar route (NEW)

Phase 7 — Tests (~2 hours)

  1. Migrate server/__tests__/*.test.js__tests__/api/*.test.ts
  2. Add unit tests for lib/calendar.ts, lib/countdown.ts
  3. Add API tests for geocode and bike-route (NEW)
  4. Add component smoke tests with @testing-library/react

Phase 8 — Cleanup (~30 min)

  1. Delete old server/ directory
  2. Delete old oebb-planner-app/ directory
  3. Delete oebb-planner.jsx
  4. Update README.md with new architecture and instructions
  5. Final integration test

8. Dependencies

Runtime

{
  "dependencies": {
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "node-ical": "^0.18.0"
  }
}

Development

{
  "devDependencies": {
    "typescript": "^5.6.0",
    "@types/node": "^22.0.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "@types/node-ical": "^0.18.0",
    "tailwindcss": "^3.4.0",
    "postcss": "^8.4.0",
    "autoprefixer": "^10.4.0",
    "vitest": "^2.0.0",
    "@testing-library/react": "^16.0.0",
    "@testing-library/jest-dom": "^6.0.0",
    "jsdom": "^25.0.0"
  }
}

9. Environment Variables

# .env.example

# Server port
PORT=3001

# ÖBB HAFAS API
HAFAS_URL=https://fahrplan.oebb.at/bin/mgate.exe

# Nominatim geocoding (OpenStreetMap)
NOMINATIM_URL=https://nominatim.openstreetmap.org

# OSRM bicycle routing
OSRM_URL=https://router.project-osrm.org

# Nominatim user-agent / referer (required by their ToS)
NOMINATIM_USER_AGENT=OebbPlanner/1.0

10. Benefits of the Rewrite

  1. Single processnpm run dev starts everything
  2. Type safety — TypeScript catches bugs at compile time
  3. Composability — 700-line component becomes ~15 focused components
  4. Reusabilitylib/ functions testable independently
  5. Deployable — one artifact, works on Vercel / any Node host
  6. Maintainable — hooks encapsulate side effects, components are pure UI
  7. Modern tooling — no more CRA, no more react-scripts eject anxiety
  8. Bicycle routing — complete picture of train vs bike for each event
  9. Calendar view — browse events by date, not just a flat list
  10. Train+bike comparison — side-by-side travel times in each event card

11. Estimated Effort

Phase Description Est. Time
1 Scaffold Next.js + Tailwind + TS 30 min
2 Types + lib layer (incl. geocode, bike-route) 2 hours
3 API routes (incl. geocode, bike-route) 45 min
4 Custom hooks (incl. bike-route, events-store) 2.5 hours
5 UI Components (incl. bike section, navbar) 3.5 hours
6 Calendar page (incl. month view, day events) 1 hour
7 Tests (API + lib + component) 2 hours
8 Cleanup (delete old dirs, update README) 30 min
Total ~13-14 hours