KNOWLEDGE BASE // SONICWALL MSP COMMAND CENTER
SonicWall MSP Command Center
A multi-vendor firewall operations console for MSP engineers. Manages SonicWall, FortiGate, and Meraki fleets from a single browser-based cockpit — covering fleet health, live triage, bulk firmware operations, and per-device deep inspection across 212 demo devices and 24 tenants.
4 Console Views
3 Vendor APIs
Live CVE + Threat Intel Widgets
AES-GCM API Key Encryption
01Overview

The SonicWall MSP Command Center is a single-file HTML console designed for MSP engineers managing multi-tenant firewall environments. It requires no build process, no backend server in demo mode, and no external framework beyond Chart.js loaded from CDN.

The console operates in two modes: Demo Mode (default — all device data is generated from TENANTS, MODELS, and VERSIONS arrays at page load) and Live Mode (API key entered → NSM token generated → real data replaces demo arrays). Switching requires no code change — just connecting an API key.

Multi-Vendor by Design
The console is not SonicWall-only. It manages 212 devices across three vendors — 142 SonicWall (NSM), 38 FortiGate (FortiManager JSON-RPC), and 32 Meraki (Dashboard API v1) — through a unified vendor abstraction layer. Every vendor's data is normalized to the same device object shape before rendering.
Primary Audience

MSP L2/L3 network engineers managing SonicWall NSM, FortiManager, or Meraki Dashboard fleets. Shift leads running firmware campaigns across multiple tenants. NOC engineers triaging real-time firewall alerts during incidents.

What It Replaces

Jumping between NSM Portal, FortiManager UI, Meraki Dashboard, and your PSA to answer: which devices need upgrades, which tenants have active alerts, and which firewalls can be actioned right now.

02Architecture & Dependencies

The file has one external script dependency and three Google Fonts. All state, rendering, and API logic is vanilla JavaScript inside a single <script> block. There is no module system and no build step.

DependencySourceRequired ForIf Unreachable
Chart.js 4.4.1cdnjs.cloudflare.comEnvironment donut chart in the C1 right panel (donut showing 92% / 74% dual-ring). The only chart in the file.Donut renders blank. All other UI works — KPI bars, tables, alerts, and all four consoles are pure CSS/HTML.
Exo 2Google FontsAll heading text, tab labels, KPI values, modal titles, platform health card names.Falls back to system sans-serif. Layout unaffected.
JetBrains MonoGoogle FontsAll monospace values — IPs, serials, versions, timestamps, log lines, rail stats, latency badge.Falls back to 'Fira Code','Courier New',monospace.
RajdhaniGoogle FontsBody text, panel descriptions, button labels, filter labels.Falls back to system sans-serif.
Self-Host for Production
For deployments without reliable internet access, download Chart.js 4.4.1 from the Chart.js GitHub releases and host it locally. Update the <script src> tag to a relative path. Fonts can be served from a local web server or bundled using the Google Fonts CSS download tool.

Page Structure

The layout is three stacked fixed-position layers: the 32px Rail at the top, the 36px Tab Bar directly below it, and the App container filling the remaining viewport. The App container uses position:fixed; top:calc(32px + 36px) and holds four console divs that swap visibility via switchConsole(n).

CSS LAYOUT VARIABLES
--rail-h: 32px;   /* top ticker bar height */
--tabs-h: 36px;   /* console tab bar height */

/* App viewport starts below both bars: */
#app { top: calc(var(--rail-h) + var(--tabs-h)); }
03Layout & Navigation

Navigation between the four consoles is handled by switchConsole(n) where n is 0-based (0 = Fleet Health, 1 = Live Triage, 2 = Bulk Ops, 3 = Device Explorer). The function toggles the .active class on both the .console divs and the .tab-btn buttons simultaneously.

ConsoleIndexElement IDDisplay ModeKey Shortcut
Fleet Health0#c1display:grid — 3-column layout1
Live Triage1#c2display:grid — feed + right sidebar2
Bulk Ops2#c3display:grid — incident list + action area3
Device Explorer3#c4display:flex; flex-direction:column4
Focus on Console Switch
Switching to Console 4 (Device Explorer) automatically focuses the search input: document.getElementById('devSearch').focus(). This is intentional — the Device Explorer is primarily a keyboard-driven search surface. Tab, type, filter, act.

Responsive Breakpoints

The console is designed for 1400px+ wide screens. Below key breakpoints, panels collapse:

