Call Center Dashboard started as a front-end mockup — navigation, report pages, filter forms, results tables — before a single row of real data existed anywhere. By the time it was done, it was a live analytics platform running on Azure SQL and Azure Functions, serving 61 reports against a database holding more than 41 million rows. This is the story of how it got there.
Starting With the Experience, Not the Data
I built the React dashboard first, designed around how a contact center actually works, before wiring up a single live data source. Only once that experience felt right did I move to the data side: an Azure SQL database modeled as a star schema, loaded with millions of rows of structured test data through a Blob Storage bulk-import pipeline. Building the front end first meant every technical decision that followed had a real interface to serve, not a guess at one.
One Report at a Time
The API layer started with a single connectivity check and one live report — Agent Call STAT 15. Once that proved the full path from browser to database and back, I turned its pattern into shared query builders and extended it across every report family and time cadence: 15-minute, 30-minute, hourly, daily, weekly, monthly, and month-to-date. That one proven pattern, repeated dozens of times, is most of how a project this size ends up with 62 backend functions instead of collapsing under its own weight.
// Every report function returns the same shape
type ReportResponse = {
rows: Record<string, unknown>[]; // capped at 50,000
totalMatched: number;
capped: boolean;
};A pattern that works once isn't done proving itself until you've repeated it under different conditions. The real payoff of that first report wasn't the report itself — it was every report after it that took an hour instead of a day.
Choosing Azure Functions Over App Service
My original plan was Azure App Service. On a new subscription, though, App Service depends on virtual-machine quotas that start at zero, and every quota increase I requested uncovered another limit behind it. I compared the options on cost, setup effort, and fit instead of pushing through: an App Service plan bills around the clock regardless of traffic, while Azure Functions on the Flex Consumption plan bill per execution with a monthly free grant — a much better match for a dashboard used in bursts during business hours, not around the clock.
“The best infrastructure decision isn't always the one you planned for. It's the one that actually fits how the thing gets used.”
A Star Schema Built for 41 Million Rows
The database follows a star schema: 11 dimension tables for the organizational lookups — agents, supervisors, VPs, sites, vendors, queues — and 17 fact tables for the measurements themselves. Most fact tables carry a 15-minute interval, the agent and reporting-line identifiers, and the metrics for that window, which is what lets the same report run at 15-minute granularity or roll all the way up to a month against the same underlying design. The organization modeled in the data is 3 VPs, 15 supervisors, and 150 agents across four shifts — big enough to make the reports mean something, small enough to keep the whole thing legible.
Loading Millions of Rows Without Breaking Anything
Inserting that volume of data row by row through an application is slow and fragile, so I built a file-based pipeline instead: generate the data as CSV, upload it to a private Blob Storage container, and let SQL Server bulk-import it directly from storage. For the call statistics table specifically, I load through a staging table first — same columns as the live table, but no primary key or constraints, so a bad row can't fail an import part-way through. I validate row counts, date ranges, and dimension values in staging, merge the clean rows into the live table in one set-based insert, then truncate staging for the next load. Reports never see a partially loaded batch, and a bad file gets corrected and reloaded without ever touching production data.
Designing for a Database That Sleeps
Azure SQL Database's serverless tier auto-pauses after an hour of inactivity, which is exactly the kind of detail that turns into a support ticket if you don't design for it. The first request after an idle hour has to wait for the database to resume, so I centralized every database call through a shared connection helper that validates the connection and retries automatically on failure. A resume delay gets absorbed instead of surfacing as an error, and the Live Wallboard refreshes on demand instead of polling, which keeps both cost and database load proportional to how often anyone's actually looking.
Serverless compute gets talked about mostly as a cost story. The bigger win, in practice, is that it forces you to design for the failure modes — cold starts, paused databases — that a comfortable always-on server would let you ignore.
The Live Wallboard
Every report on the dashboard answers a question about the past. The Live Wallboard answers one about right now: who's scheduled, who's logged in, and whether each agent is Available, on a call, in after-call work, or in Aux, refreshed on demand rather than on a timer. It's powered by a single function that determines the active shift from a fixed schedule, reads the roster, and finds each agent's most recent state-change event since that shift began — an agent with no activity since the shift started simply counts as logged out. It's the one screen on the whole platform built to be glanced at, not read.
What It Adds Up To
The finished platform is a React dashboard on Cloudflare talking to 62 Azure Functions, which query an Azure SQL star schema holding more than 41 million rows across 28 tables. Most of the reports run on live data today. None of that came from one big architectural decision — it came from proving one report end to end, then repeating the pattern until it covered everything a call center actually needs to see, in real time and in hindsight both.


