Realtime transit board for Puget Sound, available as both an installable web app and a Capacitor-based iOS app.
Link Pulse tracks live vehicles, arrivals, and service health across:
Linklight railRapidRidebus rapid transitSwiftbus rapid transit
The project keeps one shared JavaScript UI and ships it in two forms:
- a PWA web build for GitHub Pages
- an iOS shell built with Capacitor
- Multi-system board with
Map,Trains/Buses,Favorites, andInsights - Live arrivals and vehicle positions via Puget Sound OneBusAway
- Station dialog with arrivals, alerts, favorites, sharing, and board display mode
- Train detail dialog with previous/current/next stop and ETA-to-terminal
- Station search across names, lines, systems, and nearby stations
- Persistent preferences for theme, language, favorites, and recent searches
- Bilingual UI in English and Simplified Chinese
- iPhone polish for dialogs, board mode, map labels, icons, and splash screens
- Vite + vite-plugin-pwa
- Vanilla JavaScript (ES modules) + SVG rendering
- Capacitor for iOS packaging
- Capacitor plugins:
PreferencesGeolocationShareClipboardHapticsSplash Screen
- Vitest for unit tests
- Node.js
20.19+or22.12+ - npm
10+ - Xcode for simulator or device testing
npm install
# Optional: use your own OneBusAway key instead of TEST
cp .env.example .env.local
# Edit .env.local and set VITE_OBA_KEY=your_keyStatic GTFS-derived data refresh is explicit:
npm run refresh:dataUse that when you want fresh source data. Routine web and iOS builds do not force a GTFS download first.
# Web dev
npm run dev
npm run dev:refresh
# Production builds
npm run build
npm run build:web
npm run build:native
# iOS sync / open
npm run cap:sync
npm run ios:open
# Local preview
npm run preview
# Tests
npm test
npm run test:run
npm run test:ui
npm run test:coveragenpm run dev starts the Vite dev server. In this project, the web app is served under /link/, so the page is typically available at:
http://localhost:5173/link/
npm run build:web produces the PWA build:
- service worker enabled
- GitHub Pages base path
/link/
The iOS app currently ships as:
- App name:
Link Pulse - Bundle ID:
com.linkpulse.app - Scheme:
LinkPulse
Relevant files:
- Capacitor config:
capacitor.config.json - Xcode project:
ios/App/App.xcodeproj - App source/assets:
ios/App/LinkPulse/
Typical simulator/device loop:
# Build web assets for the native shell and sync them into ios/
npm run cap:sync
# Open the Xcode project
npm run ios:opennpm run build:native differs from the web build in two important ways:
- it switches the base path to
/ - it disables PWA registration so the native shell does not ship a service worker
Native integrations live in src/native/:
src/native/platform.jssrc/native/storage.jssrc/native/location.jssrc/native/share.jssrc/native/haptics.jssrc/native/splash.js
| Variable | Purpose |
|---|---|
VITE_OBA_KEY |
OneBusAway API key. Falls back to public TEST if omitted. |
VITE_TARGET |
Build target selector used by scripts (web or native). |
VITE_BASE |
Override the base path for unusual deployments. |
VITE_SHARE_BASE_URL |
Base URL used when generating shared station links. |
The app works with the public
TESTkey, but refreshes more conservatively and backs off more aggressively after rate limiting.
| Source | Endpoint |
|---|---|
| Live vehicles | Puget Sound OneBusAway vehicles-for-agency/{agencyId}.json |
| Stop arrivals | arrivals-and-departures-for-stop/{stopId}.json |
| Static system data | Generated by scripts/build-link-data.mjs from GTFS feeds and OBA geometry |
docs/
ios-app-store-readiness.md iOS rollout and App Store notes
ios/
App/
App.xcodeproj/ Xcode project
CapApp-SPM/ Capacitor Swift package bridge
LinkPulse/ App source, assets, plist, privacy manifest
public/
icon.svg Web icon source
splash-light.svg Light splash source
splash-dark.svg Dark splash source
pulse-data.json Generated static transit data artifact
scripts/
build-link-data.mjs GTFS / OBA static data generator
src/
dialogs/ Station, train, alert, and overlay dialog logic
native/ Web/native adapters
renderers/ Map, train list, and insights renderers
main.js App shell and UI wiring
static-data.js Static data loading and bootstrap
station-search.js Search and nearby lookup
favorites.js Favorite station management
recent-stations.js Recent station session state
style.css Global styles
capacitor.config.json Capacitor app config
vite.config.js Web/native Vite build config
Web deploys are published through deploy.yml.
| Branch | URL |
|---|---|
main |
https://kevinngw.github.io/link/ |
dev |
https://kevinngw.github.io/link/dev/ |
This repo already includes the basics needed for iOS packaging:
- native app shell via Capacitor
- iOS app icons and splash assets
PrivacyInfo.xcprivacy- location usage copy in
Info.plist - native-safe storage, geolocation, sharing, haptics, and splash handling
Longer implementation and review notes live in docs/ios-app-store-readiness.md.