Fleet state,first
Replacing a legacy telecom platform, one module at a time
Redesigning an enterprise connectivity platform
for 70,000+ SIMs, starting with the one module
everything else would inherit from.
Senior Product Designer · Enterprise IoT & Telecom · Web platform
- 4/ 8 beforetabs to read one SIM’s details
- 3/ 1 beforedistinct empty, loading and failed states
- 1/ 44 controlscomposable query bar replaces the advanced search form
- 0/ 5 searchesto see fleet state: it’s now the default view
Counts from my audit of the legacy system against the delivered design. Adoption and task-time metrics are pending post-launch instrumentation.
Built for a smaller,
slower world
The SIMs inside delivery trucks, payment terminals, meters and trackers. When one stops transmitting, something physical stops working. Operators had learned the legacy system’s quirks by memorising workarounds, and that competence hid the cost.
Every legacy screen on this page is a neutral wireframe recreation, redrawn to preserve the original's structure — column counts, tab counts and control counts — without reproducing the operator's branding or any real data.
No fleet-level state
The platform could show 25 rows, not your fleet. Every aggregate question meant an export to Excel.
Fleet state is the landing view
Clickable status cards above the table: reading the fleet and narrowing it become one gesture.
Purely reactive
Overages, roaming spikes and IMEI mismatches were discovered on the invoice.
Triage surfaced up front
A consistent AI insight banner names the risk, ranks it by consequence and offers one next step.
Two gears, no middle
One exact identifier, or a 44-control boolean builder. The everyday query fit neither.
A composable query bar
One bar across seven identifiers, inline filters underneath, Advanced Search one click away.
Raw data, not information
Bytes, not megabytes. Timestamps, not “6 days ago”. Secrets in plain text.
Translate, don’t transcribe
Every reading carries its quota, trend and recency. PIN/PUK masked behind an explicit reveal.
Broken edges
No way to tell “no data” from “failed to load”: a costly ambiguity in an incident.
Never render silence
Every empty, loading and failed state is designed, and every one says what to do next.
English-only, LTR-only
Arabic customer names were already in the data. The interface wasn’t built to hold them.
Arabic from the first frame
Real Arabic strings in every layout, with the RTL mirror specified alongside LTR.
Because operators had adapted, the platform’s problems read internally as training issues rather than design failures. Part of the job was making the failures visible again, with an audit of the live product rather than a competitor sweep.
Why SIM Management
went first
The brief was open: replace the platform outright, not reskin it. The risk in a brief that size is designing twelve modules shallowly instead of one properly. The first thing I argued for was sequencing.
Analytics dashboards
Inherits the KPI card, chart and empty-state library, and the persistent filter bar.
Sprint 2 · DeliveredAI layer
Insights banner, docked assistant and full-page assistant, with one behaviour model everywhere.
Sprint 3 · DeliveredInvoices & billing
Consumes the established table, drill-down and audit patterns. No new list-view inventions.
NextReporting, support & users
Customer and user management, reporting and support, built on the same shell and states.
NextSprint 1 · the pattern source
Three reasons it went first
-
Reason 1
It’s where the time goes
SIM lifecycle is the highest-traffic module by a wide margin. Fixing it delivers the most relief per unit of design effort.
-
Reason 2
It contains every hard pattern
Dense table, bulk destructive actions, deep drill-down, audit log, config rules, sensitive fields and search at two levels. Solve them here and later modules inherit solved problems.
-
Reason 3
It’s the credibility test
If the new platform handles 70,000 SIMs convincingly, stakeholders fund the rest. If it can’t, no dashboard will save it.
- Answer the question before it’s askedFleet state before rows. Deltas before totals. Risk before history.
- Two gears, and a middle oneEvery powerful capability needs a low-effort entry point.
- Translate, don’t transcribeBytes become megabytes with a quota. Timestamps become elapsed time.
- Never render silenceEvery empty, loading and failed state says what to do next.
- AI is infrastructureSame placement, same tone, same dismiss behaviour, everywhere.
SIM Overview,
rebuilt
A working slice of Sprint 1. Click a status card, search, select rows and act on them, open a SIM. Pick a walkthrough step or explore freely.
SIMs
Demo fleet · sample dataSample data. Rebuilt in the browser from the delivered Figma designs; the front-end team owns the production build.
Seven decisions that
carried the module
Each started as a specific failure in the legacy audit. The failure stays attached to the fix, because the fix on its own is just a screenshot.
Make fleet state the landing state, and make it clickable
- Problem
- The legacy overview opened onto rows. Answering “how is my fleet doing?” meant an export.
- Move
- A status card row above the table (All, Active, Suspended, Terminated, Inactive, Ordered), each with a today-delta, each a filter on the table below.
- Why
- Reading fleet state and narrowing to a subset become one gesture, which is how operators already think: “213 suspended, show me those 213.”
Also in this flow
Give search a middle gear
- Problem
- One exact-match box, or 44 controls with boolean operators. Nothing in between for the query operators ran most.
- Move
- One composable bar across IMSI, MSISDN, ICCID, business unit, cost center, service profile and customer label, with four inline filters beneath and Advanced Search one click away.
- Why
- Progressive disclosure sized to real frequency, not feature parity. The legacy builder’s power is kept; it’s just no longer the price of admission.
Make bulk operations safe before making them fast
- Problem
- Operators manage SIMs in the thousands. One at a time is untenable, and bulk without guardrails is worse.
- Move
- A contextual action bar on selection: Suspend, Change profile, Manage labels, Terminate. A persistent count, one-click Deselect all, destructive actions set apart in red.
- Why
- The dangerous moment isn’t the click, it’s the ambiguity before it: not knowing what “selected” means while a filter is active. The count does more safety work than the dialog.
Eight tabs to four, with a header that never moves
- Problem
- Eight tabs organised by database table, not by task. Switching lost context about which SIM you were on.
- Move
- Overview (is it healthy?), Configuration (what may it do?), Diagnostics (why is it misbehaving?), Activity (what happened?), under a persistent identity header with copy on every identifier.
- Why
- Support agents cross tabs while a customer is on the line. The header lets them read an ICCID aloud from Diagnostics without navigating back.
Translate the data instead of transcribing it
- Problem
- “Current Data Usage (Bytes): 1342976.” Technically accurate, operationally useless.
- Move
- 1.5 MB with a bar reading 1.5 / 10 MB and “last session 6 days ago”. Rules read as sentences: “When IMEI mismatch → Then suspend SIM.”
- Why
- A number is only information once it carries its own comparison. Quota, trend and recency turn a reading into a decision.
Also in this flow
Design the edges: empty, loading, failed
- Problem
- “No element selected/no data found” under a full table header. No way to tell a real zero from a failed fetch.
- Move
- Every terminal state is a designed component. No-results echoes the query and offers Advanced Search. Failure keeps the SIM’s identity header intact behind an inline error.
- Why
- With the header alive when the body fails, an operator can still copy the MSISDN into a ticket. The interface degrades into something useful, not a dead end.
Also in this flow
Make the audit log answer “who and how”, not just “what”
- Problem
- Changes arrive from people, API integrations and bulk file uploads at once. A log without provenance is close to useless in an incident.
- Move
- Origin (Manual, API, File) as a first-class, filterable column with its own icon, alongside status, type and date bounds. Recent activity previewed on the Overview tab.
- Why
- “Who changed this profile?” and “did our integration do this?” have different remediations. Origin is the cheapest way to separate them.
Every metric has
one owner
The dashboards weren’t under-designed, they were over-supplied: the same numbers in slightly different cuts across tabs, with no agreement on which was authoritative.
The same metric, four times
Business-unit cost on four tabs. Two widgets that were one query under two names.
Charts as blank axes
Grids and labels, then nothing. The legacy silence, inherited into a new product.
The signal was buried
A large “Unknown” slice dominated the status donut while Active barely registered.
Zero read as zero
“$0.00” looked the same whether it was real or a failed fetch.
A dashboard that shows the same number four ways doesn’t give you four insights. It gives you four chances to disagree with yourself. Two of the findings turned out to be data failures wearing a design costume; telling them apart quickly is most of a dashboard review.
Bundle Details was the clearest case. It always opened empty, which read as a broken empty state — the cause was a default filter set to “Consumed: 80%+” against a fleet sitting at 1–2%. I changed the default to “Any” and routed the underlying issue to the API owner, rather than redesigning a component that was working correctly.
AI that triages,
not decides
The instruction was “add AI”. The design problem was what AI may do in a system where a wrong bulk action cuts connectivity to a customer’s field equipment.
Grounded and scoped
Every answer cites live platform data and is limited to what the signed-in user can already see. Source chips and a sync time sit under each response.
Ranked by consequence
Not “47 SIMs are near their limit” but “12 will overage; here are the top 3 by projected cost.” Triage is the product.
It prepares, you approve
The assistant can draft a bulk suspend. It can’t run one. Every action lands in the normal confirmation flow, with a human as the last step.
The insights banner appears verbatim on SIM Overview, Details, Configuration and Diagnostics. That consistency isn’t tidiness: if AI is a banner in one module and a modal interrupt in the next, users stop trusting it, and an untrusted assistant is worse than none.
Built once,
used everywhere
I built on the client’s existing design system, using its tokens and variables, and added the few components the new cases needed. Sprints 2 and 3 consumed them straight away.
- #008C88Brand primary
- #E6F4F3Primary light
- #DD3356Destructive
- #10B981Success
- #1D252DText primary
- #8E9AA0Text muted
- #F9F9F9Card surface
- #F8FAFCTable header
Type scale: 18 / 16 / 14 / 13 / 11 / 10 px from page title to timestamp. Spacing on a 4–6–8–12–16–20 scale, 8px radius for buttons and cards. Display sizes are reserved for hero numbers, so density is never traded for scale.
Clickable KPI card
Status dot, label, number, today-delta. It doubles as a filter on whatever sits beneath it.
AI insights banner
Fixed anatomy, fixed placement, session-scoped dismissal. Identical on five surfaces.
Edges, designed
Empty, loading and failed each get their own component. Switch between them.
- Data tableStatus dot, copy icons on identifiers, icon actions column, sticky selection bar.
- ToolbarSort, sync time, export. Freshness lives here, not on KPI cards, so it’s never mistaken for a real zero.
- Filter barBusiness unit and bill cycle, persisted across dashboard tabs so a scoped question survives navigation.
Arabic and RTL, from the first frame
I designed with real Arabic customer names in place rather than Latin placeholders, and specified the RTL mirror for sidebar, filter bar and label alignment alongside LTR. Retrofitting RTL after a table system is built is a rewrite; specifying it during is a constraint.
Where it
landed
Three sprints designed and handed over in Q2 2026. Phase 1 is semi-live, awaiting final integrations. Reported here: delivered scope and audit counts, not production metrics.
Detail tabs
SIM Details went from eight tabs organised by database table to four organised by task.
Bundle lookup
Checking a bundle’s status went from nine steps across two nested search forms to four.
Search
A 44-control advanced search replaced, for everyday queries, by one composable bar plus clickable status filters.
Delivered in three sprints
-
Sprint 1
SIM Management
Overview, four-tab Details, Configuration, Diagnostics, Activity, plus loading, empty, no-result, error and collapsed-nav states. The module the rest inherits from.
-
Sprint 2
Dashboards
Overview, Customers, Cost, Usage & Bundle Health, Bundle Details and Tickets, re-cut so each metric has one owner and data gaps are surfaced as KPIs.
-
Sprint 3
AI layer
Insights banner, docked and full-page assistant, conversation history: one behaviour model applied on every surface.
What I’d carry
forward
Two things I now set up in week one of any platform rebuild.
-
01
Lock the IA before the pixels
On a platform this deep, navigation groupings drift as modules grow. A week-one card sort and one stakeholder decision cost far less than reconciling structures mid-build.
-
02
Set the measurement plan at kickoff
Capture task-time and export-frequency baselines on the legacy system while it’s still live. That window closes when the replacement ships, and it’s why this page reports audit counts rather than production metrics.
Thankyou
Let’s untangle something
complex
Dense operational platforms, legacy migrations and the teams behind them. That’s the work I like most.