HOME β€Ί DASHBOARDS β€Ί GLOBAL OPS HEALTH CONSOLE β€” KB
dashboard--stack-health-dashboard.html
KNOWLEDGE BASE // DASHBOARDS // STACK HEALTH
Global Operations Health Console
Complete reference for understanding, configuring, and integrating the Global Operations Health Console β€” a unified MSP stack health dashboard covering 9 platforms, client risk scoring, SLA compliance, environment health scoring, and 24-hour alert trending. Includes the full proxy wiring guide for live API integration.
PROXY WIRED 9 PLATFORMS CLIENT RISK LEADERBOARD SLA COMPLIANCE FALLBACK TO DEMO
FILE
dashboard--stack-health-dashboard.html
INTEGRATION
fetchData() + apiFetch() fallback
PLATFORMS
9 β€” NinjaRMM Β· Auvik Β· FortiGate Β· S1 Β· Huntress Β· Backup Β· CW PSA Β· Email Β· Azure
REFRESH
45 seconds auto + Ctrl+R manual
PROXY ENDPOINTS
/api/ops/summary Β· /api/ops/trend24 Β· /api/ops/trend7d
01
WHAT THIS TOOL DOES

The Global Operations Health Console is a single-file HTML dashboard that provides a high-level operational health view across an MSP's full technology stack. It is designed as an executive-facing or NOC wall display that aggregates platform status, client risk, SLA performance, and alert volume into a 4-column grid layout refreshed every 45 seconds.

Unlike the engineer-facing triage consoles, this dashboard is observational. It answers one question at a glance: how healthy is the environment right now? Every panel is clickable and opens a drill-down detail drawer with deeper data, but there are no action workflows β€” no ticket creation, no alert acknowledgement routing, no engineer assignment. It is the command center view that precedes those actions.

PROXY WIRED β€” FALLS BACK TO DEMO The console ships with fetchData() and apiFetch() already integrated. It attempts to call three proxy endpoints on every refresh cycle. If any endpoint is unreachable or returns an error, that call silently falls back to makeData() and generates realistic demo data. The dashboard always renders regardless of API connectivity state.
02
ARCHITECTURE & DATA FLOW

The console is a self-contained single HTML file. Chart.js 4.4.1 loads from the Cloudflare CDN. All rendering is handled by dedicated render*() functions that accept a DATA object β€” the source of that object (proxy or fallback generator) is transparent to every render function.

