// 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.
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.
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)
| Function | Role |
| 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. |
| Zone | Height / Size | Description |
| Rail | 32px fixed top | Brand logo, 5 live stats, scrolling API ticker, live clock, connection mode indicator |
| Tab nav | 36px fixed below rail | 3 tabs (Fleet Health, Alerts & Events, Device Explorer), API badge, API Key button |
| App | Remaining viewport | Fixed-position container holding 3 consoles, only active one shown at a time |
| Console 1 | Grid: 1fr 1fr 290px, 4 rows | Platform health, KPI bar, device matrix, recent alerts, quick actions, env score + org risk + CVE |
| Console 2 | Grid: 1fr 310px | Alert stream with filter bar and search, plus sidebar with counts, dashboard log, alert type breakdown |
| Console 3 | Flex column | Search + filter bar, sortable device table with inline row actions, floating bulk-action bar |
| Toasts | Bottom-left stack | Non-blocking feedback for all actions. 4.2s auto-dismiss. Types: ok (green), w (amber), e (red) |
| Modals | Fullscreen overlay | API key entry (launch), reboot confirm, blink LEDs confirm — all use the .mover / .mbox classes |
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.
| Element | ID | Value | Color |
| Total Devices | #rsTotal | DEVICES.length | Cyan |
| Online | #rsOnline | Devices with status === 'online' | Cyan |
| Needs Upgrade | #rsUpg | Devices with upg === true | Amber |
| Alerts | #rsAlerts | Events with sev critical or high, blinks on crit | Red (blinks) |
| Orgs | #rsOrgs | ORGS.length | Cyan |
| Ticker | #rticker | 55s CSS scroll loop. Cyan dots = GET (Phase 1/2). Orange dots = POST (Phase 3). Hover pauses. | — |
| Clock | #rtime | 24h live clock, updates every 1s | Cyan |
| Mode | #rmode | DEMO 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.
Shown on launch (before loadDemo()) and accessible via the ⚙ API Key button in the tab nav right corner. Contains three fields:
| Field | ID | Notes |
| API Key | #mkKey | 40-character hex string. Dashboard → Profile → API access → Generate API key. Stored in MKcfg.key in memory only. |
| Organization ID | #mkOrg | Optional. Leave blank to fetch all orgs. If set, only that org's data is fetched. |
| Base URL | #mkBase | Dropdown: 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
| Function | Method | Endpoint | Returns |
api_getOrgs() | GET | /organizations | Array of org objects with id, name, url |
api_getDevices(orgId) | GET | /organizations/{orgId}/devices | Device 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/statuses | Status array — serial, status (online/offline/alerting/dormant), lastReportedAt, publicIp, lanIp |
api_getFirmware(orgId) | GET | /organizations/{orgId}/firmware/upgrades | availableVersions[], 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
| Function | Method | Endpoint | Notes |
api_getAlerts(orgId) | GET | /organizations/{orgId}/alerts/overview | Returns counts by severity: critical, high, informational. Also try /alertProfiles for configured alert rules. |
api_getEvents(networkId) | GET | /networks/{networkId}/events?productType=appliance&perPage=50 | Pagination via startingAfter / endingBefore params. Meraki uses link header pagination, not cursor in body. Max 1000/page. |
api_getVPN(orgId) | GET | /organizations/{orgId}/appliance/vpn/statuses | VPN 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
| Function | Method | Endpoint | Body / Notes |
api_reboot(serial, name) | POST | /devices/{serial}/reboot | No body required. Causes ~30s downtime. Protected by confirm modal with safety checkboxes. |
api_blinkLeds(serial, name) | POST | /devices/{serial}/blinkLeds | Body: {"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 schedule | POST | /organizations/{orgId}/firmware/upgrades | Body: {"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
| Limit | Value | Notes |
| Org-level read | 10 req/sec per org | Applies to /organizations/{orgId}/* endpoints |
| Device management | 5 req/sec | Applies to POST/PUT on /devices/{serial}/* |
| Network events | 30 req/sec | Tighter cap — be careful with multi-network fan-out |
| 429 handling | Retry-After header | apiFetch() reads Retry-After, waits, serves cache if available |
| Cache TTL | 60 seconds | CACHE_TTL = 60000. Keyed by URL + request body. Shared across all calls in session. |
| Exponential backoff | 400ms, 800ms, 1600ms | Non-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.
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.
| Detail | Value |
| API endpoint | GET https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=meraki&resultsPerPage=5 |
| Auth required | None for public access (rate limited). Optional apiKey query param for higher limits. |
| Score thresholds | CVSS ≥ 9.0 = CRIT · ≥ 7.0 = WARN · below = LOW |
| Fallback CVEs | CVE-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 interval | 900 000ms (15 min) — setInterval(loadCVEs, 900000) |
| Offline indicator | "⚠ Offline — known CVEs shown" appended to widget when NVD is unreachable |
16 //Activation Checklist
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.
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.
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.
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.
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.
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.
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.
| Key | Action |
| 1 | Switch to Tab 1 — Fleet Health (when not focused in input) |
| 2 | Switch to Tab 2 — Alerts & Events |
| 3 | Switch to Tab 3 — Device Explorer (auto-focuses search input) |
| Ctrl+R / ⌘R | Refresh all panels — calls initAll(), toasts "Refreshed" |
| Esc | Close all open modals (.mover.open) |
⊗
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