≤ 1400pxC1 right panel narrows to 280px. KPI value font sizes reduce.
≤ 1200pxC1 collapses to 2-column, C2 right sidebar hides, C3 incident context panel hides.
≤ 1024pxRail stats hide. Tab hotkey hints hide. C1 becomes single column. C1 right panel hides.
≤ 768pxAll consoles collapse to single column. KPI strip shows only 2 tiles.
04Rail & Status Bar

The Rail is the 32px fixed bar at the very top of the page. It serves as the always-visible operational status strip — engineers can read current fleet health without leaving any console view.

Rail ElementID / ClassContentBehavior
Logo.rail-logoMSP·CMD brand text with cyan glowStatic
Devices Online#railOnline / #railTotalLive count from DEVICES arrayClickable → switchConsole(0)
Active Alerts.rail-statCount of P1+P2 alerts, red with blink animation when criticalClickable → switchConsole(1)
Outdated Firmware.rail-statCount of devices not on latest versionClickable → switchConsole(3)
Ticker.rail-tickerScrolling 60s loop of device/version status blips. Pauses on hover with "▶ PAUSED" indicator.Auto-scroll, hover to pause
Latency Badge#nsmLatencyBadgeSimulated NSM API round-trip in ms. Updates every 15s. Green <40ms, amber 40–80ms, red >80ms.pingNSM() every 15s
Rate Meter#rateTickAPI requests consumed vs. limit. Fed by updateRateLimit(remaining, limit).Populated from real API response headers in live mode
Clock#railTime24-hour local time. Updates every second via updateClock().Live, 1s interval
OPS Queue toggle#opsToggleBtnActive operations count. Turns cyan when jobs are running.Opens/closes OPS Queue panel
Theme toggle#themeBtn◑ THEME / ◑ STANDARDToggles body.theme-classic
NSM ConnectRail right buttonOpens API Key modalshowApiModal()

Rail Stat Color Logic

.rail-stat-valGreen (default) — healthy value, no action needed.
.rail-stat-val.warnAmber — elevated, monitor. Applied via JS class toggle.
.rail-stat-val.critRed + blinking — immediate attention. The blink animation uses step-end for a hard flash rather than a fade.
05Console 1 — Fleet Health

Fleet Health is the default landing console. Its grid layout is grid-template-columns: 1fr 1fr 300px — two fluid center columns and a fixed 300px right panel. It contains five distinct zones.

Platform Health Cards

The four .ph-card tiles across the top row show the health of each integrated vendor platform: SonicWall NSM, FortiManager, Meraki Dashboard, and MySonicWall licensing. Each card shows a status icon, platform name, and a status string. Cards are hardcoded in HTML — update them to reflect live API connection status when going live.

KPI Summary Bar

The KPI bar spans the full width below the platform cards. Seven KPI items each have a color-coded top accent, an animated progress fill bar, and a trend indicator. All seven values are driven by data-* attributes on the .kpi-item element and populated by updateKpiSummary().

KPIdata-colordata-valuedata-totalTrend
Devices OnlinegreenLive count from DEVICESTotal DEVICES.lengthup
Active AlertsamberP1+P2 alert count20 (scale)down
Managed Tenantscyan2330up
Patch Compliancegreen87100up
HA Pairsblue3131neutral
Firmware 7.1.2 (Pending)amber71 (pct)100down → triggers upgrade modal
VPN Tunnelspurple282284neutral
KPI Color Map
The KPI fill gradient is generated from a pre-resolved colorMap object inside updateKpiSummary(). This replaces the original color-mix(in srgb, var(--cyan) 50%, transparent) call which failed silently on some Chromium versions. If you add a new KPI item with a custom color, add the hex value to the colorMap object first.

Fleet Device Matrix

The center panel renders every device in the fleet as a small status dot via renderFleetMatrix(). Each dot's class and label character encode its state:

Dot ClassLabelCondition
.onlineStatus Online, no active P1/P2 alert, firmware current
.warnStatus Warning OR CPU > 80% OR RAM > 85%
.crit!Has an active P1 or P2 alert matching device name
.upgradeFirmware does not start with 7.1.3 (SonicWall) — needs upgrade
.offlineStatus is 'offline' or 'down'

The matrix refreshes every 30 seconds via setInterval(renderFleetMatrix, 30000) and also fires 400ms after page load. A tenant filter dropdown above the matrix lets engineers isolate a single client's device group. Hovering a dot shows a tooltip with device name, tenant, model, firmware version, and status.

