Knowledge Base
Phase 1 · 2 · 3 · Live-Ready
// Knowledge Base · Meraki MSP Command Center
Meraki MSP Command Center
Complete reference for the Cisco Meraki MSP Command Center — a three-tab operations dashboard covering multi-org fleet health, live alert streaming, and a sortable device explorer across MX, MR, MS, and MG hardware. All Meraki Dashboard API v1 calls are stubbed with real commented-out fetch patterns. Phase 1 (inventory), Phase 2 (alerts/events), and Phase 3 (write actions) are fully documented. Activate live mode by entering an API key and uncommenting the real call in each function.
Orgs (demo): 5
Devices (demo): 82 (MX / MR / MS / MG)
API: Meraki Dashboard API v1
Rate limit: 10 req/sec/org · 5 req/sec device mgmt
CVE feed: NVD API · 15 min refresh
01 //What Is This Tool

The Meraki MSP Command Center is a single-file, multi-tab operations dashboard for MSPs managing Cisco Meraki infrastructure across multiple client organizations. It provides fleet-level health visibility, alert streaming, firmware compliance tracking, per-device management actions, and a live CVE feed from NVD — all driven by the Meraki Dashboard API v1.

Phase-gated architecture
The dashboard is organized into three activation phases. Phase 1 covers read-only inventory and status. Phase 2 adds alert and event polling. Phase 3 introduces write actions (reboot, blink LEDs, tag management, firmware scheduling). Each API function has the real fetch() call commented directly above the demo fallback — uncomment one line to go live per function.
Multi-org fan-out
The dashboard is designed for MSP multi-tenancy. One API key fetches all organizations via GET /organizations, then fans out with Promise.allSettled() to fetch devices, statuses, and firmware per org in parallel. The org filter dropdown (Tab 1 matrix, Tab 3 explorer) scopes all data to a single org.
Rate-limit aware
apiFetch() handles Meraki's 429 responses using the Retry-After header, returns cached data if available, and retries with exponential backoff (×3). All responses cache for 60 seconds (CACHE_TTL) to stay within 10 req/sec limits.
02 //Architecture
apiFetch(path, opts, ttl) // core helper — cache + 429 + retry └─ cache check (60s TTL) └─ fetch with X-Cisco-Meraki-API-Key header └─ 429 → Retry-After → cached fallback └─ error → exponential backoff × 3 Phase 1 (GET — read-only inventory) api_getOrgs() GET /organizations api_getDevices(orgId) GET /organizations/{orgId}/devices api_getStatuses(orgId)GET /organizations/{orgId}/devices/statuses api_getFirmware(orgId)GET /organizations/{orgId}/firmware/upgrades Phase 2 (GET — alerts and events) api_getAlerts(orgId) GET /organizations/{orgId}/alerts/overview api_getEvents(netId) GET /networks/{networkId}/events api_getVPN(orgId) GET /organizations/{orgId}/appliance/vpn/statuses Phase 3 (POST/PUT — write actions, confirm modal required) api_reboot(serial) POST /devices/{serial}/reboot api_blinkLeds(serial) POST /devices/{serial}/blinkLeds api_updateTags(serial)PUT /devices/{serial} initAll() renderKPIs() → renderOrgRisk() → renderC1Events() → renderMatrix() renderEvents() → filterDevs() Intervals: renderMatrix 60s renderEvents 30s tickLog 3.2s loadCVEs 900s (15 min)
FunctionRole
apiFetch(path, opts, ttl)All API calls route through this. Adds X-Cisco-Meraki-API-Key header, checks 60s response cache, handles 429 with Retry-After, retries up to 3× with exponential backoff.
initAll()Master render entry point. Calls all render functions in sequence. Triggered on load, Ctrl+R, and after API key submission.
renderKPIs()Populates rail stats, KPI bar, firmware breakdown, quick-action widgets, and the Alerts badge on Tab 2.
renderMatrix()Renders the device dot matrix filtered by org dropdown. Runs every 60s. Each dot = one Meraki device with status-coded color and tooltip showing full device metadata.
renderOrgRisk()Populates the Org Risk Map in the right panel with risk score bars per org.
renderEvents()Renders Alert Stream on Tab 2 with active filter and search applied. Runs every 30s.
filterDevs()Applies search, org filter, and sort state to DEVICES array and renders the device explorer table on Tab 3.
loadCVEs()Fetches from NVD API (keywordSearch=meraki). Falls back to hardcoded known CVEs on network failure. Runs every 15 minutes.
tickLog()Cycles through LOGMSGS[] every 3.2s and appends one line to the Dashboard Log in Tab 2's sidebar.
populateFilters()Injects org options into the #fOrg and #matFilter select elements from ORGS[] on load.
03 //UI Layout
ZoneHeight / SizeDescription
Rail32px fixed topBrand logo, 5 live stats, scrolling API ticker, live clock, connection mode indicator
Tab nav36px fixed below rail3 tabs (Fleet Health, Alerts & Events, Device Explorer), API badge, API Key button
AppRemaining viewportFixed-position container holding 3 consoles, only active one shown at a time
Console 1Grid: 1fr 1fr 290px, 4 rowsPlatform health, KPI bar, device matrix, recent alerts, quick actions, env score + org risk + CVE
Console 2Grid: 1fr 310pxAlert stream with filter bar and search, plus sidebar with counts, dashboard log, alert type breakdown
Console 3Flex columnSearch + filter bar, sortable device table with inline row actions, floating bulk-action bar
ToastsBottom-left stackNon-blocking feedback for all actions. 4.2s auto-dismiss. Types: ok (green), w (amber), e (red)
ModalsFullscreen overlayAPI key entry (launch), reboot confirm, blink LEDs confirm — all use the .mover / .mbox classes
04 //Rail & Ticker

