Skip to content

Latest commit

 

History

102 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Microsoft Communications Portal

Disclaimer: This is a sample/reference implementation provided "AS IS" without warranty. It is not an official Microsoft product or service and is not supported by Microsoft Support. See NOTICE.md for full details.

License: MIT Open in GitHub Codespaces

About

This project grew out of a recurring request from customers: they wanted a single pane of glass to see all the Microsoft updates that affect them — Power Platform release plans, the Microsoft 365 roadmap, Azure announcements, tenant-specific Message Center posts, and Service Health incidents — without bouncing between half a dozen portals, RSS feeds, and admin centers. Something they could deploy internally on their own infrastructure or host in Azure. The Microsoft Communications Portal brings those streams together into one filterable, dark-mode-friendly UI backed by a small Node.js server that proxies the upstream APIs and handles Microsoft Graph authentication.

If you wire up the Azure Management API as well, this same portal can surface Azure Service Health and Azure Resource Health events alongside the Microsoft 365 data, giving you a complete view of platform-side communications and tenant-side incidents in one place.

Pages

Page Route Source file Backend
Home /home home.html — (dashboard/landing page)
Power Platform Release Planner /powerplatform powerplatform.html releaseplans.microsoft.com (proxied)
Regional Release Plans /featuregeo featuregeo.html releaseplans.microsoft.com (scraped + normalised)
Microsoft 365 Roadmap /m365updates m365updates.html M365 Roadmap RSS
Azure Updates /azureupdates azureupdates.html Azure Updates RSS
Microsoft Fabric Roadmap /fabricroadmap fabricroadmap.html roadmap.fabric.microsoft.com (proxied)
Microsoft 365 Message Center /messagecenter messagecenter.html Microsoft Graph
Microsoft 365 Service Health /servicehealth servicehealth.html Microsoft Graph
Azure Service Health /azureservicehealth azureservicehealth.html Azure Management API (ARM)
Guided Report /guidedreport guidedreport.html — (multi-source report wizard)

Every page supports light and dark themes. Pass ?clawpilotTheme=light or ?clawpilotTheme=dark on the URL, or click the theme toggle in the header.

The site root (/) redirects to /home.

Quick Deploy

Seven of ten pages work with zero credentials — you can be up and running in under a minute. The remaining pages need extra setup, and each is optional:

Pages What you need Who should care
Home, Power Platform Release Planner, M365 Roadmap, Azure Updates, Fabric Roadmap, Guided Report Nothing — works immediately Everyone
Message Center, Service Health An Entra app registration that can read your tenant's Microsoft Graph data (see Setup step 2) IT admins who want tenant-specific M365 announcements and incident reports
Azure Service Health The same Entra credentials plus an Azure role assignment (see Setup step 3) Teams that also monitor Azure subscription-level health events
AI Insights (available on seven feed pages) An API key from Azure OpenAI, OpenAI, or GitHub Models (see AI summarization) Anyone who wants AI-generated summaries and "top 5 most impactful changes" digests

Not sure where to start? Pick the deployment option below that matches your situation. You can always add Graph credentials, Azure Service Health, or AI later.

Option 1 — Azure Developer CLI (recommended for Azure users)

What this does: Deploys the portal as an Azure App Service (a managed web server in your Azure subscription) with a single command. The infrastructure (server, networking, etc.) is created automatically from the included Bicep templates.

Best for: Teams that want a shared, always-on instance hosted in Azure without managing servers manually.

Prerequisites:

  • Azure Developer CLI (azd) — a command-line tool for deploying apps to Azure
  • PowerShell 7+ (pwsh) — needed for the post-deploy script that sets up Graph permissions
  • An Azure subscription with permission to create resources
azd init --template russrimm/MicrosoftCommunicationsPortal
azd up

After azd up finishes, the post-provision hook will offer to set up Graph permissions for Message Center and Service Health automatically.

Enable Entra ID authentication for shared deployments: Create or reuse a single-tenant Entra app registration, copy its Application (client) ID, and set it before deployment:

azd env set AUTH_CLIENT_ID <your-entra-app-client-id>
azd env set AUTH_CLIENT_SECRET <your-entra-app-client-secret>   # recommended
azd up

The post-provision hook reuses that app and registers <app-url>/.auth/login/aad/callback. Easy Auth then redirects unauthenticated visitors to organizational sign-in. If AUTH_CLIENT_ID is omitted, the seven public pages remain available, but tenant-specific Graph/ARM and billed AI API routes reject requests because the application refuses to trust Easy Auth headers unless App Service reports that authentication is enabled.

AUTH_CLIENT_SECRET is optional but recommended. When set, the deployment stores it in the MICROSOFT_PROVIDER_AUTHENTICATION_SECRET app setting and Easy Auth acts as a confidential client using the hybrid flow (response_type=code id_token), exchanging the authorization code server side. Without it, Easy Auth falls back to the implicit flow (response_type=id_token), which returns the ID token directly in the redirect.

Production Easy Auth: Supplying AUTH_CLIENT_SECRET covers the confidential-client requirement, but a client secret still expires and has to be rotated. For production, Microsoft recommends a user-assigned managed-identity federated credential instead; see Use a managed identity instead of a secret.

Option 2 — Azure Static Web Apps + linked App Service backend

What this does: Serves the HTML, CSS, and JavaScript from Azure Static Web Apps' global edge network, and proxies every /api/* request to the same server.js App Service that Option 1 deploys. The App Service keeps its managed identity, so Microsoft Graph and Azure Resource Manager calls are unchanged.

Best for: Teams that want edge-cached static assets and Static Web Apps' built-in Microsoft Entra ID sign-in in front of the existing API.

