A tiny static web app that shows the scheduled first & last bus time for any Singapore bus service at any stop. No backend, no build step — just HTML/CSS/JS served from GitHub Pages, reading a pre-generated JSON file.
Scheduled times only. This is a quick-glance utility, not a live tracker. See Known limitations.
The LTA DataMall API isn't CORS-enabled and its account key must stay secret, so the browser can't call it directly. Instead:
scripts/fetch_data.pypulls the BusRoutes and BusStops datasets from LTA (server-side) and writes sharded JSON: a tiny index plus one small file per bus service.- A scheduled GitHub Action runs that script daily and commits the refreshed data back to the repo.
index.html/style.css/script.jsread that static JSON — the page loads a few KB for the one service you ask about, not a big blob.
Because the data is split per service, a visitor downloads roughly 5 KB
(one service shard) instead of a multi-megabyte combined file. Shards are
cache-busted by the dataset's generated timestamp, so browsers cache them
until the next daily refresh.
scripts/fetch_data.py # LTA -> sharded data/ (paginated, compacted) + holidays
.github/workflows/ # daily + manual refresh, commits the shards
update-data.yml
data/services.json # index: { generated, services: [...] } (loaded at boot)
data/svc/<SERVICE>.json # one shard per service (stops embedded), loaded on demand
data/holidays.json # dates that use the Sun/PH schedule (auto-refreshed)
logic.js # pure helpers (times, distance)
index.html / style.css / script.js # the static site
fonts/*.woff2 # self-hosted Space Grotesk + JetBrains Mono (offline, no 3rd-party)
manifest.webmanifest / sw.js # PWA: installable + offline
icon.svg / icon-192.png / icon-512.png / apple-touch-icon.png # app icons
- Go to the LTA DataMall API request page.
- Fill in the form. The key is emailed to you (usually within a day).
- Keep it secret — treat it like a password. Do not paste it into code, commit it, or share it publicly.
- Push this repo to GitHub.
- Go to Settings → Secrets and variables → Actions → New repository secret.
- Name it exactly
LTA_ACCOUNT_KEY, paste your key as the value, and save.
The workflow reads it via ${{ secrets.LTA_ACCOUNT_KEY }} — the key never
appears in the repo or the built site.
The repo ships with committed shards under data/svc/ so the page works
immediately. To refresh from LTA (the ~23,000-record dataset):
-
In CI: go to the Actions tab → Update bus data → Run workflow. It fetches from LTA and commits the refreshed shards +
data/services.json. -
Locally (optional), if you have Python 3.9+:
# macOS / Linux export LTA_ACCOUNT_KEY="your_key_here" python scripts/fetch_data.py # Windows PowerShell $env:LTA_ACCOUNT_KEY = "your_key_here" python scripts/fetch_data.py
This regenerates
data/svc/*.json,data/services.json, anddata/holidays.json(holidays come from the key-less Nager.Date API; a failure there leaves the existing holiday file untouched). Commit them if you want (CI will keep them fresh afterwards). The script uses only the Python standard library — nopip installneeded.
- Settings → Pages.
- Under Build and deployment → Source, choose Deploy from a branch.
- Select the
mainbranch and the/ (root)folder, then Save. - Your site goes live at
https://<username>.github.io/<repo>/within a minute.
That's it — the daily Action keeps the schedule data current.
- Enter a bus service number (e.g.
196). Services with letter suffixes (196A,NR7) work too. - The route's stops show one direction at a time; a direction toggle (labelled by each terminal) flips between the two travel directions. Tap a stop for its times. Use the filter box to narrow long routes by stop name, or tap the location button to sort the list by distance from you (granting location once makes later visits sort automatically).
- Read the first/last times for Weekday / Saturday / Sun-PH (today's row is highlighted; after-midnight times are tagged +1), for the direction you picked in the toggle.
Because the page fetches JSON from data/, open it via a local web server
(not file://):
python -m http.server 8000
# then visit http://localhost:8000- No live vehicle tracking. LTA's real-time Bus Arrival endpoint isn't reachable from a static page without a proxy, so this app is scheduled-only. Adding live arrivals would mean introducing a small server-side proxy to hold the account key — a deliberate departure from this app's no-backend design.
- Public-holiday dates for lunar/Islamic holidays are estimates. The
holiday list is refreshed automatically from the
Nager.Date API (current + next year, including the
in-lieu Monday when a holiday falls on a Sunday). Movable holidays such as
Hari Raya can occasionally differ by a day from
MOM's gazette;
edit
data/holidays.jsonby hand to override a specific date. - After-midnight edge cases. Times like
0030are treated as belonging to the small hours of the next day and tagged with a+1; unusual schedules may not be perfectly represented. - Data is only as fresh as the last workflow run (daily by default).
- Shareable links. The loaded service, chosen direction, and selected stop
are kept in the URL (
?svc=196&dir=1&code=84009), so a view can be bookmarked, shared, or restored after a refresh. - Installable / offline (PWA). A
manifest.webmanifestandsw.jsservice worker cache the app shell and each service you view, so the page installs to a home screen and works with no signal (e.g. on the bus). The index and holiday data are fetched network-first so they stay fresh when you're online. BumpCACHE_VERSIONinsw.jswhen the shell files change.
This app contains information from LTA DataMall, accessed under the Singapore Open Data Licence v1.0.
It is not affiliated with or endorsed by the Land Transport Authority. The licence permits commercial and non-commercial reuse and redistribution provided the attribution above is displayed and LTA endorsement is not implied — which is why the footer carries the notice.
The application code in this repo is released under the MIT License. The bus data itself remains subject to the Singapore Open Data Licence above.