The 32px rail is fixed to the top of the viewport and provides a persistent at-a-glance status bar. All five stat chips are populated by renderKPIs() on every initAll() call.

ElementIDValueColor
Total Devices#rsTotalDEVICES.lengthCyan
Online#rsOnlineDevices with status === 'online'Cyan
Needs Upgrade#rsUpgDevices with upg === trueAmber
Alerts#rsAlertsEvents with sev critical or high, blinks on critRed (blinks)
Orgs#rsOrgsORGS.lengthCyan
Ticker#rticker55s CSS scroll loop. Cyan dots = GET (Phase 1/2). Orange dots = POST (Phase 3). Hover pauses.
Clock#rtime24h live clock, updates every 1sCyan
Mode#rmodeDEMO or LIVE — set by loadDemo() / submitApi()
05 //Tab 1 — Fleet Health

The default view. Four panels: Platform Health (4 API health cards), KPI Bar (6 metrics), Device Matrix, and a right column with Env Score + Org Risk Map + CVE Widget. Plus Recent Alerts and Quick Actions in the lower grid.

Platform Health Cards
4 cards showing status of each API surface: Meraki Dashboard API (GET /organizations), Device Status (GET .../devices/statuses), Firmware Upgrades (GET .../firmware/upgrades), Alert Engine (GET .../alerts/overview). Pills show DEMO / READY / ACTIVE badges.
KPI Bar — 6 Tiles
Online devices (with total and % bar), Active Alerts (critical count), Organizations count, Networks count, Needs Upgrade count (links to firmware info toast), Patch Compliance % (computed as (total - upgrades) / total × 100). All animated via CSS transition on width.
Device Matrix
One circle per device. Color-coded: ● Online ▲ Alerting ! Critical blue = Needs Upgrade, dim = Offline. Hover shows full tooltip (name, org, model, firmware, status, network, serial, MAC, tags). Click navigates to Tab 3. Org filter dropdown scopes the matrix. Refreshes every 60s.
Environment Score Ring
SVG arc gauge showing composite score (hardcoded 77 in demo, computed from platform scores in live mode). Sub-scores: Security (85) and Patching (69). Score and arc color scale: green ≥ 80, amber ≥ 60, red below.
Org Risk Map
Rendered by renderOrgRisk(). One row per org with a colored risk bar (0–100 scale). Colors come from each org's col field in ORGS[]. Clicking any row navigates to Tab 3.
Quick Actions
Stage Firmware Upgrades (toast), Export Network Configs (toast), Device Explorer link, Manage Tags (toast). Below: Firmware Breakdown by product/version rendered by renderKPIs(), and Network Health summary (wireless clients count, VPN tunnel count, Content Filter status, IDS/IPS gap count).
06 //Tab 2 — Alerts & Events

Live alert stream powered by GET /networks/{networkId}/events with filter buttons and free-text search. Right sidebar shows critical/high counts, a cycling dashboard log, and an alert type breakdown by percentage.

