Case study 01 — G4S Indonesia
SARAS
The screen G4S operators keep open all shift: officers moving live on a map, alarms, incidents, chat and an in-browser softphone — all pushed over WebSockets.
Role
Frontend developer · team of 2
Period
2025 — 2026
Client
G4S Indonesia
Stack
Vue 3, TypeScript, Pinia, Leaflet, Laravel Echo, JsSIP, Web Audio API, Tailwind CSS
0
markers rebuilt on a location update
1×
camera fit on first live data, not on every tick
5
live feeds: alarms, incidents, chat, locations, voice
Context
G4S runs security operations rooms that watch over branches, alarm panels and field response teams across Indonesia. SARAS is the console operators keep open for the whole shift: a dashboard map, alarm and incident feeds, live chat, CCTV zones and voice calls to officers in the field. Almost everything on screen arrives over WebSockets through Laravel Echo.
I built most of the frontend, working alongside another developer, from the first dashboard slice to production.
The hard problem: a map that lagged under its own data
The dashboard map shows branch alarm locations and — for roles allowed to see it — field officers moving in real time. Every position event on the live-location channel updated a store, and the map component responded by rebuilding.
Each tick tore markers down and created new ones, constructed fresh icon objects (including animated pins with ripple elements), and re-ran fitBounds, which animates the camera. With a few officers reporting at once, the dashboard stuttered and the CPU stayed busy — on a screen that is supposed to stay open all day. Worse, the camera kept re-fitting itself, pulling the view away from whatever an operator was inspecting.
What I did
I rewrote the map component around one idea: treat incoming data as a diff, not a re-render.
- Markers keyed by ID. Live officer markers live in a record keyed by user ID. On an update, an existing marker only gets
setLatLng()if its coordinates actually changed. New IDs create markers, missing IDs are removed. Nothing is rebuilt for a position change. - Skip no-op updates. Reactive watchers fire even when the payload is identical. A cheap hash of
id:lat,lngpairs, compared with the previous one, short-circuits those before Leaflet is touched at all. - Create icons once. Default, alarm and officer icons are built once and reused, instead of per marker per tick.
- Canvas rendering for vector layers with
preferCanvas: true. - A calmer camera. Fitting the view is debounced by 300 ms and happens once, when the first live data arrives — not on every tick. Operators keep control of the map.
Live officer positions are also gated behind a dedicated permission, so the same component serves roles with and without access.
Also built
- In-browser softphone. SIP over WebSocket with JsSIP — register, call, hang up — held in a Pinia store so call state survives navigation.
- Mic level meter on the Web Audio API: an
AnalyserNodefeeding an RMS calculation with smoothing, updated every animation frame, so operators can see they’re being heard before they speak. - Channel lifecycle. Alarm and incident listeners leave the previous private channel before joining a new one when the user or incident changes, so subscriptions never stack up.
- CCTV zone management — starting and stopping streams, RTSP and IP configuration — on top of the HLS pipeline my teammate built.
Outcome
Position updates now cost a coordinate change instead of a rebuild, and the map stays smooth with live officers on screen. The pattern — diff the data, touch the DOM only where something changed — is the one I reach for in every real-time view since.
Next case study
SIROKUM