Render.com service (gunicorn, 1 worker / 6 threads)
Trip updates feed
TFNSW_GTFS_RT_URL
Vehicle positions feed
.../vehiclepos/buses
Static schedule
refreshed every 24h
app.py (live fetch)
called on every dashboard request and every 15s poll
collector.py (cron fetch)
GitHub Actions, every 5 min, external to Render
Locked cache (double-checked locking)
get_all_rows_cached / get_vehicles_cached
gtfs-r-scrape repo
one CSV row per vehicle, per day, external storage
Processing
join on trip_id, classify on time / late / early, aggregate by operator and route
/api/heatmap
grid cell aggregation, 20s deadline, reads the repo above
render()
builds the dashboard HTML page
/api/vehicles
same data, as JSON
Historical heatmap layer
rendered on the same page
Browser (dashboard page)
map, tables and heatmap, assembled from the three boxes above
polls every 15s