Filter Buttons
All / Critical / High / VPN / Connectivity / Security. Mutually exclusive. Active state uses cyan border + background. setEvFilter(f, btn) sets evFilter var and re-renders. Free-text search via setEvSearch(v) filters on title + device + org combined.
Alert Cards
Left border stripe by severity: red = critical, amber = high, cyan = medium, dim = info. Each card shows: severity badge, category badge, title, org/network/device/age row, API endpoint reference, and contextual action buttons (Check Uplinks for connectivity, VPN Status for vpn, Review for security, Ticket for all, Ack for critical).
Dashboard Log
Cycles through LOGMSGS[] every 3.2s via tickLog(). Each entry shows a color-coded severity prefix (INFO / WARN / CRIT) and a log message. Simulates real-time API activity logs. In live mode, wire to your aggregated API call log.
07 //Tab 3 — Device Explorer

Sortable, searchable, filterable table of all 82 devices. Full columns: checkbox, name, org badge, network, product type, model, serial + MAC, firmware version (color-coded), LAN IP, tags, last seen, status, row actions.

Sorting
Click any column header to sort ascending, click again for descending. State held in dSortK and dSortAsc. sortD(k) updates state and calls filterDevs().
Firmware Badge Colors
CURRENT = on latest firmware. WARN = one version behind (starts with 18.x / MR 30 / MS 16). OUTDATED = older than that. Latest versions: LATEST_MX = '18.211.2', LATEST_MR = 'MR 30.7', LATEST_MS = 'MS 16.1'.
Row Actions (hover-reveal)
📡 Status — toasts the GET /organizations/{orgId}/devices/statuses → {serial} call. 🚀 Upgrade — only shown on devices with upg === true, stages firmware. ↺ Reboot — opens confirm modal → api_reboot(serial). 💡 Blink — opens confirm modal → api_blinkLeds(serial).
Bulk Selection + Float Bar
Checkbox in header selects all. Per-row checkboxes add to selection. When any devices are selected, a floating action bar appears at the bottom center showing selection count with Upgrade, Reboot, Blink LEDs, CSV Export, and Clear actions.
CSV Export
exportCSV() exports selected devices (or all if none selected). Columns: Name, Org, Network, Product, Model, Serial, MAC, Firmware, IP, Tags, Status, LastSeen. Downloads as meraki-devices.csv.
08 //API Key Modal

Shown on launch (before loadDemo()) and accessible via the ⚙ API Key button in the tab nav right corner. Contains three fields:

FieldIDNotes
API Key#mkKey40-character hex string. Dashboard → Profile → API access → Generate API key. Stored in MKcfg.key in memory only.
Organization ID#mkOrgOptional. Leave blank to fetch all orgs. If set, only that org's data is fetched.
Base URL#mkBaseDropdown: Global (api.meraki.com) or EU (api.eu.meraki.com). Stored in MKcfg.base.

submitApi() stores credentials to MKcfg, attempts a connection toast, then falls back to demo after 900ms (since real fetch is still commented out). loadDemo() closes the modal, shows the offline bar, and calls initAll().