Browser ──> Static Web Apps (Standard)
             ├── static content + managed Entra ID auth
             └── /api/*  ──proxy──> linked App Service (server.js)
                                     └── managed identity ──> Graph + ARM

Prerequisites:

  • Everything from Option 1 — this option adds a Static Web App in front of the App Service, it does not replace it.
  • The Standard plan. Linked backends are not available on the Free plan.
  • An Entra app registration with a client secret, because Static Web Apps' custom Entra ID provider does not support managed identity.
azd env set DEPLOY_STATIC_WEB_APP true
azd env set STATIC_WEB_APP_LOCATION eastus2   # or westus2, centralus, westeurope, eastasia
azd env set AUTH_CLIENT_ID <your-entra-app-client-id>
azd env set AUTH_CLIENT_SECRET <your-entra-app-client-secret>
azd up

Provisioning creates the Static Web App, links the App Service as its /api backend, and sets AUTH_MODE=swa on the App Service so it trusts the x-ms-client-principal header that Static Web Apps forwards.

Then set up content deployment. Static content is published by the azure-static-web-apps.yml workflow. Grab the deployment token and store it as a repository secret:

az staticwebapp secrets list --name <static-web-app-name> \
  --query "properties.apiKey" -o tsv
  • Repository secret AZURE_STATIC_WEB_APPS_API_TOKEN — the token above.
  • Repository variable ENTRA_TENANT_ID — your tenant ID. Without it the build drops the custom Entra registration and falls back to the preconfigured multi-tenant provider.

To preview the payload locally:

npm run build:swa    # writes dist/
npx @azure/static-web-apps-cli start dist --swa-config-location dist \
  --api-devserver-url http://127.0.0.1:3000

Why a build step? server.js injects a per-request CSP nonce into every inline <script>. Static Web Apps serves HTML straight from storage, so no nonce can be generated. scripts/build-swa.js replaces it with build-time sha256 CSP hashes, which keeps the policy strict without unsafe-inline.

Linking security: Linking the backend automatically adds an App Service identity provider named Azure Static Web Apps (Linked) that rejects traffic not proxied through the Static Web App. Unlinking does not remove it, so the App Service is never left anonymously exposed. Because of this, the bicep template skips its own Easy Auth configuration when DEPLOY_STATIC_WEB_APP is true — otherwise a redeploy would overwrite the linked provider and break the connection.

Pull request previews are not supported. Static Web Apps does not attach linked backends to preview environments, so the workflow deploys only on push to main and on manual dispatch.

Option 3 — Docker (recommended for quick trials or on-prem)

What this does: Runs the portal in a Docker container — a lightweight, self-contained package that includes everything the app needs. No need to install Node.js or other tools on your machine.

Best for: Trying the portal quickly, running on an existing Docker host, or deploying on-premises where Azure isn't available.

Prerequisite: Docker Desktop (Windows/Mac) or Docker Engine (Linux).

Pull and run:

docker run -p 127.0.0.1:3000:3000 ghcr.io/russrimm/microsoftcommunicationsportal:latest

Or build from source:

docker build -t mcp .
docker run -p 127.0.0.1:3000:3000 mcp

Open http://localhost:3000. Seven pages work immediately — no credentials needed.

Security note: The examples above bind to 127.0.0.1 (loopback), so only your local machine can reach the portal, and AUTH_MODE is auto-inferred as none-loopback-only. Using -p 3000:3000 (or any non-loopback publish) instead exposes the container on all network interfaces — the server will refuse to start unless you also set AUTH_MODE=reverse-proxy (or easyauth) plus API_AUTH_TOKEN and ALLOW_REMOTE_BIND=true. See Authentication modes for the full matrix.

For Graph-backed pages (Message Center, Service Health), pass credentials as environment variables:

docker run -p 127.0.0.1:3000:3000 \
  -e AZURE_TENANT_ID=your-tenant-id \
  -e AZURE_CLIENT_ID=your-client-id \
  -e AZURE_CLIENT_SECRET=your-client-secret \
  mcp

Where do these values come from? See Setup step 2 below — it walks you through creating the Entra app registration that produces these three values.

Option 4 — Run locally (best for development and testing)

What this does: Clones the source code and runs the portal directly on your machine using Node.js. You can edit the code and see changes immediately.

Best for: Developers who want to customize the portal, contribute code, or just kick the tires without Docker or Azure.

Prerequisites:

Prerequisite Version Check Install
Node.js (LTS) 24.x or later node -v nodejs.org/download
npm Bundled with Node npm -v Comes with Node.js
Git Any recent git --version git-scm.com
PowerShell 7+ (optional) 7.x pwsh -v Install PowerShell
Azure CLI (optional) Any recent az --version Install Azure CLI

Node.js note: The server uses the built-in http, https, crypto, and URL APIs with no native add-ons, so any Node 24 LTS build (x64, ARM64, etc.) works. Earlier versions (18–22) may work but are not tested.

Optional tools: PowerShell 7+ and Azure CLI are only needed if you want to run scripts/create-entra-app.ps1 to set up Graph API credentials for the Message Center and Service Health pages.

Clone and run:

git clone https://github.com/russrimm/MicrosoftCommunicationsPortal.git
cd MicrosoftCommunicationsPortal
npm install
npm start            # production mode
# or
npm run dev           # watch mode — auto-restarts on file changes

Open http://localhost:3000. Seven of ten pages work immediately with no credentials.

For Graph-backed pages (Message Center, Service Health), copy .env.example to .env and fill in your Entra app credentials — or run pwsh scripts/create-entra-app.ps1 to automate it.

Screenshots

Nine of the ten pages are shown in light and dark mode below (18 screenshots). The Feature Geography page does not yet have checked-in screenshots. Regenerate with node scripts/capture-screenshots.js (Playwright) while the server is running on http://localhost:3000. The script visits each route with both theme query strings, waits for any visible "Loading…" banner to clear (up to 45 s, since the Power Platform page fans out ~20 upstream calls on a cold cache), and writes 1440×900 × 2 DPR PNGs to screenshots/.

Home

Light Dark
Home — light Home — dark

Power Platform Release Planner

Light Dark
Power Platform Release Planner — light Power Platform Release Planner — dark

Microsoft 365 Roadmap

Light Dark
Microsoft 365 Roadmap — light Microsoft 365 Roadmap — dark

Azure Updates

Light Dark
Azure Updates — light Azure Updates — dark

Microsoft Fabric Roadmap

Light Dark
Microsoft Fabric Roadmap — light Microsoft Fabric Roadmap — dark

Microsoft 365 Message Center

Light Dark
Microsoft 365 Message Center — light Microsoft 365 Message Center — dark

Microsoft 365 Service Health

Light Dark
Microsoft 365 Service Health — light Microsoft 365 Service Health — dark

Azure Service Health

Light Dark
Azure Service Health — light Azure Service Health — dark

Guided Report

Light Dark
Guided Report — light Guided Report — dark

Setup

For a faster setup, see Quick Deploy above.

Step 1 — Install dependencies

npm install

This downloads the one runtime dependency (dotenv) and is required before the server can start.

Step 2 — Configure Microsoft Graph authentication

(Required only for the Message Center, Service Health, and Azure Service Health pages. Skip this if you only need the seven public pages.)

The Message Center and Service Health pages pull data from Microsoft Graph — Microsoft's API for reading your tenant's admin announcements and service incidents. To access Graph, the portal needs credentials that prove it has permission to read that data.

Which option should I pick?

Option Best for Pros Cons
A — Managed identity Portal hosted on Azure (App Service, Container Apps, VM) No secrets to manage or rotate; most secure; the Azure platform handles tokens automatically Only works when running on Azure infrastructure; requires CLI commands to grant Graph permissions (can't use the portal UI)
B — Automated script Local development, or non-Azure servers One command does everything; writes credentials to .env automatically; idempotent (safe to re-run) Requires PowerShell 7+ and Azure CLI installed locally; needs an admin account that can grant tenant-wide consent
C — Manual portal UI When you can't run scripts, or prefer clicking through the Azure portal No tools needed beyond a web browser; good for learning how app registrations work More steps; easy to miss a step; you must copy/paste secrets carefully

What is an "app registration"? It's an identity record in Microsoft Entra ID (formerly Azure AD) that represents this portal. Think of it as a username and password for the app itself (not for any human user). The portal uses it to call Microsoft Graph on behalf of your organization, without any user needing to sign in.

What is "admin consent"? Some Graph permissions are sensitive enough that a tenant administrator must explicitly approve them. The two permissions this portal needs (ServiceMessage.Read.All and ServiceHealth.Read.All) both require admin consent — meaning a Global Administrator, Privileged Role Administrator, or Cloud Application Administrator must click "Grant" before the portal can read data.


Option A — Managed identity (recommended for Azure deployments)

What is a managed identity? When you host an app on Azure, the platform can automatically give it an identity (like a built-in service account) so it can call other Microsoft services without you having to create or store any passwords. Azure handles the credential lifecycle — no secrets to rotate or leak.

If the portal runs on Azure (App Service, Container Apps, or a VM), assign a system- or user-assigned managed identity to the host and grant it two Graph application permissions with admin consent. Then set one variable in .env:

USE_MANAGED_IDENTITY=true

System-assigned vs. user-assigned identity:

  • System-assigned (simpler): Azure creates the identity automatically when you enable it on the App Service. It's tied to that specific resource and deleted if the resource is deleted. No extra configuration needed.
  • User-assigned (more flexible): You create the identity separately and attach it to one or more resources. Useful if you want to share one identity across multiple apps. If you use this type, also add its client ID to .env:
    AZURE_MI_CLIENT_ID=<managed-identity-client-id>
    

No client secret is needed — the platform issues the token automatically. On App Service / Container Apps the IDENTITY_ENDPOINT and IDENTITY_HEADER environment variables are set by the platform. On a VM the Azure IMDS endpoint is used.

azd up users: the post-provision hook runs scripts/create-entra-app.ps1 which automatically grants these Graph permissions to the App Service managed identity. The Bicep deployment also assigns the identity the Azure Reader role and configures the deployment subscription as the default Azure Service Health scope, so no separate Resource Health setup is needed.

Granting Graph permissions to a managed identity manually

Graph application permissions cannot be added through the portal UI for managed identities — you must use the CLI or Microsoft Graph API. Run these commands as a Global Administrator (or Privileged Role Administrator):

# 1. Get the managed identity's object ID
$miObjectId = az webapp identity show `
    --name <app-service-name> -g <resource-group> `
    --query principalId -o tsv

# 2. Get the Microsoft Graph service principal's object ID in your tenant
$graphSpId = az ad sp show `
    --id "00000003-0000-0000-c000-000000000000" `
    --query id -o tsv

# 3. Grant ServiceMessage.Read.All (Message Center)
az rest --method POST `
    --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$graphSpId/appRoleAssignments" `
    --body "{`"principalId`":`"$miObjectId`",`"resourceId`":`"$graphSpId`",`"appRoleId`":`"1b620472-6534-4fe6-9df2-4680e8aa28ec`"}"

# 4. Grant ServiceHealth.Read.All (Service Health)
az rest --method POST `
    --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$graphSpId/appRoleAssignments" `
    --body "{`"principalId`":`"$miObjectId`",`"resourceId`":`"$graphSpId`",`"appRoleId`":`"79c261e0-fe76-4144-aad5-bdc68fbe4037`"}"

You can verify the assignments in Azure portal → Entra ID → Enterprise applications → search for the managed identity object ID → Permissions.


Option B — Automated script (recommended for local dev or non-Azure hosts)

What this does: Runs a PowerShell script that creates the app registration, adds the two required permissions, grants admin consent, creates a client secret, and writes everything into your .env file automatically. If the app registration already exists, it reuses it (safe to re-run).

You'll need:

  • An account that can register apps and grant tenant-wide admin consent (Global Administrator, Privileged Role Administrator, or Cloud Application Administrator)
  • Azure CLI installed
  • PowerShell 7+ installed (pwsh)

Steps:

  1. Sign in to Azure CLI with your admin account:

    az login --tenant <your-tenant-id>

    (Replace <your-tenant-id> with your organization's tenant ID — find it in Azure portal → Entra ID → Overview.)

  2. Run the setup script:

    pwsh .\scripts\create-entra-app.ps1
  3. The script writes AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET into .env. You're done — skip ahead to Step 3.

Optional overrides (set before running):

$env:APP_NAME     = 'My Portal Name'   # default: Microsoft Communications Portal
$env:SECRET_YEARS = '1'                # default: 2 years

Option C — Manual setup via the portal UI

Use this if you prefer clicking through the Azure portal or can't install PowerShell / Azure CLI.

Click to expand step-by-step instructions

Step 2a — Create the app registration

  1. Sign in to the Entra admin center (or Azure Portal → App registrations).
  2. Go to Identity → Applications → App registrations and click + New registration.
  3. Fill in:
    • Name: Microsoft Communications Portal (or any name you like)
    • Supported account types: Accounts in this organizational directory only (Single tenant) — this means only users and apps in your organization can use it.
    • Redirect URI: leave blank — this app uses the "client-credentials flow" (app-to-app authentication), so no redirect is needed.
  4. Click Register.
  5. On the Overview blade, copy these two values — you'll paste them into .env later:
    • Application (client) ID → this becomes AZURE_CLIENT_ID
    • Directory (tenant) ID → this becomes AZURE_TENANT_ID

Step 2b — Add Microsoft Graph API permissions

These permissions tell Microsoft Graph what data the portal is allowed to read.

  1. In the new app registration, go to API permissions → + Add a permission.

  2. Choose Microsoft Graph → Application permissions (NOT "Delegated" — that's for apps where a user signs in; this portal uses app-only access).

  3. Search for and check each of these permissions:

    • ServiceMessage.Read.All — lets the portal read Message Center announcements
    • ServiceHealth.Read.All — lets the portal read Service Health incidents
  4. Click Add permissions.

  5. Back on the API permissions blade, click Grant admin consent for <your tenant> and confirm. The Status column should show a green check ✅ for both permissions.

    Don't see the "Grant admin consent" button? You need to be signed in as a Global Administrator, Privileged Role Administrator, or Cloud Application Administrator. Ask your IT admin to grant consent on your behalf.

Step 2c — Create a client secret

A client secret is like a password for the app registration. The portal uses it to prove its identity when calling Microsoft Graph.

  1. Go to Certificates & secrets → Client secrets → + New client secret.
  2. Enter a description (e.g. portal-local-dev) and pick an expiration (6–24 months). Shorter is more secure but means you'll need to rotate it sooner.
  3. Click Add.
  4. Immediately copy the secret's Value (not the "Secret ID") — it's only shown once. This is your AZURE_CLIENT_SECRET. If you navigate away before copying it, delete it and create a new one.

Step 2d — Create your .env file

  1. Copy the template:
    # macOS / Linux
    cp .env.example .env
    
    # Windows PowerShell
    Copy-Item .env.example .env
  2. Open .env in any text editor and paste in the three values you collected:
    AZURE_CLIENT_ID=00000000-0000-0000-0000-000000000000
    AZURE_CLIENT_SECRET=your-client-secret-value
    AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000
    

Keep secrets safe. Treat the client secret like a password — never commit .env to source control (the included .gitignore already excludes it). The server reads .env at startup, so restart node server.js after any change.

Data delay: Message Center and Service Health data can take up to ~1 hour to appear after first consent while Microsoft Graph provisions access to your tenant's data. This is a one-time delay.

Step 3 — Configure Azure Service Health

(Optional — only needed for the Azure Service Health page. Skip if you don't need Azure-level health monitoring.)

What is Azure Service Health? While the M365 Service Health page (Step 2) shows incidents for Microsoft 365 services (Teams, Exchange, SharePoint, etc.), the Azure Service Health page shows incidents for Azure infrastructure services (Virtual Machines, App Service, Storage, etc.) in your specific subscriptions.

How it works: This page uses the Azure Resource Manager (ARM) REST API (management.azure.com) — a different API from Microsoft Graph. It reuses the same credentials (managed identity or client secret) you set up in Step 2, but the service principal also needs an Azure RBAC role (a permission assignment) on the Azure subscription(s) you want to monitor.

What is RBAC? Role-Based Access Control is Azure's permission system. You assign a "role" (like "Reader") to an identity (like your app registration) on a "scope" (like a subscription). This grants the identity specific permissions within that scope.

Steps:

  1. Assign a Reader role to your app registration's service principal (or managed identity) on each Azure subscription you want to monitor. Skip this for the subscription deployed by azd up; its Bicep template creates the assignment:

    # Replace the placeholders with your values:
    az role assignment create \
        --assignee <service-principal-app-id-or-managed-identity-object-id> \
        --role "Reader" \
        --scope "/subscriptions/<subscription-id>"

    Which role? Reader grants the management-plane read permissions used by the subscription picker and Resource Health endpoints. The specialized Service Health Security Reader role is only needed when the app must read sensitive security-advisory details.

    Where do I find my subscription ID? Azure portal → Subscriptions → click your subscription → the Subscription ID is shown on the Overview blade.

  2. Set the default subscription in .env (optional — you can also pick subscriptions in the UI):

    AZURE_SUBSCRIPTION_ID=your-subscription-id-here
    

Without a role assignment, the ARM API calls will return 403 Forbidden. The portal includes a subscription picker on the Azure Service Health page so you can select which subscriptions to monitor after the role is granted.

Step 4 — Run the server

npm start
# or
node server.js

Step 5 — Open in browser

Navigate to http://localhost:3000. The home page shows status cards for all configured data sources so you can confirm which pages are working.

Features

Content sources

  • Power Platform Release Planner — browse release features by product, wave, and date.
  • Microsoft 365 Roadmap — current and upcoming M365 features from the official RSS feed.
  • Azure Updates — Azure product announcements from the official RSS feed.
  • Microsoft Fabric Roadmap — browse Fabric release features across 14 product areas from the official roadmap.
  • Message Center — tenant-specific Microsoft 365 Message Center announcements, filterable by severity and date.
  • Service Health — current service incidents and advisories for your tenant.
  • Azure Service Health — Azure-level service health events, resource availability, and emerging issues for selected subscriptions.

Across every page

  • Product / service logos on every card — each card's product badge auto-resolves to a Microsoft product icon from /public/*.svg using a curated alias map plus token-based fuzzy matching (Jaccard + coverage). Covers Power Platform, Dynamics 365, the full M365/Viva/Copilot family, security and identity (Defender, Entra, Purview, Intune), Office apps, and 30+ Azure services. Falls back silently when no icon matches, so unknown products never show a broken image. Implementation: static/product-icons.js.
  • ⬇ Export — one-click export of the currently filtered view to a self-contained .html file you can paste straight into a new Outlook email. The exporter (see static/outlook-export.js) commits to a single high-contrast light palette and puts inline style="…" plus a redundant legacy bgcolor="…" attribute on every cell so the table renders correctly even after Outlook's compose surface strips <style> blocks, CSS variables, and @media rules. Outlook's built-in dark-mode rendering handles the dark conversion automatically.
  • 🛠 Generate Full Export — opens a modal preloaded with the page's current filters, then lets you further focus the export by date range (the right date axis for the page — GA Date, Published, Started, etc.) plus a checklist of products / services with select-all, clear, and search. A live counter shows how many items match before you generate. Items that don't carry a product / service field appear under an (Unclassified) bucket (pinned to the bottom of the list) so they can still be included or excluded explicitly. The output is the same Outlook-friendly HTML produced by the regular export.
  • Dark mode — toggle between light and dark themes from the header, or pass ?clawpilotTheme=light / ?clawpilotTheme=dark on the URL. Theme is applied before first paint to avoid flash.
  • Auto-refresh — Graph-backed pages (Message Center, Service Health) refresh on an interval; non-Graph pages refresh on demand. Public roadmap refresh buttons bypass both browser and server TTL caches, while concurrent refreshes still coalesce into one upstream request.
  • Shareable public filters — Microsoft 365 Roadmap, Azure Updates, and Fabric Roadmap keep search, sort, grouping, timeframe, and multi-select filters in the URL. Reloading or sharing the URL restores the same public-data view without persisting tenant-specific searches.
  • Visible data freshness — public roadmap pages show the upstream source, fetch time, and whether results are fresh, cached, partial, stale, or unavailable. If a transient refresh fails, the server can serve bounded stale public data (24 hours for RSS/Fabric, 7 days for geography data) with an explicit warning instead of replacing useful results with an empty screen.
  • Resilient tenant feeds — idempotent Microsoft Graph reads retry transient network, HTTP 429, and HTTP 5xx failures with bounded exponential backoff. If Graph remains unavailable, Message Center and Service Health can serve data cached within the previous 10 minutes with a visible stale-data warning.

✨ AI Insights (optional)

  • A per-feed "Top 5 most impactful changes this week" digest at the top of the page, covering the last 14 days and refreshed on demand.
  • A Summarize with AI button inside every announcement's detail modal — produces a plain-language summary, impact rating (high / medium / low), audience (IT admins, end users, developers, security), and an admin-action flag with deadline.
  • Highlights breaking changes, retirements, GA launches, security/compliance changes, and items requiring admin action. See AI summarization below.

Security hardening

The portal is built with defense-in-depth security suitable for enterprise environments. Every feature below is implemented and active by default — no additional configuration required unless noted. For the full security policy and vulnerability reporting instructions, see SECURITY.md.

Authentication modes

The server refuses to start in a "fail-open" configuration. On every deployment you must resolve to one of four explicit authentication modes, selected via the AUTH_MODE environment variable (auto-inferred when it can be done safely):

AUTH_MODE When to use What it enforces
easyauth Azure App Service with Entra ID Easy Auth enabled. Auto-inferred when WEBSITE_INSTANCE_ID is present. Every /api/* request must carry a valid X-MS-CLIENT-PRINCIPAL header. The server base64-decodes the header, JSON-parses it, and requires auth_typ=aad plus an oid claim that matches the platform-injected X-MS-CLIENT-PRINCIPAL-ID header. Header presence alone is not sufficient. On App Service, protected requests are rejected with AUTH_NOT_ENFORCED unless the read-only platform signal WEBSITE_AUTH_ENABLED is True.
reverse-proxy Docker / VM / on-prem behind an authenticating reverse proxy (nginx, Traefik, Azure Front Door, etc.) on a trusted network. Every /api/* request must carry Authorization: Bearer <API_AUTH_TOKEN>. API_AUTH_TOKEN is required — the server refuses to start without it. Compared in constant time. This is defense-in-depth between the proxy and the app: even if the proxy misroutes an unauthenticated request, the token still gates access.
none-loopback-only Local development on 127.0.0.1/::1. Auto-inferred when HOST is loopback. No token required. The server refuses to start under this mode if HOST is non-loopback.
swa App Service used as a linked backend of an Azure Static Web App (see Quick Deploy option 2). Never auto-inferred — it must be set explicitly. Every /api/* request must carry an X-MS-CLIENT-PRINCIPAL header holding either a Static Web Apps client principal (userId + userDetails, with authenticated present in userRoles) or an App Service Easy Auth principal. Both shapes are accepted because the linked-backend hop may re-encode the header; anything else is rejected. This is safe only because linking auto-configures the Azure Static Web Apps (Linked) identity provider, which blocks any request that did not arrive through the Static Web App.

Fail-fast behavior. The server calls process.exit(1) at startup on any of these misconfigurations:

  • AUTH_MODE is unset AND HOST is non-loopback AND WEBSITE_INSTANCE_ID is unset (no safe inference possible).
  • AUTH_MODE=none-loopback-only with a non-loopback HOST.
  • AUTH_MODE=reverse-proxy with no API_AUTH_TOKEN.
  • AUTH_MODE is set to any value other than the four listed above.
  • Binding to a non-loopback host without ALLOW_REMOTE_BIND=true (defense in depth against accidental exposure).

AUTH_MODE=swa additionally logs a warning when it is used outside App Service (WEBSITE_INSTANCE_ID unset) or when WEBSITE_AUTH_ENABLED is not True, since both suggest the linked-backend identity provider is missing.

Migration from earlier versions. Older builds accepted a HOST=0.0.0.0 container as long as API_AUTH_TOKEN was set. That still works, but you must now also set AUTH_MODE=reverse-proxy explicitly (or easyauth on App Service). The old header-presence Easy Auth check has been replaced with a proper cryptographic-style header validator: spoofed values like X-MS-CLIENT-PRINCIPAL: x no longer bypass the guard.

Authentication & authorization

Feature What it does What it prevents
Entra ID Easy Auth Set the optional authClientId Bicep parameter to require organizational sign-in before any request reaches the App Service. Without it, public feeds remain available while tenant and AI API routes fail closed in the application. See Quick Deploy. Unauthorized access to tenant-sensitive Graph data, Azure Resource Health endpoints, and billed LLM API calls.
Managed identity support Preferred for Azure deployments (USE_MANAGED_IDENTITY=true). Azure issues and rotates tokens automatically — no client secret stored anywhere. See Setup → Option A. Credential leakage, secret sprawl, and manual rotation failures.
Admin endpoint auth The /api/empty-products endpoint is restricted to loopback addresses (127.0.0.1, ::1). When ADMIN_TOKEN is set, a bearer token is additionally required for both reads and mutations, compared using crypto.timingSafeEqual. Unauthorized cache manipulation and timing side-channel attacks on token comparison.
Graph token isolation OAuth bearer tokens for Microsoft Graph are never sent to any host other than graph.microsoft.com. If an @odata.nextLink points at a different host, the request is rejected. Token exfiltration via open-redirect or DNS rebinding on upstream APIs.

Cross-site scripting (XSS) prevention

Feature What it does What it prevents
Strict Content Security Policy (CSP) with nonce Every HTML response includes a CSP header. Inline <script> tags are authorized via a per-request cryptographic nonce (script-src 'self' 'nonce-…'); 'unsafe-inline' is not used. Additional directives: frame-ancestors 'none', base-uri 'none', form-action 'none', object-src 'none', frame-src 'none', media-src 'none', worker-src 'none'. Injected scripts, clickjacking (frame-ancestors), base-tag hijacking (base-uri), form-action redirection, and plugin/embed-based attacks.
Allow-list HTML sanitizer A single hardened sanitizer in static/util.js is shared across all pages. It parses untrusted feed / Graph HTML via DOMParser (which does not execute scripts), keeps only an explicit allow-list of safe tags and attributes (links forced to target="_blank" rel="noopener noreferrer"), and drops everything else — including <script>, <style>, <iframe>, <math>, <template>, <svg>, <noscript>, event-handler attributes, javascript: / data: / blob: URLs, and style attributes. Stored XSS and mutation-XSS from upstream Microsoft feed data and Graph API responses. The allow-list approach is far more resilient than deny-lists.
Event delegation (no inline handlers) All user-facing event handlers use a data-act delegation system in static/util.js. No onclick=, onchange=, or other inline handler attributes appear anywhere in the markup. Inline-handler injection that would bypass CSP. Ensures the nonce-based CSP policy is fully enforceable with no exceptions.

Server-Side Request Forgery (SSRF) & redirect safety

Feature What it does What it prevents
Redirect-following restricted to Microsoft hosts The RSS and release-plan fetchers only follow HTTP redirects to *.microsoft.com and *.azure.com over HTTPS, up to a maximum of 5 hops. Redirects to any other host are rejected with a logged error. SSRF attacks where a compromised or malicious upstream redirects the server to internal network resources or attacker-controlled hosts.
UUID validation on inputs Product IDs and other identifiers passed via query parameters are validated as well-formed UUIDs before being interpolated into upstream API paths. Path injection and cache-key abuse through malformed identifiers.

Injection prevention

Feature What it does What it prevents
Prototype pollution guard The JSON body parser uses a reviver function that strips __proto__, constructor, and prototype keys before they reach application code. Prototype pollution attacks that could modify object behavior, bypass validation, or escalate to remote code execution.
Path traversal protection The /static/ and /public/ file-serving routes reject .., \, and any resolved path that escapes the intended root directory using path.normalize() checks. Directory traversal attacks that could read arbitrary files from the server filesystem (e.g., .env, /etc/passwd).
1 MB request body limit JSON POST bodies are capped at 1 MB (/api/summarize uses a tighter 256 KB cap). Oversized payloads stop buffering and receive HTTP 413. Memory exhaustion and denial-of-service via large request bodies.

Rate limiting & abuse prevention

Feature What it does What it prevents
Per-IP rate limiting on every endpoint All API endpoints enforce per-IP fixed-window rate limits. Set TRUST_PROXY=true when running behind a reverse proxy so the limiter reads the client IP correctly (Azure App Service X-Azure-ClientIP header preferred, then the last X-Forwarded-For hop — not the first, which is client-spoofable). Denial-of-service, brute-force abuse, and billed API cost runaway.
Rate limit tiers AI summarize: 5/min. AI digest: 10/min. Graph + RSS endpoints: 60/min. Proxy: 600/min. Subscription endpoints: 30/min. Exceeded requests receive HTTP 429 with a Retry-After header. Proportional protection — tighter limits on expensive AI calls, looser on read-only feeds.
Daily LLM budget cap A global daily limit (default 200 calls/day, configurable via LLM_DAILY_LIMIT) caps total LLM invocations per UTC day regardless of how many clients are hitting the endpoint. Runaway AI API spend from automated abuse, misconfigured clients, or compromised deployments.
Cache size caps The upstream response cache and rate-limit bucket map are both hard-capped (500 and 10,000 entries respectively). Expired entries are evicted first, then oldest-inserted. Memory exhaustion from clients spraying unique cache keys or URLs to grow internal maps without bound.

Transport & network security

Feature What it does What it prevents
Loopback-only binding by default The server binds to 127.0.0.1 and refuses to start on a non-loopback host unless ALLOW_REMOTE_BIND=true AND a suitable AUTH_MODE are explicitly set. When overridden, a prominent warning is logged listing the specific risks (Graph tokens, billed LLM APIs, admin endpoints) and the resolved AUTH_MODE. See Authentication modes for the full matrix. Accidental exposure of credential-bearing services to untrusted networks.
HSTS (HTTP Strict Transport Security) All JSON and HTML responses include Strict-Transport-Security: max-age=31536000; includeSubDomains. Protocol downgrade attacks and SSL-stripping when deployed behind a TLS-terminating reverse proxy.
CORS allow-list Cross-origin requests are only permitted for origins explicitly listed in the CORS_ORIGINS environment variable (comma-separated). If unset, no Access-Control-Allow-Origin header is emitted (same-origin only). Wildcard (*) is not supported. Cross-site data theft from unauthorized origins.
Upstream request timeouts All outbound HTTP requests enforce timeouts: 15 seconds for Graph/RSS/proxy calls, 30 seconds for AI calls. Hung sockets and resource exhaustion from slow or unresponsive upstreams.
Keep-alive connection pooling A shared https.Agent with keepAlive: true and maxSockets: 32 reuses TLS connections to upstream hosts. Socket exhaustion and excessive TLS handshake overhead under load.
Secure session cookies Session cookies are HttpOnly, SameSite=Strict, and Secure for network-facing or TLS requests. The decision uses the socket and trusted proxy protocol, never the client-controlled Host header. Session disclosure over plaintext transport or cookie-flag downgrades through a spoofed host header.

Response security headers

All responses include defense-in-depth headers:

Header Value What it prevents
X-Content-Type-Options nosniff MIME-type sniffing that could reinterpret a response as executable script.
X-Frame-Options DENY Clickjacking via <iframe> embedding (defense-in-depth alongside CSP frame-ancestors).
Referrer-Policy no-referrer Leaking sensitive URL parameters (e.g., tokens, subscription IDs) to external sites via the Referer header.
Cache-Control no-store (HTML) Prevents caching of HTML pages that may contain nonces or tenant-specific data.
Content-Security-Policy Full policy (see above) Comprehensive XSS, clickjacking, and resource-loading restrictions.

Public feed JSON over 4 KB uses the shared asynchronous gzip/deflate response path. Responses include X-Cache, X-Data-Fetched-At, and a meta object so clients can distinguish fresh, cache-hit, coalesced, and stale results. CORS responses vary on both Origin and Accept-Encoding.

AI-specific security

Feature What it does What it prevents
Prompt injection defenses Both the summarize and digest system prompts include explicit instructions to ignore any directives, prompts, or instructions embedded in feed item titles or descriptions. The LLM is instructed to only analyze factual content and never change its output format, role, or behavior based on item content. Indirect prompt injection via malicious content in upstream Microsoft feeds (e.g., a crafted Azure Update title designed to hijack the LLM).
Strict JSON output format All LLM calls request structured JSON responses with a fixed schema. Responses are parsed and validated before being returned to clients. Output manipulation that could inject malicious content into the UI through LLM responses.
AI provider auto-detection The server detects the AI provider in priority order (Azure OpenAI → OpenAI → GitHub Models). When no provider is configured, AI features are cleanly disabled — /api/summarize returns 503, and the UI silently omits AI panels. Misconfiguration errors and confused failure modes when AI is not set up.

Credential & secret management

Feature What it does What it prevents
.env excluded from source control The .gitignore file excludes .env and other secret-bearing files. Accidental commit of API keys, client secrets, or tokens to a public or shared repository.
No secrets in code All credentials are loaded exclusively from environment variables or Azure managed identity. No secrets are hardcoded. Credential exposure through source code inspection or repository access.
Client secret rotation guidance The setup documentation includes expiration guidance and rotation instructions for Entra ID client secrets. Expired or compromised credentials going unnoticed.

Supply chain & dependency minimization

Feature What it does What it prevents
Single runtime dependency The server depends only on dotenv. All HTTP, HTTPS, crypto, compression, and file-serving functionality uses Node.js built-in modules. Supply chain attacks via compromised npm packages. A minimal dependency tree means a minimal attack surface.
No native add-ons The server uses no native C/C++ modules — pure JavaScript only. Build-time supply chain attacks and binary compatibility issues across platforms.
Node.js 24 LTS The engines field in package.json enforces Node.js 24+, ensuring the latest security patches and built-in API improvements. Running on outdated runtimes with known vulnerabilities.

AI summarization (optional)

The portal can call an LLM (Large Language Model) to surface what actually matters in the firehose of Microsoft updates. When enabled, seven supported feed pages show:

  • A Top 5 most impactful changes digest at the top of the page (covers the last 14 days, refreshed on demand), with one-line themes and an overall headline.
  • A ✨ Summarize with AI button inside each announcement's detail modal — produces a plain-language summary, an impact rating (high/medium/low), the intended audience (IT admins, end users, developers, security), and a flag if admin action is required before a deadline.

How to enable: Pick one of the three providers below, set the corresponding variables in .env, and restart the server. The server auto-detects which provider to use (priority: Azure OpenAI → OpenAI → GitHub Models). When no AI provider is configured the portal still works fine — the AI panel just shows a note pointing to .env.example.

Which provider should I pick?

Provider Env vars to set Pros Cons Cost
Azure OpenAI AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_OPENAI_DEPLOYMENT, (optional) AZURE_OPENAI_API_VERSION Azure-managed data privacy controls; enterprise SLAs; content filtering built in Requires an Azure OpenAI resource and organizational review of the selected region/model; most setup steps Pay-per-token (very low with gpt-4o-mini)
OpenAI OPENAI_API_KEY, (optional) OPENAI_MODEL, OPENAI_BASE_URL Fastest setup — just paste an API key; no Azure subscription needed Data leaves your tenant; usage is billed to your OpenAI account Pay-per-token
GitHub Models GITHUB_TOKEN, (optional) GITHUB_MODEL Free tier — great for trying it out; no billing setup Rate limits on free tier; data processed by GitHub; not for production workloads Free (with limits)

Recommended model: gpt-4o-mini (or your deployment name for Azure OpenAI) — fast, cheap, and plenty capable for this kind of triage.

Example .env for each provider:

# ── Azure OpenAI (recommended for enterprise) ──
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_API_KEY=your-azure-openai-key
AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini

# ── OpenAI (quickest setup) ──
OPENAI_API_KEY=sk-...

# ── GitHub Models (free, great for testing) ──
GITHUB_TOKEN=ghp_...

Responses are cached in memory (summaries for 10 minutes, digests for 15 minutes) so repeat views don't burn tokens. A per-process daily call budget defaults to 200 and can be lowered with LLM_DAILY_LIMIT. Feed content, including tenant-specific Message Center or Service Health text, is sent to the provider you configure; use only a provider and region approved for that data.

API Endpoints

The Node server exposes the following local endpoints (all return JSON):

Endpoint Description Auth Rate limit
GET /healthz or /health Health check / liveness probe None
GET /readyz Readiness probe; returns HTTP 503 while the process is shutting down None
GET /api/auth-check Reports auth configuration status for Graph, ARM, and AI None
GET /proxy?productId=...&langCode=... Power Platform Release Planner proxy (follows 301/302/307/308 redirects; auto-skips IDs cached as known-empty). Also reachable as GET /api/proxy?..., which is the path the pages call so Azure Static Web Apps can forward it to the linked backend. None 600/min
GET /api/m365updates[?refresh=1] Microsoft 365 Roadmap RSS, parsed to JSON; optional forced refresh None 60/min
GET /api/azureupdates[?refresh=1] Azure Updates RSS, parsed to JSON; optional forced refresh None 60/min
GET /api/fabricroadmap[?refresh=1] Microsoft Fabric Roadmap JSON (14 product areas); optional forced refresh None 60/min
GET /api/featuregeo[?refresh=1] Regional release plans (feature availability by geography), normalised from the public release plans site (30 min server cache) None 60/min
GET /api/messagecenter Microsoft 365 Message Center via Microsoft Graph .env 60/min
GET /api/servicehealth Microsoft 365 Service Health via Microsoft Graph .env 60/min
GET /api/subscriptions List Azure subscriptions (for subscription picker) .env 30/min
GET /api/subscriptions/selected Get currently selected Azure subscriptions None 30/min
POST /api/subscriptions/selected Save selected Azure subscriptions None 30/min
GET /api/azure-resource-health/emerging-issues Azure emerging issues (tenant-level) .env 60/min
GET /api/azure-resource-health/events Azure Resource Health events (subscription-scoped) .env 60/min
GET /api/azure-resource-health/availability-statuses Azure resource availability summary .env 60/min
GET /api/azure-resource-health/impacted-resources Resources impacted by an event .env 60/min
GET /api/azure-resource-health/resource-events Events for a specific resource .env 60/min
GET /api/azure-resource-health/resource-availability Availability history for a resource .env 60/min
GET /api/azure-resource-health/resource-status Current status of a resource .env 60/min
GET /api/ai-status Reports whether AI is configured and which provider is active None
POST /api/summarize Body {source, items[]} → per-item AI summaries AI provider 5/min
GET /api/impact-digest?source=azure|m365|messagecenter|servicehealth|fabricroadmap&limit=5&windowDays=14 Top N most impactful items for a source AI provider 10/min
GET /api/empty-products List product IDs cached as known-empty by the /proxy route Loopback + ADMIN_TOKEN
DELETE /api/empty-products Clear the entire known-empty cache Loopback + ADMIN_TOKEN
GET /static/<file> Shared client JS (util.js, nav.js, product-icons.js, outlook-export.js, ai-insights.js, export-formats.js, subscription-picker.js) and CSS (common.css, page-specific stylesheets) None
GET /public/<file> Microsoft product / service SVG icons None

OAuth tokens for Microsoft Graph are cached in-memory and refreshed 60 seconds before expiry. Microsoft Graph feed reads retry transient failures up to three attempts and can serve successful responses cached within the previous 10 minutes when Graph is temporarily unavailable. Rate limits are per-IP fixed-window counters; set TRUST_PROXY=true only behind a trusted reverse proxy. The server prefers App Service's X-Azure-ClientIP, then the last X-Forwarded-For hop appended by the proxy. AI provider responses are cached in-memory (summarize: 10 min, digest: 15 min, hashed by input). The known-empty product cache is persisted to empty-products.json so restarts don't lose the auto-skip list. Versioned /static/ URLs use a 24-hour browser cache; unversioned development URLs continue to revalidate with ETags. Static /public/ icons are cached in memory, sent with a 24-hour Cache-Control and an ETag, and SVGs over 4 KB are pre-compressed once per process.

On SIGTERM or SIGINT, the server marks /readyz unavailable, stops accepting new connections after a short readiness propagation window, drains active requests, and closes upstream keep-alive sockets. Set SHUTDOWN_GRACE_MS (0–30,000; default 5,000) to control that window. The included container health checks poll /readyz every two seconds so they can observe the transition. Set SHUTDOWN_TIMEOUT_MS (1,000–30,000; default 10,000) to control the forced shutdown deadline.

Project structure

home.html                        Home / dashboard landing page
powerplatform.html               Power Platform Release Planner UI
featuregeo.html                  Regional Release Plans (world map + rollout table by geography)
m365updates.html                 M365 Roadmap UI
azureupdates.html                Azure Updates UI
fabricroadmap.html               Microsoft Fabric Roadmap UI
messagecenter.html               M365 Message Center UI
servicehealth.html               M365 Service Health UI
azureservicehealth.html          Azure Service Health UI
guidedreport.html                Guided Report wizard (multi-source report builder)
server.js                        Node HTTP server, static file host, API proxy, AI endpoints
static/
  util.js                        Shared client-side utilities (escapeHtml, sanitizeHtml, safeUrl, theme toggle, event delegation)
  nav.js                         Shared navigation header and dropdown menu (injected into every page)
  common.css                     Shared CSS (nav dropdowns, header, layout primitives)
  ai-insights.js                 Shared client-side AI helper (digest panel + per-item summarize)
  product-icons.js               Shared client-side product-icon resolver (alias map + fuzzy matcher)
  outlook-export.js              Shared client-side Outlook-friendly HTML exporter (inline styles + bgcolor)
  export-formats.js              Multi-format export (HTML, Markdown, PDF, Word) for the Generate Full Export modal
  subscription-picker.js         Shared Azure subscription selection picker
  featuregeo.css                 Regional Release Plans page styles (map, filter rail, rollout table)
  worldmap.js                    Generated world outline (Natural Earth 110m) used by the Regional Release Plans map
public/                          Microsoft product / service SVG icons served at /public/<file>.svg
empty-products.json              Persisted cache of known-empty Release Planner product IDs (auto-skip list)
package.json                     Dependencies (dotenv only)
.env.example                     Template for Graph auth, AI providers, and server/security options
scripts/capture-screenshots.js   Playwright script that regenerates the README screenshot gallery
scripts/build-worldmap.js        Regenerates static/worldmap.js from Natural Earth 110m TopoJSON
scripts/build-swa.js             Builds dist/ for Azure Static Web Apps (stages pages + assets, computes CSP script hashes)
staticwebapp.config.json         Azure Static Web Apps routes, auth, security headers (source template — placeholders filled by build-swa.js)
dist/                            Generated Static Web Apps payload (git-ignored; created by npm run build:swa)
screenshots/                     Light + dark mode PNGs rendered into the README above

Requirements

  • Node.js 24 LTS or later (uses the built-in http, https, crypto, and global URL APIs — no native add-ons)
  • npm (bundled with Node.js)
  • An Entra app registration (only for the Message Center and Service Health pages)
  • PowerShell 7+ and Azure CLI (only if using scripts/create-entra-app.ps1)

Contributing

This project welcomes suggestions and feedback. Please open an issue to discuss changes before submitting a pull request. See CODE_OF_CONDUCT.md for community guidelines.

License

This project is licensed under the MIT License — see LICENSE for details.

Contact

Questions, feedback, or ideas to make this portal better? Reach out to Russ Rimmerman at russ.rimmerman@microsoft.com or connect on LinkedIn.

Releases

Packages

Contributors

Languages