Right Panel

The fixed 300px right panel contains four stacked components:

Env DonutChart.js doughnut with two concentric rings — outer (green, 92%) for Security score, inner (amber, 74%) for Patching score. Rendered by initCharts().
Quick AlertsTop 4 alerts from the ALERTS array, rendered by renderC1Alerts(). Each row shows severity badge, title, device name, and age. Clicking navigates to Console 2.
Tenant Risk MapHorizontal bar chart per tenant showing a risk score (0–100). Higher score = more risk. Rendered by renderRiskMap() with hardcoded tenant data. Clicking any row navigates to Console 3.
Top VulnerabilitiesCVE feed loaded by loadVulns() from the NVD API with a curated static fallback. See Section 12.
06Console 2 — Live Triage

Live Triage is the incident response console. It is a two-column layout: a scrollable alert feed on the left and a fixed 340px right sidebar with stats, the NSM system log, alert distribution, and the threat intelligence widget.

Alert Stream

Alerts are rendered by renderAlerts() from the ALERTS array. Each alert card shows severity badge, title, tenant, device, source system, ticket ID, and age. The bottom of each card has four action buttons:

🚫 Isolate VPNFires showToast('warn', 'VPN Isolated', ...). In production: POST to NSM API to disable IPsec tunnels on the device.
🛑 Block IPFires showToast('warn', 'IP Blocked', ...). In production: POST to NSM block list API.
🎫 Create TicketFires toast with a random ticket number. In production: POST to ConnectWise or Autotask API.
🚀 Upgrade FirewallOnly shown on alerts where a.upgrade === true. Opens the firmware upgrade modal.

Alert Filters

Filter buttons call filterAlerts(f, btn) with values: 'all', 'P1', 'P2', 'P3', 'upgrade'. The 'upgrade' filter checks a.upgrade === true on each alert object. The search input calls searchAlerts(value) which does a case-insensitive substring match on a.title and a.device.

NSM System Log

The log feed is rendered by renderNSMLog() which rotates through the LOG_MSGS array every 2.8 seconds, prepending new lines and trimming the feed to 40 entries. Each line has a timestamp, severity token [INFO/WARN/CRIT], and message text. This simulates a live syslog tail — in production, replace the interval with a WebSocket subscription to the NSM event stream.

Right Sidebar Stats

