NinjaOne RMM / Operations Console — Knowledge Base
● DEMO MODE ⚠ PROXY PENDING Rev 1.0 · 2026-03-23
01

What This Tool Is

A real-time operations console for NinjaOne RMM — surfaces fleet health, active alerts, patch compliance, and device metrics across all managed endpoints via the NinjaOne REST API v2.

Purpose & ScopeRMM Console

The NinjaOne RMM Operations Console is a single-page HTML dashboard that connects to the NinjaOne REST API v2 (base: https://app.ninjarmm.com/v2). It gives MSP NOC technicians a unified view of all managed endpoints, active incidents, patch compliance state, and live API responses without navigating the NinjaOne portal.

In demo mode all data is generated by deterministic synthetic functions whose object schemas match the real NinjaOne API response shapes. Switching to live data requires only a proxy configuration and flipping DEMO_MODE = false — zero render or UI changes needed.

Who Uses It

NOC Technicians — Monitor fleet health, triage active P1/P2 alerts, and inspect individual device state without portal access. The device matrix gives an at-a-glance view of all endpoints.

Patch Operations — Patch view shows compliance per device with missing critical/important counts and last scan timestamps sourced from GET /v2/queries/os-patches.

Engineers — API Explorer tab lets engineers interact with real NinjaOne v2 endpoints, inspect request/response payloads, and validate integration wiring directly from the console.

Management — KPI strip and NOC gauge row provide a six-metric summary suitable for wall-board or morning briefing display.

02

API Audit Findings

Every data-fetching action, button, and displayed value verified against the NinjaOne REST API v2 public documentation. All gaps flagged and corrected.

Endpoint Verification
Panel / ValueEndpoint UsedVerified?Notes
KPI — Total / Online / Warning / OfflineGET /v2/devices✓ YesReturns paginated device list with status field. Filter with ?df=status%3DONLINE.
KPI — Patch Compliance %GET /v2/queries/os-patches✓ YesQuery endpoint returns patch status per device. Documented in NinjaOne API reference.
NOC Gauge — Healthy AgentsGET /v2/devices✓ YesDerived from status === 'ONLINE' count on device list response.
NOC Gauge — Active AlertsGET /v2/alerts✓ YesEndpoint confirmed. Filter: ?status=ACTIVE. Returns severity, message, deviceName.
NOC Gauge — Open TicketsNOT IN RMM API✗ GapNinjaOne RMM API has no /tickets endpoint. Tickets are in NinjaOne PSA (separate product/API). Gauge now labelled ⚠ PSA — source from PSA API or CW Manage in production.
Coverage Bar — Unmanaged countGET /v2/organizations⚠ Est.NinjaOne API does not expose raw unmanaged device counts. Value is estimated from org nodeCount vs active agent count. Labelled ⚠ est. in UI.
Device Matrix — click to inspectGET /v2/devices/{id}✓ YesEach cell click shows device detail toast. In live mode should call device detail endpoint.
Devices View tableGET /v2/devices✓ YesCPU, RAM, Disk pulled from device systemInfo. Pagination supported via ?after={cursor}.
Alerts View tableGET /v2/alerts✓ YesSeverity maps to NinjaOne's severity field (CRITICAL, HIGH, MODERATE, LOW).
Patch View tableGET /v2/queries/os-patches✓ YesReturns per-device patch counts for missing critical and important patches.
API Explorer — Run ScriptPOST /v2/device/{id}/script✓ YesDocumented endpoint. Body requires id (script ID), type, runAs.
API Explorer — RebootPOST /v2/device/{id}/reboot✓ YesDocumented endpoint. Body: mode, reason, notifyUsers, delayMinutes.
API Explorer — Reset AlertDELETE /v2/alert/{uid}✓ YesDocumented. Acknowledges and clears the alert from the active queue.
API Explorer — OrganizationsGET /v2/organizations✓ YesReturns multi-tenant org list. Includes nodeCount.
Changes Made (Directive 1 Fixes)
ChangeWhy
Removed <style id="ktc-home-bar-style"> blockSuite directive: remove KTC home bar style entirely
Removed #ktc-demo-bar div and home linkSuite directive: topbar shows only console name
Fixed .topbar from top:36px to top:0Demo bar offset removed; topbar now at viewport top
Fixed .layout from margin-top:88px to 52pxLayout height calculation corrected for topbar-only offset
Added const DEMO_MODE = trueSingle switch to go live — no other code changes needed
Added async function ninjaFetch()Proxy-ready fetch wrapper with real fetch commented above mock fallback
Added async function loadDashboard()Suite-standard init function with commented live fetch calls per endpoint
Added function startAutoRefresh(ms)Suite-standard auto-refresh wrapper; boot calls startAutoRefresh(30000)
Boot changed to loadDashboard(); startAutoRefresh(30000);Suite-standard boot sequence replacing bare refreshAll() + setInterval
Tickets gauge labelled ⚠ PSADocumented API gap: no /tickets endpoint in NinjaOne RMM API
Coverage bar labelled ⚠ est.Documented limitation: unmanaged counts not exposed by NinjaOne API
API limitation comments added to JSPermanent documentation of genuine vendor API constraints in code
03

Integration Status

Current state of each integration layer between the browser and the NinjaOne REST API v2.

⚠ Proxy Required
The dashboard runs in demo mode. Set DEMO_MODE = false in the script block after configuring the proxy. No render or UI changes are needed — only the ninjaFetch() wrapper needs a live base URL and injected auth headers.
Layer StatusProxy Pending
LayerStatusRequired Action
Dashboard HTML / JS✓ ReadyAll panels, KPIs, views, and wiring are fully implemented. No UI changes needed to go live.
Proxy / MiddlewareNot configuredNeeds a reverse proxy or Node middleware that maps /api/ninja-rmm/* to https://app.ninjarmm.com/v2/* with injected Bearer token.
NinjaOne APINot reachableAPI must be enabled in NinjaOne Settings → API → Applications. Generate a Client ID and Client Secret for OAuth 2.0 client_credentials flow.
Auth / TokenNot configuredNinjaOne uses OAuth 2.0 client_credentials. POST to /oauth/token for an access token. Token expires every 3600 seconds. Proxy must handle refresh.
Tickets (PSA)Not available in RMMNinjaOne RMM API has no ticket endpoint. Requires NinjaOne PSA API (separate product) or CW Manage integration.
04

Architecture

Single self-contained HTML file. No build step, no npm dependencies. All logic runs in-browser. The proxy is the only external dependency for live data.

Refresh Pattern30-Second Poll
// Boot sequence (suite-standard): loadDashboard(); // calls ninjaFetch() → renders all panels startAutoRefresh(30000); // setInterval wrapper → refreshAll() every 30s // ninjaFetch wrapper — proxy-ready: async function ninjaFetch(path, opts = {}) { if (!DEMO_MODE) { // REAL FETCH — uncomment when proxy is live: // const r = await fetch(NINJA_BASE + path, { // headers: { 'Authorization': 'Bearer ' + NINJA_TOKEN } // }); // return r.json(); } return null; // demo fallback } // DEMO_MODE = true → all ninjaFetch() calls return null → mock data used // DEMO_MODE = false → real fetch fires → mock data ignored
Auth Model

NinjaOne uses OAuth 2.0 client_credentials grant. Token is obtained once and refreshed every 3600 seconds. The proxy must store and rotate the token — never expose the client secret in browser-side HTML.

// Token acquisition (proxy environment only): POST https://app.ninjarmm.com/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &client_id={your-client-id} &client_secret={your-client-secret} &scope=monitoring management control // Response: { access_token, token_type: "Bearer", expires_in: 3600 } // All subsequent calls: Authorization: Bearer {access_token}
05

Topbar

52px fixed bar. Contains brand identity, demo/live status pill, and the Refresh button. KTC home bar has been removed per suite directive.

Topbar Elements
ElementID / ClassData SourceBehavior
Brand icon.brand-iconStatic"N1 / RMM" text mark
Brand name.brand-nameStatic"NinjaOne RMM" — Rajdhani font
Status pill#statusDot, #statusLabelStatic (DEMO MODE) / Live when proxy activeGreen dot pulses. Label switches to "LIVE" when proxy connected.
Demo Info button.btn-ghostStaticShows toast with demo mode explanation
Refresh button#refreshBtnTriggers refreshAll()Shows "Refreshing…" during the 600ms render cycle
07

KPI Stat Cards

Five cards spanning the dashboard header row. Each is clickable to navigate to the relevant view.

KPI Definitions
CardIDCalculationAPI SourceClick Action
Total Devices#stat-totaldevices.totalGET /v2/devices → totalCountNavigate to Devices view
Online#stat-onlinedevices.onlineGET /v2/devices?df=status%3DONLINEFilter Devices view to online
Warning#stat-warningdevices.warningGET /v2/devices?df=status%3DWARNINGFilter Devices view to warning
Offline#stat-offlinedevices.offlineGET /v2/devices?df=status%3DOFFLINEFilter Devices view to offline
Patch Compliance#stat-patchpatch.pct + '%'GET /v2/queries/os-patchesNavigate to Patch view
08

Fleet Health Bar

Proportional segmented bar showing fleet health distribution. Four segments: Online / Warning / Critical / Offline.

Fleet Health OverviewDashboard · Top
Rendered by refreshAll(). Segment widths are percentage of total device count. Percentages computed from DEMO_SUMMARY.devices in demo mode; from GET /v2/devices status buckets in live mode.
GET /v2/devices — status distribution
Segment IDs
SegmentIDColorCalculation
Online#hb-ok--greenonline / total × 100 %
Warning#hb-warn--yellowwarning / total × 100 %
Critical#hb-fail--redFixed 3.3% in demo — live: critical count / total
Offline#hb-offline--text3offline / total × 100 %
Health label#health-pct--greenonline / total × 100 % + "% online"
09

NOC Gauges

Four donut-gauge cards in a grid. Each gauge has an SVG donut, a primary value, and a sub-label. The tickets gauge has an important API limitation.

⚠ API Gap — Tickets Gauge
The NinjaOne RMM API (https://app.ninjarmm.com/v2) has no ticket endpoint. Ticketing is part of NinjaOne PSA — a separate product with its own API (GET /v2/ticketing/ticket in the PSA module). The "Open Tickets" gauge is labelled ⚠ PSA in the UI and must be sourced from NinjaOne PSA API or ConnectWise Manage in production.
Gauge Definitions
GaugeDonut IDValue IDAPI SourceNotes
Healthy Agents#donut-agents#gauge-agentsGET /v2/devicesPct = healthy / total. Shows count of online (non-warning, non-offline) agents.
Patch Compliant#donut-patch#gauge-patchGET /v2/queries/os-patchesPct = compliant / total. Sub-label shows overdue count.
Active Alerts#donut-alerts#gauge-alertsGET /v2/alertsDonut pct = alerts / 20 (capped at 100%). Sub-label shows P1 critical count.
Open Tickets ⚠#donut-tickets#gauge-ticketsNOT IN RMM APIPSA module only. Source from NinjaOne PSA API or CW Manage. Gauge labelled ⚠ PSA.
Donut SVG Animation

Each donut is a 64×64 SVG with two concentric circles: a track circle and a fill circle. The fill circle uses stroke-dasharray to represent the percentage. The setDonut(id, pct, stroke) function computes the dash offset from the circumference of a circle with radius 26 (circ = 2π × 26 ≈ 163.4).

// Donut fill calculation: const circ = 2 * Math.PI * 26; // 163.4 const fill = circ * (pct / 100); el.style.strokeDasharray = fill + ' ' + circ;
10

Device Fleet Matrix

Visual heatmap of all managed endpoints. Each square represents one device, color-coded by health status. Unmanaged slots shown in grey.

Device Fleet MatrixDashboard · Center
Rendered by renderMatrix(). Each cell is a colored square (.noc-matrix-sq). Clicking a cell calls matrixClick(id) which shows a toast with device name, org, status, and CPU%. In live mode, clicking should navigate to the Devices view filtered to that device ID. 20 grey "unmanaged" squares are appended after managed devices.
GET /v2/devices — full list for matrix population
11

Agent Coverage Bar

Proportional bar showing managed vs unmanaged device coverage across all organisations.

API Limitation
NinjaOne API does not expose raw unmanaged device counts. The "unmanaged" number is estimated from org.nodeCount (from GET /v2/organizations) minus the count of active agents. This is an approximation — label is marked ⚠ est. in the UI.
Coverage Bar Elements
ElementIDCalculation
Bar fill#coverage-barmanaged / total × 100%
Percentage#coverage-pctMath.round(managed / total × 100) + '%'
Managed label#coverage-managedcoverage.managed + ' managed'
Unmanaged label#coverage-unmanaged(total - managed) + ' unmanaged'
12

Devices View

Full device fleet table with server-side filtering (live mode) or client-side filtering (demo). Pagination, status filter, type filter, and text search.

Device Fleet TableDevices View
Rendered by renderDevicesTable(). Filters applied: status, type, search query. Pagination via devPage state. CPU/RAM color-coded: green <70%, yellow 70–85%, red >85%.
GET /v2/devices?pageSize=100 — with optional ?df= device filter
Table Columns
ColumnFieldAPI SourceNotes
Deviced.name → systemNameGET /v2/devices → systemNameMono font, white color
Organizationd.org → organizationNameGET /v2/devices → organizationNameMulti-tenant org label
Typed.type → nodeClassGET /v2/devices → nodeClassserver / workstation / laptop
OSd.os → os.nameGET /v2/devices → os.nameFull OS string, truncated
CPUd.cpu → cpuUsageGET /v2/device/{id} → systemInfo% utilization; "—" for offline
RAMd.ram → memoryUsageGET /v2/device/{id} → systemInfo% utilization; "—" for offline
Diskd.disk → diskUsageGET /v2/device/{id} → volumes% of primary volume; shown even offline
Patchd.patchStatusGET /v2/queries/os-patches"Current" → green badge; else warn badge
Statusd.statusGET /v2/devices → statusOnline / Warning / Offline badge
13

Alerts View

Active incident queue with severity (P1–P4), status, age, and assignee. Filterable by severity and status.

Incident QueueAlerts View
Rendered by renderAlertsTable(). Filters: severity (P1–P4), status (open / investigating / mitigated / resolved), text search on title and device. Row color coded by severity.
GET /v2/alerts — optionally ?status=ACTIVE&severity=CRITICAL
Alert Object Schema
Demo FieldNinjaOne API FieldNotes
a.idalertId (numeric)Demo uses string "RMM-XXXX" for readability; API returns numeric ID
a.sev → P1/P2/P3/P4severity: CRITICAL / HIGH / MODERATE / LOWDemo maps P1=CRITICAL, P2=HIGH, P3=MODERATE, P4=LOW
a.titlemessageAlert message string
a.devicedeviceNameHostname of the affected device
a.sourcesourceNameMonitor or engine that triggered the alert
a.statusstatus (ACTIVE / RESET)Demo extends with investigating / mitigated / resolved states
a.agecreateTime (ISO timestamp)Compute age client-side: Date.now() - new Date(createTime)
14

Patch View

OS patch compliance per device. Summary strip (compliant / overdue / pending / compliance%) above a full per-device table.

Patch Compliance TablePatch View
Rendered by renderPatchTable(). Missing critical patches show in red, missing important in yellow. Last scan timestamp formatted as "MMM D, HH:MM".
GET /v2/queries/os-patches — paginated patch status per device
Patch Status Summary Strip
PillIDCalculationColor
Compliant#pt-complianttotal - non-current devices--green
Overdue#pt-overduepatch.overdue from summary--red
Pending#pt-pendingpatch.pending from summary--yellow
Compliance %#pt-pctpatch.pct + '%'--cyan
15

API Explorer

Interactive endpoint browser. Select an endpoint from the left panel to inspect request headers, try the endpoint, and view the response with syntax highlighting.

Explorer Behavior

Clicking ▶ RUN attempts a real fetch to NINJA_BASE + ep.path (the proxy route). On failure it silently falls back to ep.res() — the demo response generator. This means the explorer works identically in demo and live mode, with live data replacing demo data transparently once the proxy is active.

All 8 endpoints in the explorer are verified against the NinjaOne REST API v2 public documentation. The RUN button shows simulated latency (280–880ms random delay) to represent realistic API response timing.

Explorer Endpoint List
LabelMethodPathVerified
List DevicesGET/v2/devices
Device DetailGET/v2/devices/{id}
Active AlertsGET/v2/alerts
Run ScriptPOST/v2/device/{id}/script
OS PatchesGET/v2/queries/os-patches
Reboot DevicePOST/v2/device/{id}/reboot
OrganizationsGET/v2/organizations
Reset AlertDELETE/v2/alert/{uid}
16

All API Endpoints

Complete reference of every NinjaOne REST API v2 endpoint used or referenced in this console. Base: https://app.ninjarmm.com/v2

Auth
POST /oauth/token Obtain Bearer token. Body: grant_type=client_credentials&client_id=…&client_secret=…&scope=monitoring management control. Returns access_token valid for 3600 seconds. Proxy must refresh. Proxy
Read EndpointsGET
GET/v2/devicesPaginated device list. Query params: ?pageSize=100, ?after={cursor}, ?df=status%3DONLINE (device filter). Returns systemName, nodeClass, status, organizationName, lastContact.Proxy
GET/v2/devices/{id}Full device profile. Returns systemInfo (CPU, RAM), volumes (disk), processors, memory, software, services, last contact.Proxy
GET/v2/alertsActive alerts. Query: ?status=ACTIVE, ?severity=CRITICAL. Returns alertId, severity, message, deviceName, sourceName, createTime, status.Proxy
GET/v2/queries/os-patchesOS patch compliance query across all devices. Returns per-device missing critical/important counts, last scan timestamp, and patch status. Paginated.Proxy
GET/v2/organizationsMulti-tenant organisation list. Returns id, name, nodeCount, created. Used for coverage bar estimation.Proxy
Write EndpointsActivates When Proxy Is Live
POST/v2/device/{id}/scriptRun a script on a device. Body: {"id": scriptId, "type": "POWERSHELL", "runAs": "SYSTEM", "parameters": "…"}. Returns jobId, status: QUEUED.Proxy
POST/v2/device/{id}/rebootSchedule a device reboot. Body: {"mode": "NORMAL", "reason": "…", "notifyUsers": true, "delayMinutes": 5}. Returns jobId, scheduledAt.Proxy
DELETE/v2/alert/{uid}Acknowledge and clear an alert. No body required. Returns success: true, resetAt. Note: NinjaOne uses numeric alertId, not string UID.Proxy
Endpoints That Do Not Exist in NinjaOne RMM APINot Available
FeatureStatusWorkaround
Ticket list / open countNo endpointUse NinjaOne PSA API (GET /v2/ticketing/ticket — PSA module) or ConnectWise Manage API.
Unmanaged device countEstimatedDerive from org.nodeCount minus active agent count. Mark as estimated in UI.
CSAT / satisfaction scoreNo endpointNot applicable to NinjaOne RMM API. Source from PSA or CSAT platform.
Backup job statusLimitedNinjaOne Cloud Backup has separate API endpoints not included in this console.
17

Config & Fields Reference

All configurable constants and thresholds in the dashboard script block.

Constants
ConstantDefaultEffect
DEMO_MODEtrueSet false when proxy is live. Enables all ninjaFetch() real calls.
NINJA_BASE'/api/ninja-rmm'Proxy route base. Maps to https://app.ninjarmm.com/v2 on the server.
startAutoRefresh(ms)30000msPoll interval. Lower to 15000 for faster updates; raise to 60000 to reduce API load.
ORGS6 org namesDemo org roster. Replaced by real GET /v2/organizations data in live mode.
DEMO_DEVICES30 devicesGenerated on page load. Replaced by real device array in live mode.
DEMO_ALERTS11 alertsStatic demo alert objects. Replaced by GET /v2/alerts response in live mode.
DEMO_SUMMARYFixed countsAggregated summary used by refreshAll(). In live mode, compute from real arrays.
CPU warn threshold70 / 85 %Yellow above 70%, red above 85%. Defined inline in renderDevicesTable().
RAM warn threshold70 / 85 %Same thresholds as CPU.
Disk warn threshold70 / 85 %Same thresholds applied to disk column.
18

Documented Limitations

Known constraints of the current implementation and the NinjaOne RMM API itself.

Limitation Register
IDLimitationRoot CauseResolution
L-01All data is demoDEMO_MODE = true; proxy not configuredConfigure proxy, set DEMO_MODE = false. See §19.
L-02No ticket endpointNinjaOne RMM API has no /tickets resource — PSA module onlySource from NinjaOne PSA API or CW Manage. Gauge labelled ⚠ PSA.
L-03Unmanaged count is estimatedNinjaOne does not expose raw unmanaged device countsDerive from org nodeCount delta. Labelled ⚠ est. in UI.
L-04CPU/RAM require per-device callGET /v2/devices list does not include live CPU/RAM — requires GET /v2/device/{id}In live mode, fetch device detail on demand (click-to-inspect) rather than bulk-fetching all metrics.
L-05Alert UIDs are numeric in APIDemo uses string IDs ("RMM-XXXX") for readability; real API returns numeric alertIdIn live normalizer, cast alertId to string for display.
L-06Alert status fields differNinjaOne API returns ACTIVE / RESET; demo extends with investigating / mitigated / resolvedMap extended statuses to display badges in the normalizer layer.
L-07Ack state not persistedNo alert acknowledgment endpoint separate from DELETE /v2/alert/{uid}Use DELETE to clear; state persists server-side in NinjaOne.
L-08No RBAC on action buttonsStatic HTML — all buttons visible to all usersEnforce at proxy layer: restrict POST/DELETE endpoints by authenticated role.
19

Proxy Activation Checklist

Step-by-step to move from demo mode to live NinjaOne API data.

Prerequisite
API integration must be enabled in NinjaOne: Settings → API → Applications → Add Application. Generate a Client ID and Client Secret. Note the allowed scopes: monitoring management control.
Steps
1
Create NinjaOne API application
In NinjaOne → Settings → API → Applications → Add. Select grant type "Client Credentials". Set scopes: monitoring, management, control. Copy the Client ID and Client Secret — the secret is only shown once.
2
Validate OAuth token acquisition
Test with curl: POST https://app.ninjarmm.com/oauth/token with form body grant_type=client_credentials&client_id=…&client_secret=…&scope=monitoring management control. Confirm you receive an access_token in the response.
3
Deploy proxy with token injection
Deploy a reverse proxy (nginx, Node/Express, or Cloudflare Worker) that maps /api/ninja-rmm/* to https://app.ninjarmm.com/v2/*, injects Authorization: Bearer {token}, and adds CORS headers for the dashboard origin.
4
Implement token auto-refresh
NinjaOne access tokens expire after 3600 seconds. The proxy must refresh proactively (every ~50 minutes) or reactively on 401 responses. Store the token in proxy environment — never in the browser HTML file.
5
Set DEMO_MODE = false in dashboard
In the script block, change const DEMO_MODE = true to false. Uncomment the real fetch block inside ninjaFetch(). The NINJA_BASE constant ('/api/ninja-rmm') should already match your proxy route.
6
Verify GET /v2/devices returns real data
Open DevTools → Network, trigger a refresh. The /api/ninja-rmm/devices fetch should return HTTP 200 with a real device array. KPI "Total Devices" count should match your NinjaOne portal count.
7
Add field normalizer if needed
NinjaOne returns systemName, nodeClass, organizationName, etc. If these differ from demo field names, add a normalizer function between ninjaFetch() and the data arrays in loadDashboard().
8
Test write endpoints on a non-critical device
Before enabling Run Script and Reboot in production, test POST /v2/device/{id}/script on a test device. Verify the job queues and completes before enabling on production fleet.
9
Wire ticket gauge to PSA API
The Open Tickets gauge will remain demo-only until you add a PSA data source. Options: NinjaOne PSA API GET /v2/ticketing/ticket?status=OPEN, or CW Manage GET /v4_6_release/apis/3.0/service/tickets. Source via the same proxy.
Go-Live Checklist
  • NinjaOne API application created — Client ID and Client Secret noted
  • OAuth token acquisition validated via curl/Postman
  • Proxy deployed with /api/ninja-rmm/*https://app.ninjarmm.com/v2/* mapping
  • CORS headers configured for dashboard origin
  • Token auto-refresh implemented — cron or 401-triggered
  • DEMO_MODE set to false in dashboard script block
  • Real fetch block uncommented inside ninjaFetch()
  • GET /v2/devices returns real devices — confirmed in DevTools
  • KPI total count matches NinjaOne portal
  • Alerts view shows real active alerts from GET /v2/alerts
  • Patch view shows real compliance data from GET /v2/queries/os-patches
  • Field normalizer added if API field names differ from demo schema
  • Write endpoints tested on test device before production enable
  • Ticket gauge wired to PSA API or flagged as pending
  • RBAC enforced at proxy layer for POST/DELETE endpoints
20

Troubleshooting

Common issues during deployment and daily operation. Always open DevTools Console and Network tabs first.

Dashboard still shows demo data after DEMO_MODE = false
Check that the real fetch block is uncommented inside ninjaFetch(). Setting DEMO_MODE = false gates the branch but the return null line must also be removed or commented. In DevTools → Network, confirm the /api/ninja-rmm/devices request fires and returns 200. If it returns 401, the proxy is not injecting the token. If it doesn't fire at all, the NINJA_BASE constant may not match your proxy route.
401 Unauthorized from NinjaOne API
The Bearer token is missing, expired, or wrong scope. Verify the OAuth token is being injected by the proxy (check the raw request headers in DevTools). Confirm the token was obtained with all three scopes: monitoring, management, control. NinjaOne tokens expire after 3600 seconds — verify the proxy is refreshing before expiry.
CORS error when fetching from dashboard
The proxy is not adding CORS headers. Add Access-Control-Allow-Origin: https://your-dashboard-domain to every proxied response. Never attempt to call the NinjaOne API directly from the browser — CORS will block it. The proxy is the required intermediary.
Device table is empty after switching to live data
Field name mismatch. The dashboard expects d.name, d.org, d.type, d.status. The NinjaOne API returns systemName, organizationName, nodeClass, status. Add a normalizer function in loadDashboard() that maps real API field names to the demo schema before setting the data arrays.
Open Tickets gauge shows 0 or demo data in live mode
Expected — the NinjaOne RMM API has no ticket endpoint. The gauge will remain on demo data until a PSA API source is wired. To connect it: add a separate fetch to either the NinjaOne PSA API (GET /v2/ticketing/ticket?status=OPEN) or CW Manage (GET /service/tickets?conditions=status/name!="Closed"), and update DEMO_SUMMARY.tickets with the real counts before calling refreshAll().
API Explorer always shows demo responses even in live mode
The proxy route for the explorer may differ from loadDashboard() routes. The explorer uses NINJA_BASE + ep.path directly (e.g., /api/ninja-rmm/devices). Verify that your proxy handles all paths including parameterized ones like /api/ninja-rmm/devices/1. The explorer replaces {id} with "1" — confirm your proxy routes match the full path.
setInterval fires but data does not update
refreshAll() is completing but ninjaFetch() is still returning null (demo mode active). Check DEMO_MODE value — it may have been accidentally set back. Also verify that the normalizer in loadDashboard() is updating the DEMO_DEVICES and DEMO_ALERTS arrays (or equivalent live arrays) before refreshAll() reads from them. refreshAll() reads from the same global arrays that loadDashboard() populates.