Microsoft 365 Service Health Dashboard: monitoring M365 services in real time
How I built a web dashboard with Azure Functions, Microsoft Graph API and Azure Static Web Apps to monitor the status of Microsoft 365 services and active incidents, with Entra ID authentication via Easy Auth.
In this article13sections
- The real problem
- Why not use the existing alternatives
- The solution architecture
- Why PowerShell and not Node.js or C#
- Why HTML/CSS/JS and not React or other services
- Graph API data: richer than it looks
- The dashboard: design choices with an operational purpose
- Authentication: Easy Auth with Entra ID
- Known limitations and deliberate choices
- What I learned
- Roadmap
- Tech stack
- Final thoughts
How I built a dashboard to monitor the health of Microsoft 365 services
As an M365 sysadmin, one of the most frustrating situations is discovering a service problem after users have already reported it to you. Exchange Online slows down, Teams won’t load files, SharePoint throws errors: the first reflex is to open the Microsoft 365 Admin Center, navigate to Service Health and wait for the page to load. If you’re lucky, there’s already an incident open. If you’re not, you start investigating on your own and lose precious time.
This cycle convinced me it was worth building something better: a dedicated dashboard, always open, showing the status of M365 services in real time without having to open the portal every time.
The real problem
Service Health in the Admin Center is functional, but it isn’t designed for continuous monitoring. It’s buried among various menu items, doesn’t refresh automatically and doesn’t give you an immediate overall picture.
But the problem isn’t just speed — it’s context. When a user opens a ticket because “Outlook isn’t working”, the sysadmin needs to work out quickly whether it’s a local problem, a misconfiguration or an incident on Microsoft’s side. Without an immediate view of service status, you always start from the worst-case assumption: you investigate as if it were an internal problem, you pull in colleagues, you open remote sessions. Then, twenty minutes later, you find out Microsoft has published an advisory on Exchange Online that explains everything. Twenty minutes wasted, multiplied by every ticket of the same kind.
What I wanted was simple: a web page, always open on a secondary monitor or in a pinned tab, that would show me at a glance whether something was going wrong. With the details of active incidents, Microsoft’s updates in real time, and the ability to filter by service or classification.
Why not use the existing alternatives
Before building something from scratch, I evaluated the options already available.
The Admin Center is the official source but, as I said, it isn’t designed for passive monitoring. It requires a login and navigation, and it doesn’t update in real time. You can’t keep it open on a monitor and expect to spot a change at a glance.
Service Health notification emails do arrive, but with a variable delay, and they end up in the noise of the inbox. In an environment where you get dozens of emails a day from portals, ticketing systems and security alerts, a Service Health email gets lost easily.
Third-party solutions (StatusPage aggregators, SaaS monitoring tools) exist, but they introduce an external dependency, often a paid one, and require you to share credentials or tokens with an external service. For an internal operational monitoring tool, I prefer to keep everything within the Azure/M365 perimeter.
The dashboard I built doesn’t replace any of these tools — it complements them. It’s the always-on monitor that tells you “everything is fine right now” or “heads up, there’s been an incident on Exchange for 6 days”. A piece of information that changes how you approach the day.
The solution architecture
Microsoft Graph API exposes two specific endpoints for this purpose: one for the overall status of services (/admin/serviceAnnouncement/healthOverviews) and one for active incidents and advisories (/admin/serviceAnnouncement/issues). All I needed was a way to query them periodically and present the data in a readable form.
I chose a serverless architecture on Azure, with three main components.
Azure Function App (PowerShell) — the heart of the solution. An HTTP-triggered Function that, on every call, obtains an OAuth2 token via the client credentials flow and queries the two Graph endpoints in sequence, returning the combined data as JSON. I used a Function as a proxy, rather than calling Graph directly from the browser, for two reasons: to keep the app credentials out of the frontend code, and to have a single place to handle authentication and call logic.
$tokenBody = @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSecret
scope = "https://graph.microsoft.com/.default"
}
$tokenResponse = Invoke-RestMethod `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-Method POST `
-Body $tokenBody
$health = (Invoke-RestMethod `
-Uri "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews" `
-Headers @{ Authorization = "Bearer $($tokenResponse.access_token)" }).value
Azure Static Web App — hosts the frontend, a single HTML/CSS/JS file with no external dependencies and no build step. I chose not to use a framework to keep the solution as simple as possible to maintain: no npm, no bundles, nothing to update. The Static Web App deploys automatically from GitHub on every push to the main branch.
Microsoft Entra ID (two App Registrations) — one for app-only access to Graph (client credentials, no interactive user), one for authenticating users to the dashboard via Easy Auth.
Separating the two App Registrations is no accident. The Graph App Registration has Application-level permissions (ServiceHealth.Read.All, ServiceMessage.Read.All) that must never be exposed or mixed up with the user context. The Auth App Registration only handles sign-in and has no Graph permissions. This separation is a security best practice: if one day I had to revoke or rotate the credentials of one of them, the other would keep working.
Why PowerShell and not Node.js or C#
Choosing PowerShell as the Function runtime deserves an explanation. For an M365 sysadmin, PowerShell is the natural language: it’s the same one you use to manage Exchange Online, SharePoint, Entra ID and the rest of the Microsoft stack. Writing the Function in PowerShell means any sysadmin on the team can read, understand and modify the code without having to learn a different language. In an operational context, maintainability matters more than performance — and for a proxy that makes two REST calls, PowerShell’s performance is more than adequate.
Why HTML/CSS/JS and not React or other services
Same reasoning: simplicity and maintainability. A single HTML file with no external dependencies means zero build steps, zero npm vulnerabilities to patch, zero framework breaking changes. You open it, you read it, you change it. For an internal tool used by a small technical team, it’s the right choice. If it had to scale to dozens of views and features, I’d reconsider.
Graph API data: richer than it looks
One of the things that surprised me is the depth of the data returned by the Graph API Service Health endpoints.
The healthOverviews endpoint doesn’t just return a green/red traffic light per service. Each entry includes a detailed status with values that distinguish between serviceDegradation, serviceInterruption, serviceOutage, restoringService and falsePositive — each with a different operational meaning. In the dashboard, I mapped these states to coloured badges and, for critical states, to an animated dot that immediately catches your peripheral vision.
The issues endpoint is even richer. Each incident includes:
- Title and ID — for tracking and correlation with internal tickets
- Classification —
incident(outage with direct impact) vsadvisory(partial degradation, limited impact). This distinction is crucial: an advisory on a secondary SharePoint feature doesn’t call for the same reaction as an incident on Exchange Online - startDateTime and lastModifiedDateTime — let you work out how long the incident has been open and how recent Microsoft’s latest update is
- Service, feature and featureGroup — a level of granularity the Admin Center does show, but which here arrives structured as JSON, ready to be filtered
- List of posts/updates — the full timeline with every update published by Microsoft, in chronological order. Each post includes a timestamp and content (in HTML, which the dashboard converts into readable text)
Combined in a single view, this data gives the sysadmin a complete picture that in the Admin Center takes more clicks and more pages.
The dashboard: design choices with an operational purpose
The frontend isn’t an aesthetic exercise — every element is designed to answer a specific operational question.
The summary bar at the top answers the most urgent question: “is something wrong, right now?” Four counters — total services, operational, degraded, open incidents — visible without scrolling and readable from three metres away on a secondary monitor.
The services grid is sorted by criticality: degraded services appear at the top, operational ones at the bottom. You don’t have to scroll through 40 green cards to find the one yellow one. The ordering reflects operational priority: whatever needs attention is shown first.
The incident age bar under each card is a visual indicator that answers an often underrated question: “how long has this incident been open?” An incident open for 2 days and one open for 12 call for different responses — the second probably needs a workaround, a communication to users or an escalation. The bar uses a progressive colour scale:
| Threshold | Colour | Operational meaning |
|---|---|---|
| ≤ 3 days | 🟢 Green | Recent incident, Microsoft is probably still investigating |
| 4–7 days | 🟡 Yellow | Ongoing for a few days, consider notifying users |
| 8–14 days | 🟠 Orange | Attention — consider a workaround or escalation |
| > 14 days | 🔴 Red | Critical due to duration — action required |
The update timeline inside each incident shows Microsoft’s posts in reverse chronological order (most recent at the top). Long posts are truncated to 3 lines with a “Show all” link to expand them. This stops a single incident with 15 updates from taking up the whole viewport.
The filters (search, service, classification) exist for the scenarios where you already have a hunch: “the problem the user reported is about Teams — are there active incidents on Teams?” A quick filter answers in a second.
Dark/Light mode isn’t a whim: anyone who keeps the dashboard open on a second monitor for hours appreciates being able to turn the brightness down. The preference is saved in localStorage and persists across sessions.
Authentication: Easy Auth with Entra ID
Initially the dashboard was public — accessible to anyone who had the URL. Acceptable during development, but not in production. So I added an authentication layer through Azure Static Web Apps Easy Auth, which natively integrates Microsoft Entra ID without requiring any changes to the frontend code.
It works transparently: anyone who opens the dashboard without an active session is automatically redirected to the Microsoft sign-in page. After authenticating, they’re sent back to the dashboard. I didn’t write a single line of code to handle this flow — it’s all configured through a staticwebapp.config.json file in the root of the repo:
{
"routes": [
{
"route": "/*",
"allowedRoles": ["authenticated"]
}
],
"responseOverrides": {
"401": {
"redirect": "/.auth/login/aad",
"statusCode": 302
}
}
}
The routes section blocks access to every path for unauthenticated users. responseOverrides turns the 401 into an automatic redirect to the sign-in page, instead of showing an error page. The result is a clean flow: you open the dashboard, if you don’t have an active session you go to the Microsoft sign-in, and you come back to the dashboard authenticated.
The whole setup requires a second App Registration in Entra ID (separate from the one that accesses Graph) and a few minutes of configuration in the Static Web App portal. No changes to the HTML.
An important detail: on first access, Entra ID shows a consent screen for the basic profile permissions. If a Global Admin or Privileged Role Admin ticks “Consent on behalf of your organization” during that first sign-in, consent is granted tenant-wide and subsequent users will never see the screen. A small step that significantly improves the user experience.
Known limitations and deliberate choices
No solution is perfect, and I prefer to be transparent about what this dashboard doesn’t do and where it could be improved.
No caching. Every page load generates two Graph API calls (one for healthOverviews, one for issues). For individual use or a small team this is irrelevant, but in scenarios with dozens of concurrent users it could cause throttling. A caching layer with a TTL of a few minutes (Azure Table Storage or Redis) would solve the problem.
Cold start on the Consumption plan. The first invocation after a period of inactivity has a delay of a few seconds. For a monitoring tool this isn’t critical — it isn’t a production API with a latency SLA — but it’s noticeable. Anyone who wants to eliminate it can move to an App Service plan with Always On enabled.
No history. The dashboard only shows the current state. Resolved incidents disappear. For a real-time monitoring tool that’s the right choice, but if you needed history for post-mortem analysis or reports, you’d need a persistence layer.
What I learned
Building this dashboard confirmed a few things I already suspected.
Graph API for Service Health is surprisingly rich: the endpoints return not only the status of services, but also the full timeline of updates, the impacted workloads, the incident classification and much more. Data that the Admin Center shows in fragments all arrives here in a single JSON call.
Azure Functions on the Consumption plan are perfect for this kind of lightweight proxy: practically zero cost, zero infrastructure maintenance, and a cold start of a few seconds on first run is perfectly acceptable for an internal monitoring tool.
Easy Auth is one of those Azure features that solves a real problem with disarming simplicity. Adding Microsoft authentication to a Static Web App literally takes one App Registration, one configuration entry in the portal and one JSON file in the repo. For internal tools — where you don’t need granular role management or complex authorisation flows — it’s the right choice.
Architectural simplicity pays off. No frontend framework, no database, no containers, no Kubernetes. Three managed Azure services, an HTML file and a PowerShell script. The solution was up and running in less than an afternoon, and since then it hasn’t needed any maintenance apart from periodic rotation of the client secrets. In an operational context where time is the scarcest resource, a solution that works and doesn’t ask for attention is the best possible outcome.
Roadmap
- ✅ User authentication via Entra ID (Easy Auth on Static Web App)
- 🔲 Caching with configurable TTL (Azure Table Storage)
- 🔲 History of resolved incidents (last N days)
- 🔲 Automatic notifications via Teams Webhook on new incidents
- 🔲 PDF/CSV export of the current status
- 🔲 Multi-tenant support
Tech stack
Built with: Azure Functions · Microsoft Graph API · Azure Static Web Apps · Microsoft Entra ID · PowerShell · HTML/CSS/JS
Final thoughts
The dashboard is in daily use. I keep it open on a secondary monitor and within thirty seconds I know whether something is wrong with Microsoft’s services — before user reports start coming in. For anyone managing M365 environments with a significant number of users, having this immediate visibility changes how you react to problems: no longer reactive, but informed. It’s not the same as being proactive — you don’t prevent the problem — but you know it exists before someone else tells you. And in an operational context, that makes all the difference.
The architecture is deliberately simple and repeatable: two App Registrations, a Function App, a Static Web App. Nothing exotic, all managed Azure services. If you manage an M365 tenant and want to stop finding out about problems from user tickets, this dashboard is a good starting point.