REFRESH CYCLE
DOMContentLoaded β†’ refreshAll() β†’ fetchData() // 3 parallel proxy calls β†’ apiFetch('/api/ops/summary', genSummaryFallback) β†’ apiFetch('/api/ops/trend24', genTrend24Fallback) β†’ apiFetch('/api/ops/trend7d', genTrend7dFallback) β†’ renderAll(d) // renders all panels setInterval(45 000ms) β†’ fetchData() β†’ renderAll(d) Ctrl+R / ⌘+R or Refresh button β†’ refreshAll() β†’ fetchData() β†’ renderAll(d)
apiFetch() FALLBACK BEHAVIOUR
async function apiFetch(path, fallbackFn) { try { var r = await fetch(PROXY_BASE + path, {signal: AbortSignal.timeout(6000)}); if (!r.ok) throw new Error('HTTP ' + r.status); return await r.json(); // ← live data path } catch(e) { if (!DEMO_MODE) console.warn(...); return fallbackFn(); // ← demo fallback path } }

The 6-second timeout prevents a slow or unreachable proxy from blocking the UI. If the fetch times out, fails, or returns a non-200 status, the fallback generator runs immediately. When DEMO_MODE = true (default) the fallback is silent β€” no console warnings. Set DEMO_MODE = false in production to surface fetch errors in browser DevTools.

RENDER FUNCTIONS
renderAll(d)
Master orchestrator. Updates topbar stats, calls all sub-renders, updates ticker. Called on every refresh cycle.
renderPlatforms()
Renders the 9-tile platform health grid. Each tile shows status badge, health score, alert counts, uptime, and last-seen time. Clicking opens the platform detail drawer.
renderAlertSummary()
Renders the global alert count breakdown and the alert detail rows list. Panel pulses red when critCount > 5.
renderEnvScore()
Renders the environment health ring gauge SVG and per-platform score bars. Also updates the topbar arc.
renderOpsSnapshot()
Renders four operational KPI tiles: open tickets, closed today, avg response time, clients at risk.
renderClientRisk()
Renders the client risk leaderboard sorted by risk score descending.
renderAlertTrend()
Renders the 24-hour stacked area chart (crit/warn/info layers) using Chart.js.
render7Day()
Renders the 7-day platform alert volume stacked bar chart using Chart.js.
03
TOPBAR KPIs

The sticky topbar contains six animated KPI values that update on every refresh using the anim() function β€” a cubic ease animation from the previous value to the new one over 600ms. An SVG arc gauge also reflects the environment health score visually.

Env Health Score
Average health score across all 9 platforms (0–100). Green β‰₯80, orange β‰₯60, red below 60. Rendered as a number and as a partial-arc SVG that fills proportionally. Clicking opens the Environment Health drawer.
Critical Alerts
Total critical-priority alert count. Source: d.critCount. Demo range: 3–12.
Warn Alerts
Total warning-priority alert count. Source: d.warnCount. Demo range: 8–25.
Open Tickets
Total open ticket queue from ConnectWise PSA. Source: d.openTix. Demo range: 60–140.
SLA Compl.
Overall SLA compliance percentage. Source: d.slaOv. Demo range: 88–99%.
Clients At Risk
Count of clients with risk score above healthy threshold. Derived from client risk array: clientRisks.filter(c => c.risk !== 'ok').length.
04
PLATFORM HEALTH GRID

The full-width top panel shows a tile for each of the 9 monitored platforms. Tile border color reflects platform status. Clicking any tile opens the platform detail drawer showing a 24-hour activity chart, health score, alert counts, uptime bar, last seen time, and category.

IDPLATFORMCATEGORYPRIMARY LIVE DATA SOURCE
ninjaπŸ–₯ Ninja RMMEndpoint MonitoringNinjaOne REST API β€” agent status, patch compliance, device counts, alert feed
auvik🌐 AuvikNetwork MonitoringAuvik API β€” device health, interface status, network alerts
fortiπŸ›‘ FortiGateFirewall / SecurityFortiOS REST API β€” policy hits, VPN tunnels, interface status
s1βš” SentinelOneEndpoint SecurityS1 Management API β€” threat detections, agent health, isolation status
huntressπŸ” HuntressThreat DetectionHuntress API β€” incident reports, persistent threats, footholds
backupπŸ’Ύ Backup SystemsData ProtectionVeeam / Cove / Datto API β€” job results, failure counts, restore points
cw🎫 ConnectWise PSAService DeskCW REST API β€” tickets, SLA, engineer assignments, CSAT
emailπŸ“§ Email SecurityEmail ProtectionMimecast / Defender API β€” threat blocks, quarantine, delivery rates
azure☁ Azure / M365Cloud InfrastructureMicrosoft Graph API β€” tenant health, service alerts, license status
GUARANTEED CRITICAL IN DEMO makeData() enforces at least one platform is always crit status. If random generation produces no critical platforms, one is forcibly set. Remove this enforcement when wiring live data β€” your real environment may genuinely have all platforms healthy.
05
GLOBAL ALERT SUMMARY

The right-side panel (spans rows 2–3) shows the global alert breakdown and a 24-hour trend chart. The three large numbers show Critical, Warning, and Info alert counts. Below them is a scrollable list of up to 18 alert detail rows. The panel gains a red pulse animation (pulsecrit class) when critCount > 5.

Clicking the panel or the panel header opens the alert drawer. Individual alert rows in the drawer are clickable β€” clicking an alert row dims it and fires a toast acknowledging it. In production, this onclick should be wired to your PSA's acknowledge endpoint (PATCH /api/proxy/psa/alerts/{id}/ack).

Alert ID format
ALT-NNNNN format, starting from ALT-09000 in demo. In production this should be the native ID from whichever platform generated the alert.
Alert priority values
'crit', 'warn', 'info' β€” lowercase strings. The drawer filter uses these exact values. Case-sensitive.
Alert assigned field
Engineer name string or 'Unassigned'. The filter drawer shows this in the alert row meta line.
06
ENVIRONMENT HEALTH SCORE

The Environment Health panel (row 2, col 1) shows an SVG ring gauge with the aggregate environment score and a grade letter, plus a set of horizontal bars showing each platform's individual score contribution. Clicking opens the Environment Health drawer, which lists all platform scores as clickable bars β€” clicking a platform bar opens that platform's detail drawer.

Score calculation
Simple average of all platform score fields: Math.round(platforms.reduce((s,p) => s+p.score, 0) / platforms.length). Range 0–100.
Grade thresholds
A β‰₯90 Β· B β‰₯80 Β· C β‰₯70 Β· D β‰₯60 Β· F below 60. Shown large in the ring gauge center.
Color thresholds
Green β‰₯80 Β· Orange β‰₯60 Β· Red below 60. Applied to the ring stroke, grade letter, topbar arc, and individual platform score bars.
Platform score ranges
ok: 88–100 Β· warn: 70–87 Β· crit: 30–60 Β· deg (degraded): 60–75. Your proxy should compute these from actual platform API health responses.
07
OPERATIONAL SNAPSHOT

The Operational Snapshot panel (row 2, col 2) shows four PSA-derived KPI tiles. All four come from d fields that in production map directly to ConnectWise PSA API responses.

Open Tickets
Source: d.openTix. Live: GET /service/tickets?conditions=status/name="Open"&pageSize=1&fields=id β€” use the X-Total-Count response header for the count without fetching all records.
Closed Today
Source: d.closedToday. Live: CW tickets closed since midnight today. Use dateResolved >= [today-00:00] filter.
Avg Response
Source: d.avgResp (float, hours). Live: average time-to-first-response across tickets opened this week from CW time entries or SLA report endpoint.
Clients At Risk
Source: d.clientsAtRisk. Derived from the client risk array β€” count of clients where risk !== 'ok'. Same value as in the topbar.
08
SLA COMPLIANCE

The SLA Compliance panel (row 2, col 3) shows an SVG ring gauge for overall SLA percentage and three horizontal bar gauges for P1 response, P2 response, and overall compliance. The ring color is green β‰₯95%, orange β‰₯85%, red below 85%. Clicking opens the SLA detail drawer.

slaP1
P1 ticket SLA compliance percentage. Demo range 88–98. Live: derived from CW SLA report for P1-priority tickets.
slaP2
P2 ticket SLA compliance percentage. Demo range 91–99. Live: same report, P2 filter.
slaOv
Overall SLA. Computed as Math.round((slaP1 + slaP2) / 2). You can also supply this directly from the PSA's aggregate SLA endpoint.
09
CLIENT RISK LEADERBOARD

The Client Risk Leaderboard (row 3, cols 1–2) shows up to 10 clients sorted by risk score descending β€” highest-risk first. Each row shows a color-coded risk badge, client name, risk score, open alert count, backup failure count, security incident count, and patch failure percentage. Clicking a row or the panel opens the client detail drawer.

Risk level assignment
score > 70 β†’ 'crit' (red) Β· score > 45 β†’ 'warn' (orange) Β· otherwise β†’ 'ok' (green). Your proxy should compute the score from the client's actual alert and compliance data.
Score calculation (suggested)
A composite score based on open alerts, backup failures, security incidents, and patch compliance β€” similar to the burnout console pattern. Higher score = higher risk. Suggest: 100 - openAlerts*4 - backupFails*8 - secInc*15 - patchFail/4 clamped to 5–95.
Client name field
The name field must match a string in the CLIENTS array or be added to it. The drawer lookups use name-string matching against DATA.clientRisks.
10
ANALYTICS CHARTS
24-HOUR ALERT TREND

Stacked area chart rendered by renderAlertTrend() inside the Global Alert Summary panel. Three layers: critical (red), warning (orange), info (blue). Shows alert volume per hour over the past 24 hours. Clicking the panel opens the trends drawer which also shows this chart at larger scale.

Data shape: array of 24 objects β€” [{ h:0, crit:2, warn:5, info:8 }, ...] β€” one per hour, h is the hour (0–23). The proxy endpoint GET /api/ops/trend24 returns this array directly.

7-DAY PLATFORM ALERT VOLUME

Stacked bar chart rendered by render7Day() in the bottom-right panel. Shows the first 5 platforms (by PLATFORMS array order) with 7 daily values each. Clicking opens the trend drawer.

Data shape: array of platform objects β€” [{ name:'Ninja', vals:[12,8,15,6,20,4,9] }, ...] β€” name is a short display label, vals is Mon–Sun alert counts. The proxy endpoint GET /api/ops/trend7d returns this array directly.

CHART.JS CDN DEPENDENCY Both charts require Chart.js 4.4.1 from cdnjs.cloudflare.com. If this file is deployed in an environment without reliable internet access, host Chart.js locally and update the <script> src. If the CDN request fails, both chart canvases will be blank β€” all other panels still function normally.
11
DETAIL DRAWER

The detail drawer is a 380px slide-in panel from the right side of the screen. It is opened by clicking any major panel and closed by clicking the overlay, pressing Escape, or clicking the close button. It renders different content based on the type argument passed to openDrawer().

TRIGGERTYPE PASSEDDRAWER CONTENT
Platform tile clickopenPlatformDrawer(p)Platform health score, crit/warn counts, uptime bar, last seen, category, 24h activity bar chart. Drawer message invites API connection.
Alert panel / critical count'alerts-crit'Critical alert count stats + full list of critical alert detail rows. Each row is clickable to acknowledge.
Alert panel / warning count'alerts-warn'Warning alert stats + warning alert list.
Alert panel / info count'alerts-info'Info event stats + generated info event list.
Environment Health panel'env'Overall env score, platform breakdown counts, per-platform score bars (clickable β†’ platform drawer).
SLA panel'sla'P1/P2/overall SLA gauges, distribution bar, top violating clients.
Ops Snapshot (tickets)'tickets'Open/closed/overdue ticket counts, priority distribution, recent ticket list.
Client Risk row / panelopenClientDrawer(c)Client risk score, open alerts, backup fails, security incidents, patch fail rate, risk breakdown bars.
7-Day trend panel'trends'Expanded 7-day chart at larger scale, platform alert volume table, week-over-week delta.
12
KEYBOARD & CONTROLS
Escape
Closes the detail drawer if open. Bound globally β€” works regardless of which panel is visible.
Ctrl+R / ⌘+R
Triggers a manual data refresh. e.preventDefault() suppresses the browser reload. Calls refreshAll() β†’ fetchData() β†’ renderAll().
↻ Refresh button
Topbar button that triggers the same refreshAll() path. Button dims during the fetch and restores on completion.
45s auto-refresh
setInterval fires fetchData() every 45 seconds if DATA is not null (i.e. initial load has completed). Does not run until first render is done.
13
DATA MODEL

The proxy must return objects matching these shapes. All render functions consume these shapes directly β€” field names are hardcoded throughout. The /api/ops/summary response carries most of the data; trend endpoints return simpler arrays.

PLATFORM OBJECT (in platforms[])
{ id: string, // 'ninja'|'auvik'|'forti'|'s1'|'huntress'|'backup'|'cw'|'email'|'azure' name: string, // display name e.g. 'Ninja RMM' icon: string, // emoji icon cat: string, // category e.g. 'Endpoint Monitoring' status: string, // 'ok' | 'warn' | 'crit' | 'deg' score: number, // 0–100 health score critAlerts: number, // critical alert count for this platform warnAlerts: number, // warning alert count uptime: number, // uptime % e.g. 99.7 lastSeen: string, // e.g. '3m ago' devices: number, // managed device count }
ALERT DETAIL OBJECT (in alertDetails[])
{ id: string, // e.g. 'ALT-09001' or native platform ID priority: string, // 'crit' | 'warn' | 'info' β€” lowercase, exact title: string, // alert description platform: string, // platform name string e.g. 'SentinelOne' client: string, // client name string age: string, // e.g. '12m' | '2h' assigned: string, // engineer name or 'Unassigned' }
CLIENT RISK OBJECT (in clientRisks[])
{ name: string, // client display name score: number, // 0–100 risk score (higher = more at risk) openAlerts: number, backupFails: number, secInc: number, // security incidents patchFail: number, // patch failure count risk: string, // 'ok' | 'warn' | 'crit' }
FULL SUMMARY RESPONSE SHAPE
GET /api/ops/summary β†’ { platforms: Platform[], // 9 items, one per platform ID alertDetails: AlertDetail[], // up to 18, sorted crit first clientRisks: ClientRisk[], // up to 12, sorted score desc slaP1: number, // P1 SLA % e.g. 94 slaP2: number, // P2 SLA % slaOv: number, // overall SLA % openTix: number, // open ticket count closedToday: number, // tickets closed today avgResp: number, // avg response time in hours (float) critCount: number, // optional β€” derived from alertDetails if absent warnCount: number, // optional infoCount: number, // optional } GET /api/ops/trend24 β†’ [{ h:0-23, crit, warn, info }] // 24 items GET /api/ops/trend7d β†’ [{ name, vals:[mon,tue,wed,thu,fri,sat,sun] }] // 5 items
14
HEALTH SCORING & THRESHOLDS
FIELDGREENORANGERED
Platform statusok β€” score 88–100warn β€” score 70–87 Β· deg β€” 60–75crit β€” score 30–60
Env / platform scoreβ‰₯ 80β‰₯ 60< 60
Environment gradeA β‰₯90 Β· B β‰₯80C β‰₯70 Β· D β‰₯60F < 60
SLA ring gaugeβ‰₯ 95%β‰₯ 85%< 85%
Client riskok β€” score ≀ 45warn β€” score 45–70crit β€” score > 70
Alert panel pulsecritCount ≀ 5β€”critCount > 5 β†’ pulsecrit CSS
15
PLATFORM API REFERENCE

These are the real API endpoints your proxy will call to populate the platform health objects. Each platform's health score should be computed from the response β€” map error rates, alert counts, and uptime data to the 0–100 score range.

PLATFORMAPI BASE / DOCSKEY DATA POINTSAUTH
NinjaRMMapp.ninjarmm.com/v2GET /devices (device count, agent status), GET /alerts (active alerts by severity)OAuth2 client credentials
Auvikauvikapi.us1.my.auvik.com/v1GET /inventory/network (device status), GET /statistics/network/detail (latency, errors)Basic auth (user:apiKey)
FortiGateFortiOS REST API on deviceGET /api/v2/monitor/system/status, GET /api/v2/monitor/vpn/ssl (VPN tunnels)API key header Authorization: Bearer
SentinelOneusea1.sentinelone.net/web/api/v2.1GET /threats (active threats), GET /agents (agent health + count)API token header Authorization: ApiToken
Huntressapi.huntress.io/v1GET /incident_reports (active incidents), GET /accounts/{id}/summaryBasic auth (apiKey:apiKey)
BackupVeeam / Cove / Datto β€” variesJob results, failure counts, last backup timestamps per protected workloadVaries by vendor
ConnectWise PSAna.myconnectwise.net/v4_6_release/apis/3.0GET /service/tickets (open count, SLA), GET /time/entries (response time)Basic auth (clientId+publicKey:privateKey)
Email SecurityMimecast / Defender for 365Threat blocks, quarantine counts, delivery rates β€” vendor-specific endpointsVaries by vendor
Azure / M365graph.microsoft.com/v1.0GET /admin/serviceAnnouncement/healthOverviews (M365 service health), tenant license statusOAuth2 with Entra ID app registration
CONFIGURATION GUIDE
C1
PREREQUISITES
Azure Function Proxy
API credentials for all 9 platforms must never appear in browser code. Store each vendor's API key or token in Azure Key Vault, built via the Gateway Provisioner. The Function App reads credentials via Managed Identity and exposes the three aggregation endpoints the dashboard calls.
Platform API access
Confirm API credentials for each platform you intend to wire. You do not need to wire all 9 at once β€” any platform that is not yet connected will have its data generated by makeData() automatically via the fallback.
SharePoint or web host
The HTML file must be served over HTTPS. SharePoint document library is the standard deployment target. Set PROXY_BASE to the Function App URL if it is on a different origin from the HTML.
Chart.js CDN access
The deployment environment must be able to reach cdnjs.cloudflare.com. If not, host Chart.js locally and update the <script> src.
C2
PROXY CONFIG VARIABLES

Two variables at the top of the script block control the integration behaviour. No other code changes are needed to switch from demo to live mode.

// Line ~1185 in the script block var PROXY_BASE = ''; // empty = same origin as this HTML file // set to full URL if proxy is on different domain: // e.g. 'https://your-funcapp.azurewebsites.net' var DEMO_MODE = true; // true = fallback errors are silent (default) // false = fallback errors logged to browser console
SAME-ORIGIN DEPLOYMENT If this HTML file and the Function App proxy are both served from the same SharePoint site or the same origin URL, leave PROXY_BASE as an empty string. All three proxy calls will resolve relative to the page's own origin. No CORS configuration is needed in same-origin mode.
C3
WIRE SUMMARY ENDPOINT

The /api/ops/summary endpoint is the most work-intensive. It must aggregate data from all platforms you want to show live and return a single JSON object. Build this incrementally β€” start with one or two platforms and let the rest fall back to demo data.

  • 1
    Platform health arrayFor each platform, call its health API, compute a 0–100 score, set status based on score thresholds, and collect alert counts, uptime, and device count. Return all 9 platform objects β€” use the exact id strings from the PLATFORMS array (ninja, auvik, forti, etc.). Platforms not yet wired can be populated with default/neutral values.
  • 2
    Alert details arrayCollect active critical and warning alerts from the platforms that are wired. Normalize each to the alert detail shape: { id, priority, title, platform, client, age, assigned }. Sort criticals first. Limit to 18 items.
  • 3
    Client risk arrayFor each client, aggregate their open alerts (cross-platform), backup failures, security incidents, and patch compliance. Compute a risk score and set the risk field. Sort by score descending. Limit to 10–12 clients.
  • 4
    PSA metricsCall ConnectWise PSA for openTix, closedToday, avgResp, slaP1, slaP2. These are the most straightforward fields β€” CW has native endpoints for each.
  • 5
    CountsEither supply critCount, warnCount, infoCount directly, or omit them β€” fetchData() will derive them from the alertDetails array if absent.
C5
GO LIVE CHECKLIST
  • βœ“
    Set PROXY_BASE. Update the PROXY_BASE variable at the top of the script to your Function App URL if it is on a different origin from the HTML file. Leave empty for same-origin.
  • βœ“
    Set DEMO_MODE = false. Flip the flag so fetch failures appear in browser DevTools and you can diagnose proxy issues.
  • βœ“
    Proxy returns valid JSON for all three endpoints. Open browser DevTools β†’ Network tab. Trigger a refresh (Ctrl+R). Confirm each proxy endpoint returns 200 with a JSON body matching the documented shape.
  • βœ“
    Platform IDs match exactly. The id field in each platform object must match one of: ninja, auvik, forti, s1, huntress, backup, cw, email, azure. Any mismatch causes that tile to render with default values.
  • βœ“
    Alert priority values are lowercase. The drawer filter compares a.priority === 'crit' β€” exact lowercase match. 'CRIT' or 'Critical' will not match and alerts will disappear from the drawer.
  • βœ“
    Trend arrays have correct lengths. /api/ops/trend24 must return exactly 24 items (h: 0–23). /api/ops/trend7d returns 5 items. Shorter arrays produce blank chart sections.
  • βœ“
    API key in Key Vault, not in proxy code. Verify the Function App's Application Settings show Key Vault reference strings (@Microsoft.KeyVault(...)), not raw key values.
  • βœ“
    Remove the enforced-critical logic from makeData(). The makeData() fallback forces at least one critical platform. This is fine for demo but should be removed from the fallback if the fallback is still in use alongside live data, to avoid mixing real and forced values.
C6
TROUBLESHOOT
SYMPTOMCAUSEFIX
Dashboard shows demo data despite proxy being upapiFetch() caught an error and fell back silentlySet DEMO_MODE = false, then open DevTools β†’ Console. The fallback warning will appear with the actual error message.
Platform tiles show wrong platform namesProxy platform id doesn't match the PLATFORMS array IDsEnsure each platform object has exactly the correct id string. Check against the Platform Health Grid table in Section 04.
Alert drawer shows no alerts after filteringAlert priority field is not lowercase 'crit'/'warn'/'info'Normalise priority values in your proxy before returning. priority.toLowerCase() if sourcing from a mixed-case API.
Charts are blank on loadChart.js CDN failed to loadCheck the browser console for a Script error on the Chart.js CDN URL. Either fix CDN access or host Chart.js locally.
Charts go blank after a few refreshesChart instances not destroyed before re-renderAll chart rendering goes through mkChart() which calls chart.destroy() before creating a new instance. If you add custom charts that bypass mkChart(), add explicit destroy calls.
Environment score ring shows wrong colorScore is outside the expected 0–100 rangeVerify your proxy clamps all platform scores to 0–100. A score above 100 causes the SVG arc calculation to exceed full-circle and wrap.
Refresh button stays dimmed after a refreshfetchData() promise rejected without being caughtA render function receiving unexpected data shape may throw uncaught. Open DevTools β†’ Console. Look for TypeError or undefined property errors.
Drawer opens blankDATA is null when drawer is triggeredA toast "Loading…" appears and drawer opens empty if initial fetch hasn't completed. This resolves itself β€” wait for the first refresh cycle to finish.