Sendy Firmansyah.
← All work

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,lng pairs, 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 AnalyserNode feeding 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