The four stat boxes (#c2-p1, #c2-p2, #c2-resolved, and Avg MTTR) are hardcoded in HTML. Update #c2-p1 and #c2-p2 dynamically from the ALERTS array in production. The Avg MTTR value of ~23m is static demo text.

07Console 3 — Bulk Ops

Bulk Ops is the command-and-control console. It has a fixed 300px left panel for incident context and a fluid right area with two subtabs switchable via switchSubtab(n). Subtabs are also accessible via Ctrl+1 and Ctrl+2 while Console 3 is active.

Incident Context Panel

The left panel renders incidents from the INCIDENTS array via renderIncidents(). Clicking an incident card calls selectIncident(i, card) which highlights the card and populates the Affected Devices list below with the incident's device names. Right-clicking any affected device opens the context menu targeting that device.

Subtab 0 — Security & Config

Six action cards trigger specific operations. All six fire showToast() in demo mode. In production, wire the onclick of each card's button to the relevant API call:

ActionDemo BehaviorProduction Target
Push Security PolicyToast: "DPI-SSL template queued"NSM: POST /api/devices/policy with template ID
Enable DPI-SSLToast: "DPI-SSL enabled"NSM: PUT /api/devices/{id}/dpi-ssl
SD-WAN TemplateToast: "SD-WAN template applied"NSM: POST /api/devices/sdwan
Isolate VPN TunnelToast warningNSM: DELETE /api/devices/{id}/vpn/tunnels/{tunnelId}
Block Source IPToast: "IP added to block list"NSM: POST /api/devices/{id}/blocklist
Create Ticket + LogsToast: "Ticket #47291 created"PSA API: ConnectWise POST /service/tickets

Below the action cards, a Bulk Config Status table rendered by renderConfigTable() shows the first 12 devices with randomized policy, DPI-SSL, and SD-WAN status badges. In production this table should be populated from the NSM bulk configuration endpoint.

Subtab 1 — Firmware & Maintenance

The firmware tab has three sections:

Mass Upgrade CTALarge button calls showUpgradeModal(). The modal reads the count of currently selected devices from DEVICES.filter(d=>d.selected).length and falls back to 41 if none are selected.
Quick CardsStaged Rollout → showStagedModal(). Upgrade All Ready → toast. Upload Custom .sig → toast info.
Firmware TableRendered by renderFwTable(filter). Shows all DEVICES with their current version vs. target 7.1.3-7020, a progress bar (random in demo), and per-row action buttons: ⬆ Now, 💾 backup, ↺ reboot. Checkbox column feeds toggleAllFw() and updateFwSel().
08Console 4 — Device Explorer

Device Explorer is the full-fleet inventory and search surface. It is the only console with a flex-direction:column layout rather than grid — search bar and filter controls are fixed at the top, and the scrollable table or card grid fills the remainder.

Vendor Filter Tabs

Four tabs above the search bar filter the device list by vendor: ALL, SONICWALL, FORTIGATE, MERAKI. Clicking calls setVendorFilter(v, btn) which sets activeVendorFilter and re-runs filterDevices(). The vendor count labels to the right of the tabs are updated by updateVendorCounts().

Filter Controls

Five filter inputs all call filterDevices() on change. The filter function applies each condition sequentially — all five must pass for a device to appear:

#devSearchSubstring match on name + tenant + ip + serial + model (lowercased). Fires on every input event.
#filterTenantExact match on d.tenant.
#filterModelExact match on d.model.
#filterVerExact match on d.version.
#filterHAExact match on d.ha: Primary, Secondary, or Standalone.

Table View vs Card View

setDevView('table') and setDevView('card') toggle between the two render modes. Table view (#tableView) renders via renderDevTable(devs) — a full 12-column sortable table. Card view (#cardView) renders via renderDevCards(devs) — a responsive grid of device tiles with hover-reveal action buttons.

Table column headers that call sortDevices(key) toggle ascending/descending on the same key. The current sort state is tracked in devSortKey and devSortAsc.

Floating Action Bar

The #floatBar appears at the bottom of the console when one or more devices are selected (d.selected === true). It shows the count and six bulk action buttons. When more than 3 devices are selected, it gains the .pulse-glow class for a pulsing cyan border animation to draw attention to the pending bulk operation.

Selection state is stored directly on each device object as d.selected. toggleDev(id, checked) updates a single device. toggleSelectAll(checked) updates all devices and re-runs filterDevices(). clearSelection() resets all to false.

Export CSV

exportCSV() builds a CSV from either the selected devices (if any) or the full DEVICES array. It uses a data: URI download via a synthetic anchor element. Columns: Name, Tenant, Model, IP, Serial, Version, HA, Status, LastSeen.

Context Menu

Right-clicking any device row or dot triggers showCtxMenu(event, deviceName) which positions the #ctxMenu element at the cursor. Eight actions are available: View in NSM, Upgrade Firmware, Backup Config, Reboot, Isolate VPN, Block Source IP, Create Ticket, and Force 7.1.3-7020. All call ctxAction(action) which fires the appropriate toast with an action-specific severity level.

09Data Model

All device, alert, and incident data is generated or defined in a single <script> block. The three vendor device arrays are generated independently and then merged via DEVICES.push(...FG_DEVICES, ...MRK_DEVICES).

Unified Device Object Shape

Every device — regardless of vendor — is normalized to this shape before being pushed into DEVICES. The vendor abstraction layer's _normalizeDevice() method is responsible for the mapping.

DEVICE OBJECT // Unified shape post-normalization
{
  id:           1,                    // unique integer across all vendors
  vendor:       'sonicwall',          // 'sonicwall' | 'fortigate' | 'meraki'
  name:         'ACME-FW-001',
  tenant:       'ACME Corp',
  model:        'TZ470',
  ip:           '192.168.14.22',
  serial:       'C0AEK9X2SW',
  version:      '7.1.2-6501',         // firmware version string
  firmware:     '7.1.2-6501',         // alias — set by _normalizeDevice()
  ha:           'Primary',            // 'Primary' | 'Secondary' | 'Standalone'
  status:       'Online',             // 'Online' | 'Warning' | 'Offline'
  lastSeen:     '14m ago',
  upgradeReady: true,                 // version !== latest for this vendor
  patchOk:      true,
  selected:     false,               // managed by toggleDev() — not persisted

  // FortiGate-specific (added by FortiGateAPI._normalizeDevice):
  adom:         'root',
  vdom:         'root',

  // Meraki-specific (added by MerakiDashboardAPI._normalizeDevice):
  orgId:        'L_abc123456',
  networkId:    'N_def789012',
  tags:         ['MSP', 'Managed'],
}

Fleet Size by Vendor

VendorArrayCountID RangeLatest Version
🛡 SonicWallDEVICES (base)1421–1427.1.3-7020
🔥 FortiGateFG_DEVICES38200–2377.4.3
☁ MerakiMRK_DEVICES32300–33118.211.2
Total fleetDEVICES (merged)212

Alert and Incident Arrays

The ALERTS array contains 8 hardcoded alert objects. Alert objects have: id, sev (P1/P2/P3), tenant, device, title, age, source, and upgrade (boolean — controls whether the "Upgrade Firewall" button appears on the alert card).

The INCIDENTS array contains 3 incident objects used by Console 3. Each has: id, sev, title, tenant, devices (array of device name strings), and desc.

10Vendor Abstraction Layer

The vendor abstraction layer provides a consistent interface for querying any supported firewall platform. All three vendor classes extend VendorAPI and override its abstract methods. In demo mode every method returns from the static DEVICES/ALERTS arrays. In live mode the commented-out fetch() calls go live.

VendorAPI Base Class

The base class provides shared infrastructure that all vendor implementations inherit:

_cached(key, ttl)Returns cached result if age is within TTL (milliseconds). SonicWall uses 60s cache, Meraki 30s (per their rate limit guidance).
_setCache(key, val)Stores a value with current timestamp. Used in all getDevices() implementations.
_fetch(url, opts, retries, backoff)Fetch wrapper with exponential backoff retry (default 3 attempts, 400ms base). Handles HTTP 429 (rate limit) by waiting backoff * 2^attempt ms before retrying.
_normalizeDevice(raw, vendor)No-op in base class. Each vendor overrides this to map their API response shape to the unified device object.

Vendor Implementations

ClassAuthBase URL PatternKey Rate Limit
SonicWallNSMAPI Authorization: Bearer {token} https://nsm-{region}.sonicwall.com/api ~1000 req/hr. Cache 60s.
FortiGateAPI JSON-RPC session token in request body POST {host}/jsonrpc Varies per FortiManager instance. Cache 60s.
MerakiDashboardAPI X-Cisco-Meraki-API-Key: {key} https://api.meraki.com/api/v1 10 req/s per org. Cache 30s minimum.

Vendor Registry & Unified Refresh

The VENDOR_APIS object holds one instantiated API class per vendor. refreshAllVendors() calls all three getDevices() methods in parallel via Promise.allSettled(). Using allSettled (not Promise.all) ensures that one vendor's failure does not prevent the other two from loading.

VENDOR REGISTRY // Instantiation
const VENDOR_APIS = {
  sonicwall: new SonicWallNSMAPI({region:'uswest', token:null}),
  fortigate:  new FortiGateAPI({host:'https://fortimanager.local', token:null}),
  meraki:     new MerakiDashboardAPI({key:null}),
}

// To configure for live use:
VENDOR_APIS.sonicwall.config.token = 'your-nsm-bearer-token';
VENDOR_APIS.sonicwall.config.region = 'useast';
VENDOR_APIS.meraki.config.key = 'your-meraki-api-key';
VENDOR_APIS.fortigate.config.host = 'https://your-fortimanager.example.com';
VENDOR_APIS.fortigate.config.token = 'your-fmg-session-token';
11API Key & NSM Connection

The API Key modal (#apiModal) is auto-skipped on load in demo mode — skipApiModal() is called immediately in the initApiCheck() IIFE. To re-enable the onboarding flow for live deployment, remove or comment out the skipApiModal() call inside initApiCheck().

Key Encryption Flow

When a user submits an API key and PIN via the modal, the flow is:

  1. 01
    PIN derivation. The PIN is padded to 32 bytes and imported as a raw AES-GCM key via crypto.subtle.importKey('raw', pinBuf, 'AES-GCM', false, ['encrypt','decrypt']).
  2. 02
    Encryption. A random 12-byte IV is generated. The API key string is encrypted: crypto.subtle.encrypt({name:'AES-GCM', iv}, rawKey, encodedKey).
  3. 03
    Storage. The IV (array), ciphertext (array), and NSM region are JSON-serialized and stored in localStorage under key 'sw_msp_enc_key'. The plaintext API key is never written to storage.
  4. 04
    NSM token. simulateNSMConnect(key, region) simulates a 900ms POST to nsm-{region}.sonicwall.com/api/mssp/generatetoken and stores the resulting token in 'sw_msp_token' with a 24-hour expiry timestamp.
  5. 05
    Data load. loadFromNSM() is called to populate device and alert data from the live API. In demo mode this fires a toast and calls cacheOfflineSnapshot().
PIN Security Note
The encryption key is derived from the PIN padded to 32 bytes — not a proper PBKDF2 derivation. A 4-digit PIN has only 10,000 possible values. For production deployments where real API keys are stored, replace the PIN padding with crypto.subtle.deriveKey() using PBKDF2 with at least 100,000 iterations.

Permission Levels

setPermission('readonly') disables all .btn-success, .btn-danger, and .btn-amber buttons by setting disabled=true and opacity 40%. This is designed for API keys that have read-only scope. The permission banner (#permBanner) becomes visible with a warning message.

Offline Cache

cacheOfflineSnapshot() saves the first 50 devices and 20 alerts to localStorage as 'sw_offline_cache' with a timestamp. checkOfflineCache() reads this on load (called by skipApiModal()) and shows the offline banner with the cache age in minutes. The browser offline and online events also toggle the banner.

12Live Widgets

Three data widgets run independently on their own refresh intervals. All three are initialized on page load and retry or degrade gracefully if their data source is unavailable.

Widget 1 — Top Vulnerabilities (NVD CVE Feed)

loadVulns() fetches from https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=sonicwall&resultsPerPage=5. CVSS scores are read from cvssMetricV31 or cvssMetricV30. Color coding: ≥ 9.0 = red/crit, ≥ 7.0 = amber/warn, below = green/ok.

Static Fallback
If the NVD API is unreachable (CORS error in browser, network timeout, or API down), loadVulns() renders five curated static SonicWall CVEs including CVE-2024-40766 (CVSS 9.3, SSLVPN access control), CVE-2022-22274 (CVSS 9.4, pre-auth RCE), and three others. A note at the bottom of the widget flags that the live feed is unavailable. Refresh interval: 15 minutes.

Widget 2 — Threat Intelligence

loadThreatMap() renders a curated list of five threat IPs with country flags, attack type, confidence percentage, and report count. Each IP links to its AbuseIPDB lookup page. The data is static — replace it with a live GET https://api.abuseipdb.com/api/v2/blacklist call using your AbuseIPDB API key. Refresh interval: 10 minutes.

Widget 3 — Compliance Snapshot

loadCompliance() renders five compliance check items with status pills. Currently all five items are static hardcoded data. In production, wire this to your RMM or PSA compliance reporting endpoint. Refresh interval: 15 minutes.

WidgetFunctionContainer IDRefreshData Source
CVE FeedloadVulns()#vuln-widget15 minNVD API v2 → static fallback
Threat IntelloadThreatMap()#threat-map10 minCurated static (AbuseIPDB live optional)
ComplianceloadCompliance()#compliance-widget15 minStatic hardcoded — wire to RMM/PSA
13Modals & OPS Queue

Firmware Upgrade Modal

showUpgradeModal() opens #upgradeModal by adding the .open class. It reads the selected device count at open time and sets #modal-dev-count. The modal contains three safety requirement checkboxes, a target version selector, and a staged rollout toggle that reveals wave size options.

startUpgrade() closes the modal and fires a toast with the count of devices queued. It uses selectedStage (set by clicking wave options via selectStage(el, n)) or falls back to 41. In production, replace the toast with the actual NSM bulk upgrade API call.

Staged Rollout Modal

showStagedModal() opens #stagedModal. It displays the four rollout waves (1 → 5 → 20 → All) with wait times and risk levels. The "Start Staged Rollout" button calls both startUpgrade() and closeModal('stagedModal').

OPS Queue Panel

The OPS Queue is a floating panel (#opsQueue) that tracks background jobs — firmware upgrades, backups, policy pushes. It is toggled by toggleOpsQueue() from the rail button.

addOpsJob(name, deviceCount) creates a job entry and simulates progress by incrementing job.pct by 3–11 every 400ms via setInterval. When a job reaches 100%, it is removed from the opsJobs array after 3 seconds and the panel re-renders. If active jobs exist and the panel is closed, updateOpsBadge() auto-opens it.

Hooking Real Operations into the Queue
Call addOpsJob('Firmware Upgrade', selectedCount) at the start of any long-running operation, then clear the interval when the real API confirms completion. This gives engineers instant visual feedback without polling.

FAB (Floating Action Button)

The blue ⚡ FAB in the bottom-right corner toggles a four-item quick-action menu (#fabMenu). The four items mirror the most common keyboard shortcuts: Mass Upgrade, Bulk Backup, Refresh All, and Device Search. Clicking any FAB item also calls toggleFab() to close the menu.

Toast Notifications

showToast(type, title, msg) creates a toast element, prepends it to #toasts, and removes it after 4 seconds. Type values: 'ok' (green left border), 'warn' (amber), 'err' (red). Toasts stack with a visual offset effect — the second toast scales to 98% and the third to 96% via CSS sibling selectors.

14Keyboard Shortcuts

All shortcuts are handled by a single document.addEventListener('keydown') listener. Shortcuts are suppressed when focus is inside an INPUT, TEXTAREA, or SELECT element to prevent conflicts with typing.

Console Switching

1
Switch to Fleet Health (Console 1)
2
Switch to Live Triage (Console 2)
3
Switch to Bulk Ops (Console 3)
4
Switch to Device Explorer (Console 4) — also focuses search input

Global Actions

Ctrl+U
Open Firmware Upgrade modal
Ctrl+B
Bulk backup all devices (toast)
Ctrl+R
Refresh all consoles
Ctrl+F
Jump to Device Explorer and focus search input
Esc
Close open modal, context menu, or FAB menu

Console 3 Subtabs (only active when Console 3 is visible)

Ctrl+1
Switch to Security & Config subtab
Ctrl+2
Switch to Firmware & Maintenance subtab
15Theme Toggle & Offline Cache

Theme Toggle

toggleTheme() adds or removes the body.theme-classic class. The "NSM Classic" variant uses slightly warmer blue tones — --bg:#080f1e, --cyan:#00b8ff — that more closely resemble the NSM Portal's color palette. After toggling, initCharts() and updateKpiSummary() are called after a 50ms delay so Chart.js can re-read the updated CSS variables for the donut chart colors.

Theme is Not Persisted
The theme preference is not saved to localStorage. Refreshing the page resets to Standard Dark. To persist it, add localStorage.setItem('sw_theme', themeClassic?'classic':'standard') to toggleTheme() and read it on init.

Offline Cache

The offline cache is a localStorage snapshot of the first 50 devices and 20 alerts with a Unix timestamp. It is written by cacheOfflineSnapshot() after every successful NSM sync and read by checkOfflineCache() on page load (called from skipApiModal() in demo mode).

The browser window.offline and window.online events are wired to show/hide #offlineBanner automatically. The banner text is set at cache read time and includes the age in minutes. In live mode, the banner should also show when the NSM API returns HTTP 503 or the WebSocket connection drops.

16Going Live

Go-Live Checklist

  1. 01
    Remove demo auto-skip. In the initApiCheck() IIFE at the bottom of the script, comment out skipApiModal(). The API modal will now prompt for credentials on first load.
  2. 02
    Wire simulateNSMConnect() to the real NSM token endpoint. Replace the setTimeout simulation with a real fetch to https://nsm-{region}.sonicwall.com/api/mssp/generatetoken. Pass the API key as a Bearer token in the Authorization header.
  3. 03
    Wire loadFromNSM() to real device and alert fetches. Uncomment the commented-out fetch blocks inside loadFromNSM(). The shapes of the responses map to mapNSMDevice() and mapNSMAlert() — implement those normalizer functions to match the unified device shape in Section 09.
  4. 04
    Configure FortiGate and Meraki credentials. If managing multi-vendor fleets, set the token, host, and key fields on the VENDOR_APIS instances. Uncomment the real fetch() blocks in each vendor class. Update the _rpcBody() session token for FortiManager.
  5. 05
    Self-host Chart.js. Download chart.umd.min.js v4.4.1 from the Chart.js GitHub releases. Place it alongside the HTML file. Update the <script src> tag to a relative path. The CDN may be unreachable from secure internal networks.
  6. 06
    Improve PIN-based key encryption. Replace the pin.padEnd(32, '0') approach with a proper PBKDF2 derivation. See the security note in Section 11. This is critical if real API keys will be stored in browser localStorage.
  7. 07
    Wire toast actions to real APIs. Every showToast() call in the action buttons (Isolate VPN, Block IP, Push Policy, etc.) is a stub. Replace each with the appropriate vendor API call wrapped in a try/catch, using the toast to surface success or error.
  8. 08
    Replace the NSM log ticker with a real WebSocket feed. The renderNSMLog() interval is cosmetic. In production, open a WebSocket to the NSM event stream and call addLine() on each incoming event.
  9. 09
    Populate the C2 stat counters from live data. The four boxes in the Console 2 right sidebar (#c2-p1, #c2-p2, #c2-resolved, MTTR) are hardcoded. Compute them from the ALERTS array and your PSA's closed-ticket endpoint.
  10. 10
    Persist theme preference. Add localStorage.setItem('sw_theme', ...) to toggleTheme() and read it during init. Optional but a quality-of-life improvement for engineers who always use NSM Classic.
  11. 11
    Serve from an authenticated internal host. The file will contain real API key material after configuration. Never serve it from a publicly accessible URL. Host behind an authenticated internal web server or VPN-gated URL.
  12. 12
    Update the AbuseIPDB threat widget. Add your AbuseIPDB API key to a backend proxy endpoint and update loadThreatMap() to call GET https://api.abuseipdb.com/api/v2/blacklist via the proxy. Direct browser-to-API calls will be blocked by CORS.
17FAQ
Q The donut chart in the right panel is blank.

Chart.js failed to load from the CDN. Open the browser console — a 404 or network error on chart.umd.min.js will be visible. Self-host Chart.js as documented in Section 02. The rest of the console — all four tabs, all tables, all widgets — is pure HTML/CSS and works without Chart.js.

Q The KPI progress bars don't animate or are invisible.

This was a known bug in the original file where color-mix(in srgb, var(--cyan) 50%, transparent) failed silently. The current version uses a pre-resolved colorMap object with hex values for each named color. If a bar is still invisible, verify the data-color attribute on the .kpi-item matches one of the seven keys in colorMap: green, amber, cyan, red, purple, blue, orange.

Q The compliance widget shows "Loading CVE data…" and never updates.

The compliance widget (#compliance-widget) is loaded by loadCompliance(). In the original file this function was defined but never called. The current version calls loadCompliance() on init and sets a 15-minute refresh interval. If you are running the unfixed original file, add loadCompliance(); after the function definition.

Q The fleet matrix only shows SonicWall devices even though FortiGate and Meraki are in the data.

renderFleetMatrix() uses DEVICES which includes all three vendors after the DEVICES.push(...FG_DEVICES, ...MRK_DEVICES) merge call. However, the FortiGate firmware check uses !d.firmware.startsWith('7.1.3') — this is a SonicWall version string and will always trigger the upgrade dot on FortiGate and Meraki devices regardless of their actual firmware status. Update the check to be vendor-aware: (d.vendor==='sonicwall' && !d.version.startsWith('7.1.3')) || (d.vendor==='fortigate' && d.version !== '7.4.3') || (d.vendor==='meraki' && d.version !== '18.211.2').

Q Keyboard shortcuts 1–4 aren't switching consoles.

Shortcuts require focus to be outside any text input. The console switch listener checks e.target.tagName === 'INPUT' and suppresses if true. Click anywhere on the console background (not in a search field) and try again. Also verify no ctrlKey or metaKey modifier is held — the number keys only work as plain key presses.

Q How do I add a new tenant to the device list?

Add the tenant name to the TENANTS array near the top of the script block. Device names are auto-generated as {TENANT_FIRST_WORD}-FW-{NNN}. The tenant filter dropdowns in Consoles 3 and 4 are hardcoded HTML <option> elements — add a matching option to each <select> element for the tenant to appear in filters.

Q The OPS Queue opens automatically even when I don't want it.

updateOpsBadge() calls toggleOpsQueue() automatically when active (non-done) jobs exist and the panel is currently closed. This behavior is intentional — the queue auto-surfaces when a background job is running. To disable auto-open, remove the if(n>0 && !opsOpen)toggleOpsQueue() line from updateOpsBadge().

Q How do I add a fifth console tab?

Add a <button class="tab-btn" onclick="switchConsole(4)"> to the #tabs bar. Add a <div class="console" id="c5"> inside #app. If the new console uses grid layout, add #c5.active { display:grid; } to the CSS (or flex/block as needed). The switchConsole(n) function uses querySelectorAll index matching, so the new button must be the fifth .tab-btn and the new console must be the fifth .console in DOM order.