09 //Modals — Reboot & Blink LEDs
Reboot Modal (#rMod)
Triggered by showReboot(name). Shows device name, two safety checkboxes (user impact confirmed, within maintenance window). Confirm does not currently validate checkbox state — add that guard when going live. Confirm calls confirmReboot()api_reboot(serial, name). Real API: POST /devices/{serial}/reboot.
Blink LEDs Modal (#blinkMod)
Triggered by showBlink(name). Non-disruptive — blinks physical LEDs for 30s for visual identification. Confirm calls confirmBlink()api_blinkLeds(serial, name). Real API: POST /devices/{serial}/blinkLeds with body {"duration":30,"period":1,"duty":50}.
10 //Phase 1 — Inventory & Status
FunctionMethodEndpointReturns
api_getOrgs()GET/organizationsArray of org objects with id, name, url
api_getDevices(orgId)GET/organizations/{orgId}/devicesDevice array — name, serial, mac, model, networkId, tags, productType. Note: MAC is primary identifier; serial used for management actions.
api_getStatuses(orgId)GET/organizations/{orgId}/devices/statusesStatus array — serial, status (online/offline/alerting/dormant), lastReportedAt, publicIp, lanIp
api_getFirmware(orgId)GET/organizations/{orgId}/firmware/upgradesavailableVersions[], upcomingUpgrades[], scheduledUpgrades[]
Multi-org fan-out pattern
In live mode, call GET /organizations first, then fan out: Promise.allSettled(orgs.map(o => api_getDevices(o.id))). Use allSettled (not all) so one failing org doesn't block the rest. Merge results with org metadata before rendering.
11 //Phase 2 — Alerts & Events
FunctionMethodEndpointNotes
api_getAlerts(orgId)GET/organizations/{orgId}/alerts/overviewReturns counts by severity: critical, high, informational. Also try /alertProfiles for configured alert rules.
api_getEvents(networkId)GET/networks/{networkId}/events?productType=appliance&perPage=50Pagination via startingAfter / endingBefore params. Meraki uses link header pagination, not cursor in body. Max 1000/page.
api_getVPN(orgId)GET/organizations/{orgId}/appliance/vpn/statusesVPN peer count, status per peer, latency. Used for Quick Actions VPN display.
Event polling rate limit
Network events are capped at 30 req/sec across all network endpoints (tighter than the org-level 10/sec). If you're fan-outing events across many networks, stagger the calls or use the org-level /alerts/overview for counts and only deep-poll on alert.
12 //Phase 3 — Write Actions
FunctionMethodEndpointBody / Notes
api_reboot(serial, name)POST/devices/{serial}/rebootNo body required. Causes ~30s downtime. Protected by confirm modal with safety checkboxes.
api_blinkLeds(serial, name)POST/devices/{serial}/blinkLedsBody: {"duration":30,"period":1,"duty":50}. Non-disruptive. Duration in seconds, period = blink interval (s), duty = on-time %.
api_updateTags(serial, tags)PUT/devices/{serial}Body: {"tags":["tag1","tag2"]}. Overwrites all tags — merge with existing before PUT if preserving tags.
Firmware schedulePOST/organizations/{orgId}/firmware/upgradesBody: {"upgradeBatchId":"...", "scheduledFor":"2024-01-15T02:00:00Z"}. Schedules an upgrade window. Not yet wired in the UI — documented for future Phase 3 expansion.
Reboot modal — add checkbox validation before going live
The current confirmReboot() does not check whether the safety checkboxes are ticked before proceeding. Before enabling the real POST /devices/{serial}/reboot call, add: if(!document.getElementById('chkUsers').checked || !document.getElementById('chkMW').checked){ showToast('e','Required','Confirm both safety checks'); return; }
13 //Rate Limits & Caching
LimitValueNotes
Org-level read10 req/sec per orgApplies to /organizations/{orgId}/* endpoints
Device management5 req/secApplies to POST/PUT on /devices/{serial}/*
Network events30 req/secTighter cap — be careful with multi-network fan-out
429 handlingRetry-After headerapiFetch() reads Retry-After, waits, serves cache if available
Cache TTL60 secondsCACHE_TTL = 60000. Keyed by URL + request body. Shared across all calls in session.
Exponential backoff400ms, 800ms, 1600msNon-429 errors retry up to 3×. Formula: (2**i) * 400
14 //Documented Limitations
Reboot modal lacks checkbox enforcement
Safety checkboxes are present but confirmReboot() does not validate them. Add the guard before enabling the real POST call — see Section 12 callout for the exact code.
CORS — cannot call Meraki API directly from browser
The Meraki Dashboard API does not include CORS headers that allow browser-origin requests. All apiFetch() calls must route through a backend proxy that adds your API key server-side. The current commented-out real calls will fail with CORS errors if run directly from a browser page. Deploy a simple Node/Express proxy or use Cloudflare Workers.
Environment score is static in demo
The SVG arc ring shows a hardcoded score of 77 in the HTML. In live mode, compute the score from DEVICES array metrics: patch compliance %, online %, alert count, etc. The sub-scores (Security: 85, Patching: 69) are also static and need to be wired to real data.
Tag update overwrites — no merge
api_updateTags(serial, tags) uses PUT /devices/{serial} which replaces all tags. If you need to add a tag without removing existing ones, first call GET /organizations/{orgId}/devices, find the device, read device.tags, merge your new tag in, then PUT the merged array.
Firmware scheduling not wired
POST /organizations/{orgId}/firmware/upgrades for scheduling upgrade windows is documented in the code comments and KB but has no UI button yet. The "Stage Firmware Upgrades" quick action toasts only. Add a firmware schedule modal as the next Phase 3 addition.
MG (cellular gateway) devices appear in data but no model-specific columns
MG devices are in the PRODS pool and appear in the matrix and explorer, but MG-specific firmware versions and cellular-specific status fields (connectionType, signalStat) are not displayed. Add MG-specific columns to the explorer table if your fleet includes MG hardware.
15 //CVE Widget

The bottom of the right panel on Tab 1 shows a live CVE feed for Meraki hardware pulled from the NVD (National Vulnerability Database) API. It refreshes every 15 minutes and falls back to a hardcoded list of known Meraki CVEs when the network is unavailable.

DetailValue
API endpointGET https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=meraki&resultsPerPage=5
Auth requiredNone for public access (rate limited). Optional apiKey query param for higher limits.
Score thresholdsCVSS ≥ 9.0 = CRIT · ≥ 7.0 = WARN · below = LOW
Fallback CVEsCVE-2023-20269 (9.1), CVE-2023-20128 (8.8), CVE-2022-20857 (9.8), CVE-2022-20931 (7.2), CVE-2022-20827 (9.0)
Refresh interval900 000ms (15 min) — setInterval(loadCVEs, 900000)
Offline indicator"⚠ Offline — known CVEs shown" appended to widget when NVD is unreachable
16 //Activation Checklist
1
Deploy a backend proxy to handle CORS
Create a Node/Express proxy (or Cloudflare Worker) that forwards requests to https://api.meraki.com/api/v1/{path} with the X-Cisco-Meraki-API-Key header added server-side. Point MKcfg.base to your proxy URL instead. This is required — direct browser-to-Meraki calls are blocked by CORS.
2
Generate a Meraki Dashboard API key
In Meraki Dashboard: My Profile → API access → Generate new API key. Key is 40 hex chars. Copy it — it's only shown once. The key needs read access for Phase 1–2, and write access for Phase 3 actions.
3
Uncomment Phase 1 real calls
In api_getOrgs(), api_getDevices(), api_getStatuses(), and api_getFirmware() — each has a commented return await apiFetch(...) line directly above the demo fallback. Uncomment that line and remove or comment out the demo return below it.
4
Uncomment Phase 2 real calls
Same pattern in api_getAlerts(), api_getEvents(), api_getVPN(). For events, decide whether to fan-out per network or poll org-level alerts only — see Section 11 rate limit note.
5
Add checkbox validation to reboot modal
Before uncommenting the Phase 3 POST calls, add checkbox guard to confirmReboot(). See Section 12 callout for exact code. This prevents accidental reboots when safety checks are skipped.
6
Uncomment Phase 3 write actions
In api_reboot(), api_blinkLeds(), and api_updateTags() — uncomment the real await fetch(...) blocks. The showToast() fallback below each block can remain as a success confirmation, just reposition it to fire after the real call succeeds.
7
Update rmode indicator
Change document.getElementById('rmode').textContent = 'DEMO' to 'LIVE' in loadDemo(), or add a separate live path in submitApi() that sets it to 'LIVE' on successful connection.
17 //Keyboard Shortcuts
KeyAction
1Switch to Tab 1 — Fleet Health (when not focused in input)
2Switch to Tab 2 — Alerts & Events
3Switch to Tab 3 — Device Explorer (auto-focuses search input)
Ctrl+R / ⌘RRefresh all panels — calls initAll(), toasts "Refreshed"
EscClose all open modals (.mover.open)
18 //Troubleshooting
CORS error on API calls
Meraki Dashboard API does not serve browser-permissive CORS headers. All calls must go through a server-side proxy. If you see CORS policy blocked in DevTools, the proxy is not deployed or MKcfg.base is still pointing directly to api.meraki.com.
429 Rate Limited toast appears repeatedly
apiFetch() shows a toast on every 429. If you're seeing this on load, you have too many concurrent org-level calls. Stagger api_getDevices() calls with a small delay between orgs, or reduce setInterval(renderMatrix, 60000) to a longer interval.
Matrix shows all devices as Offline after org filter
renderMatrix() reads document.getElementById('matFilter').value for org filter. If the select hasn't been populated yet when it runs (race between populateFilters() and initAll()), all devices match no org and show offline. Verify populateFilters() runs before loadDemo() / initAll() — it does in the current init order.
Float bar stays visible after CSV export
exportCSV() does not call clearSel() after exporting. Call clearSel() at the end of exportCSV() if you want the selection to reset after export.
CVE widget shows "Offline — known CVEs shown"
NVD API is rate-limited for anonymous requests and sometimes unavailable. The fallback shows 5 real Meraki CVEs from 2022–2023. If the NVD API is important for your workflow, register for a free NVD API key at nvd.nist.gov/developers/request-an-api-key and add it as the apiKey query param in loadCVEs().
Device Explorer table is empty after search
filterDevs() searches across d.name + d.org + d.network + d.product + d.model + d.serial + d.ip + d.tags.join(' '). Search is case-insensitive but requires substring match. If no results, check that the search string matches one of those fields — MAC address is not currently in the search index.

KB · Meraki MSP Command Center · stack-adv-stack-meraki.html · v1.0