eKiosk IKN
Two control rooms for Nusantara: kiosk content management and live bus tracking with arrival times that grade themselves

- control rooms: kiosk content and bus fleet
- 2
- between live bus position readings
- ~5 s
- arrival accuracy bands, by hour and stop
- 1/2/3 min
- pages across the two apps
- 24
On this page
The problem
IKN's smart city program puts information kiosks in public places across Nusantara. Each one runs an app that shows government content: maps, announcements and transport schedules. With no central control, changing what a kiosk shows meant touching that device. That doesn't scale as more kiosks go up. Operators also couldn't tell whether a kiosk was running, had crashed, or was showing old content.
IKN also runs a bus rapid transit fleet. An earlier in-house service tracked bus positions in memory only. It kept no history, and it couldn't tell riders or operators when the next bus would arrive. Two teams needed tools: content staff managing kiosk playlists, and transport staff watching the fleet.
What I built
Kiosks report in, and the server works out their state
Every kiosk checks in every five minutes with what it's playing, where it is, and how much storage and memory it has left. From that check-in, the server works out whether the kiosk is operational, stale, disconnected or in maintenance. Each playlist carries a fingerprint that changes when its content changes. A kiosk reports the fingerprint it last loaded, so the server knows at once whether that device is behind. Operators build playlists by dragging media into order, and changes reach the kiosks on their own.
Arrival times learned from the buses' own history
A background service reads every bus's GPS position about every five seconds and places it on its route. For arrival times, I didn't assume a fixed speed. Each night, a job studies past trips and learns how long each stretch between stops usually takes, by hour and by weekday or weekend. The estimate starts from where the bus is on the road, then adds those learned times stop by stop. Where history is thin, it falls back to the timetable, then distance, then stop count, so it always gives an answer.
The system grades its own predictions
I wanted to know whether the arrival times were any good, so the system checks itself. It samples its own predictions, then records the error the moment the bus reaches that stop. A report shows the average error and the share of arrivals within one, two or three minutes, broken down by hour and by stop. Staff can see exactly where the estimates are weakest.
Buses that change route are caught without flip-flopping
Buses sometimes get reassigned or leave their scheduled route. A separate check compares each bus's recent path with every route and suggests a correction when one route is clearly the best match. My first version switched a bus after a few readings near another route. At stops shared by overlapping routes, buses flipped back and forth. Now a switch only happens after the bus visits a stop unique to the new route, or at least 80% of that route's stops. Every correction is logged.
Two apps, one sign-in
What began as one dashboard with a fleet tab is now two separate apps, eKiosk CMS and Fleet Management, with a shared launcher and a switcher between them. Operators sign in once through IKN's single sign-on, and Fleet Management is limited to staff. Kiosks don't use that sign-in at all. They register with an ID assigned at first boot, since a kiosk has no person to log in as.
Result
Both apps run in production for IKN, across 24 pages. Content staff manage kiosk playlists and see device health, with trend charts and an email alert when a kiosk goes silent. Transport staff get a live bus map, a route and stop editor, fleet schedules, a journey planner with points of interest, and reports on headways, travel times and arrival accuracy. The daily fleet report used to time out under load, so I rewrote it as a single database query. The work ran from March to July 2026. The system tracks vehicles only; it has no passenger or fare data.
Under the hoodTechnical detail for engineers
- Kiosk status (OPERATIONAL / STALE / DISCONNECTED / MAINTENANCE) is a model
@propertyderived fromlast_heartbeat,heartbeat_interval,operational_statusand the playlist hash, so it can't be filtered in the ORM. The summary endpoint filters in Python. - Each playlist stores a deterministic SHA-256 hash of its ordered items, recomputed on save. Sync status is an O(1) comparison.
- Kiosk endpoints are
AllowAny; kiosks identify by a UUID assigned at first boot. Dashboard users sign in through Keycloak viamozilla-django-oidc, with a JWT held in Zustand and NIP, jabatan and direktorat synced intoUserProfileon each login. poll_gps_dataruns as its owntransport-pollerDocker service. Movement is bulk-inserted intoVehicleTrackabout every 60 seconds, only past a minimum distance.RouteGraphis a hand-rolled directed graph with a terminal sentinel that closes the loop. It avoids a 30 MB networkx dependency.compute_segment_statsruns nightly from host cron. It splits GPS history into trips, trims outliers above 3× the median, and stores median, p25 and p75 per (route, from-stop, to-stop, hour, weekday/weekend) bucket once at least three samples exist.- ETA projects the GPS fix onto the current polyline segment, then walks forward through
SegmentTravelStatbuckets. Fallbacks: timetable deltas, haversine distance over an assumed speed, stop count. EtaPredictionLogsamples at most once per vehicle and upcoming stop per 60 seconds, closed on geofence entry with signed error in seconds.route_detectorscores trails by perpendicular distance to the nearest segment, with a confidence margin over the runner-up and a spatial confirmation gate. Reverting needs several confirming ticks. Changes go toRouteCorrectionLog.- Fleet analytics is one PostgreSQL query using
LAG(), inline haversine andFILTERclauses, which replaced a Python loop that timed out gunicorn workers. - No pandas, numpy, geopy, shapely, networkx, Celery or Redis. Statistics use the stdlib
statisticsmodule. - Frontend: React 19 and Vite, TanStack Query polling (vehicles every 5 s, performance panels every 30 s, no websockets), React Leaflet, @dnd-kit, Recharts, and a hand-rolled inline-style design system with no component library.
- 4 Django apps, about 30 actions on
TransportViewSet, about 7,500 lines of Python and 17,200 of JSX, 113 backend and 133 frontend commits.
Screenshots
Hiring, or building something similar?
I'm open to full-time roles and select freelance work.