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.
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.
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.
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.
| Panel / Value | Endpoint Used | Verified? | Notes |
|---|---|---|---|
| KPI — Total / Online / Warning / Offline | GET /v2/devices | ✓ Yes | Returns paginated device list with status field. Filter with ?df=status%3DONLINE. |
| KPI — Patch Compliance % | GET /v2/queries/os-patches | ✓ Yes | Query endpoint returns patch status per device. Documented in NinjaOne API reference. |
| NOC Gauge — Healthy Agents | GET /v2/devices | ✓ Yes | Derived from status === 'ONLINE' count on device list response. |
| NOC Gauge — Active Alerts | GET /v2/alerts | ✓ Yes | Endpoint confirmed. Filter: ?status=ACTIVE. Returns severity, message, deviceName. |
| NOC Gauge — Open Tickets | NOT IN RMM API | ✗ Gap | NinjaOne 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 count | GET /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 inspect | GET /v2/devices/{id} | ✓ Yes | Each cell click shows device detail toast. In live mode should call device detail endpoint. |
| Devices View table | GET /v2/devices | ✓ Yes | CPU, RAM, Disk pulled from device systemInfo. Pagination supported via ?after={cursor}. |
| Alerts View table | GET /v2/alerts | ✓ Yes | Severity maps to NinjaOne's severity field (CRITICAL, HIGH, MODERATE, LOW). |
| Patch View table | GET /v2/queries/os-patches | ✓ Yes | Returns per-device patch counts for missing critical and important patches. |
| API Explorer — Run Script | POST /v2/device/{id}/script | ✓ Yes | Documented endpoint. Body requires id (script ID), type, runAs. |
| API Explorer — Reboot | POST /v2/device/{id}/reboot | ✓ Yes | Documented endpoint. Body: mode, reason, notifyUsers, delayMinutes. |
| API Explorer — Reset Alert | DELETE /v2/alert/{uid} | ✓ Yes | Documented. Acknowledges and clears the alert from the active queue. |
| API Explorer — Organizations | GET /v2/organizations | ✓ Yes | Returns multi-tenant org list. Includes nodeCount. |
| Change | Why |
|---|---|
Removed <style id="ktc-home-bar-style"> block | Suite directive: remove KTC home bar style entirely |
Removed #ktc-demo-bar div and home link | Suite directive: topbar shows only console name |
Fixed .topbar from top:36px to top:0 | Demo bar offset removed; topbar now at viewport top |
Fixed .layout from margin-top:88px to 52px | Layout height calculation corrected for topbar-only offset |
Added const DEMO_MODE = true | Single 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 ⚠ PSA | Documented 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 JS | Permanent documentation of genuine vendor API constraints in code |
Integration Status
Current state of each integration layer between the browser and the NinjaOne REST API v2.
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 | Status | Required Action |
|---|---|---|
| Dashboard HTML / JS | ✓ Ready | All panels, KPIs, views, and wiring are fully implemented. No UI changes needed to go live. |
| Proxy / Middleware | Not configured | Needs a reverse proxy or Node middleware that maps /api/ninja-rmm/* to https://app.ninjarmm.com/v2/* with injected Bearer token. |
| NinjaOne API | Not reachable | API must be enabled in NinjaOne Settings → API → Applications. Generate a Client ID and Client Secret for OAuth 2.0 client_credentials flow. |
| Auth / Token | Not configured | NinjaOne 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 RMM | NinjaOne RMM API has no ticket endpoint. Requires NinjaOne PSA API (separate product) or CW Manage integration. |
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.
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.
Topbar
52px fixed bar. Contains brand identity, demo/live status pill, and the Refresh button. KTC home bar has been removed per suite directive.
| Element | ID / Class | Data Source | Behavior |
|---|---|---|---|
| Brand icon | .brand-icon | Static | "N1 / RMM" text mark |
| Brand name | .brand-name | Static | "NinjaOne RMM" — Rajdhani font |
| Status pill | #statusDot, #statusLabel | Static (DEMO MODE) / Live when proxy active | Green dot pulses. Label switches to "LIVE" when proxy connected. |
| Demo Info button | .btn-ghost | Static | Shows toast with demo mode explanation |
| Refresh button | #refreshBtn | Triggers refreshAll() | Shows "Refreshing…" during the 600ms render cycle |
Sidebar & Navigation
210px left sidebar with system status, nav links, and a live clock. Persists across all views.
| Section | Element | Data Source |
|---|---|---|
| Device count | #live-device-count | DEMO_SUMMARY.devices.total → live: GET /v2/devices totalCount |
| Devices badge | #badge-devices | Same as device count |
| Alerts badge | #badge-alerts | P1 critical count → live: GET /v2/alerts?severity=CRITICAL |
| Clock | #sidebar-clock | new Date().toLocaleTimeString() — updates every 1 second |
| Nav items | .nav-item | Call showView(v) — switches active view and nav highlight |
KPI Stat Cards
Five cards spanning the dashboard header row. Each is clickable to navigate to the relevant view.
| Card | ID | Calculation | API Source | Click Action |
|---|---|---|---|---|
| Total Devices | #stat-total | devices.total | GET /v2/devices → totalCount | Navigate to Devices view |
| Online | #stat-online | devices.online | GET /v2/devices?df=status%3DONLINE | Filter Devices view to online |
| Warning | #stat-warning | devices.warning | GET /v2/devices?df=status%3DWARNING | Filter Devices view to warning |
| Offline | #stat-offline | devices.offline | GET /v2/devices?df=status%3DOFFLINE | Filter Devices view to offline |
| Patch Compliance | #stat-patch | patch.pct + '%' | GET /v2/queries/os-patches | Navigate to Patch view |
Fleet Health Bar
Proportional segmented bar showing fleet health distribution. Four segments: Online / Warning / Critical / Offline.
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.| Segment | ID | Color | Calculation |
|---|---|---|---|
| Online | #hb-ok | --green | online / total × 100 % |
| Warning | #hb-warn | --yellow | warning / total × 100 % |
| Critical | #hb-fail | --red | Fixed 3.3% in demo — live: critical count / total |
| Offline | #hb-offline | --text3 | offline / total × 100 % |
| Health label | #health-pct | --green | online / total × 100 % + "% online" |
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.
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 | Donut ID | Value ID | API Source | Notes |
|---|---|---|---|---|
| Healthy Agents | #donut-agents | #gauge-agents | GET /v2/devices | Pct = healthy / total. Shows count of online (non-warning, non-offline) agents. |
| Patch Compliant | #donut-patch | #gauge-patch | GET /v2/queries/os-patches | Pct = compliant / total. Sub-label shows overdue count. |
| Active Alerts | #donut-alerts | #gauge-alerts | GET /v2/alerts | Donut pct = alerts / 20 (capped at 100%). Sub-label shows P1 critical count. |
| Open Tickets ⚠ | #donut-tickets | #gauge-tickets | NOT IN RMM API | PSA module only. Source from NinjaOne PSA API or CW Manage. Gauge labelled ⚠ PSA. |
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).
Device Fleet Matrix
Visual heatmap of all managed endpoints. Each square represents one device, color-coded by health status. Unmanaged slots shown in grey.
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.Agent Coverage Bar
Proportional bar showing managed vs unmanaged device coverage across all organisations.
org.nodeCount (from GET /v2/organizations) minus the count of active agents. This is an approximation — label is marked ⚠ est. in the UI.
| Element | ID | Calculation |
|---|---|---|
| Bar fill | #coverage-bar | managed / total × 100% |
| Percentage | #coverage-pct | Math.round(managed / total × 100) + '%' |
| Managed label | #coverage-managed | coverage.managed + ' managed' |
| Unmanaged label | #coverage-unmanaged | (total - managed) + ' unmanaged' |
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.
renderDevicesTable(). Filters applied: status, type, search query. Pagination via devPage state. CPU/RAM color-coded: green <70%, yellow 70–85%, red >85%.?df= device filter| Column | Field | API Source | Notes |
|---|---|---|---|
| Device | d.name → systemName | GET /v2/devices → systemName | Mono font, white color |
| Organization | d.org → organizationName | GET /v2/devices → organizationName | Multi-tenant org label |
| Type | d.type → nodeClass | GET /v2/devices → nodeClass | server / workstation / laptop |
| OS | d.os → os.name | GET /v2/devices → os.name | Full OS string, truncated |
| CPU | d.cpu → cpuUsage | GET /v2/device/{id} → systemInfo | % utilization; "—" for offline |
| RAM | d.ram → memoryUsage | GET /v2/device/{id} → systemInfo | % utilization; "—" for offline |
| Disk | d.disk → diskUsage | GET /v2/device/{id} → volumes | % of primary volume; shown even offline |
| Patch | d.patchStatus | GET /v2/queries/os-patches | "Current" → green badge; else warn badge |
| Status | d.status | GET /v2/devices → status | Online / Warning / Offline badge |
Alerts View
Active incident queue with severity (P1–P4), status, age, and assignee. Filterable by severity and status.
renderAlertsTable(). Filters: severity (P1–P4), status (open / investigating / mitigated / resolved), text search on title and device. Row color coded by severity.?status=ACTIVE&severity=CRITICAL| Demo Field | NinjaOne API Field | Notes |
|---|---|---|
| a.id | alertId (numeric) | Demo uses string "RMM-XXXX" for readability; API returns numeric ID |
| a.sev → P1/P2/P3/P4 | severity: CRITICAL / HIGH / MODERATE / LOW | Demo maps P1=CRITICAL, P2=HIGH, P3=MODERATE, P4=LOW |
| a.title | message | Alert message string |
| a.device | deviceName | Hostname of the affected device |
| a.source | sourceName | Monitor or engine that triggered the alert |
| a.status | status (ACTIVE / RESET) | Demo extends with investigating / mitigated / resolved states |
| a.age | createTime (ISO timestamp) | Compute age client-side: Date.now() - new Date(createTime) |
Patch View
OS patch compliance per device. Summary strip (compliant / overdue / pending / compliance%) above a full per-device table.
renderPatchTable(). Missing critical patches show in red, missing important in yellow. Last scan timestamp formatted as "MMM D, HH:MM".| Pill | ID | Calculation | Color |
|---|---|---|---|
| Compliant | #pt-compliant | total - non-current devices | --green |
| Overdue | #pt-overdue | patch.overdue from summary | --red |
| Pending | #pt-pending | patch.pending from summary | --yellow |
| Compliance % | #pt-pct | patch.pct + '%' | --cyan |
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.
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.
| Label | Method | Path | Verified |
|---|---|---|---|
| List Devices | GET | /v2/devices | ✓ |
| Device Detail | GET | /v2/devices/{id} | ✓ |
| Active Alerts | GET | /v2/alerts | ✓ |
| Run Script | POST | /v2/device/{id}/script | ✓ |
| OS Patches | GET | /v2/queries/os-patches | ✓ |
| Reboot Device | POST | /v2/device/{id}/reboot | ✓ |
| Organizations | GET | /v2/organizations | ✓ |
| Reset Alert | DELETE | /v2/alert/{uid} | ✓ |
All API Endpoints
Complete reference of every NinjaOne REST API v2 endpoint used or referenced in this console. Base: https://app.ninjarmm.com/v2
grant_type=client_credentials&client_id=…&client_secret=…&scope=monitoring management control. Returns access_token valid for 3600 seconds. Proxy must refresh.
Proxy
?pageSize=100, ?after={cursor}, ?df=status%3DONLINE (device filter). Returns systemName, nodeClass, status, organizationName, lastContact.Proxy?status=ACTIVE, ?severity=CRITICAL. Returns alertId, severity, message, deviceName, sourceName, createTime, status.Proxyid, name, nodeCount, created. Used for coverage bar estimation.Proxy{"id": scriptId, "type": "POWERSHELL", "runAs": "SYSTEM", "parameters": "…"}. Returns jobId, status: QUEUED.Proxy{"mode": "NORMAL", "reason": "…", "notifyUsers": true, "delayMinutes": 5}. Returns jobId, scheduledAt.Proxysuccess: true, resetAt. Note: NinjaOne uses numeric alertId, not string UID.Proxy| Feature | Status | Workaround |
|---|---|---|
| Ticket list / open count | No endpoint | Use NinjaOne PSA API (GET /v2/ticketing/ticket — PSA module) or ConnectWise Manage API. |
| Unmanaged device count | Estimated | Derive from org.nodeCount minus active agent count. Mark as estimated in UI. |
| CSAT / satisfaction score | No endpoint | Not applicable to NinjaOne RMM API. Source from PSA or CSAT platform. |
| Backup job status | Limited | NinjaOne Cloud Backup has separate API endpoints not included in this console. |
Config & Fields Reference
All configurable constants and thresholds in the dashboard script block.
| Constant | Default | Effect |
|---|---|---|
| DEMO_MODE | true | Set 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) | 30000ms | Poll interval. Lower to 15000 for faster updates; raise to 60000 to reduce API load. |
| ORGS | 6 org names | Demo org roster. Replaced by real GET /v2/organizations data in live mode. |
| DEMO_DEVICES | 30 devices | Generated on page load. Replaced by real device array in live mode. |
| DEMO_ALERTS | 11 alerts | Static demo alert objects. Replaced by GET /v2/alerts response in live mode. |
| DEMO_SUMMARY | Fixed counts | Aggregated summary used by refreshAll(). In live mode, compute from real arrays. |
| CPU warn threshold | 70 / 85 % | Yellow above 70%, red above 85%. Defined inline in renderDevicesTable(). |
| RAM warn threshold | 70 / 85 % | Same thresholds as CPU. |
| Disk warn threshold | 70 / 85 % | Same thresholds applied to disk column. |
Documented Limitations
Known constraints of the current implementation and the NinjaOne RMM API itself.
| ID | Limitation | Root Cause | Resolution |
|---|---|---|---|
| L-01 | All data is demo | DEMO_MODE = true; proxy not configured | Configure proxy, set DEMO_MODE = false. See §19. |
| L-02 | No ticket endpoint | NinjaOne RMM API has no /tickets resource — PSA module only | Source from NinjaOne PSA API or CW Manage. Gauge labelled ⚠ PSA. |
| L-03 | Unmanaged count is estimated | NinjaOne does not expose raw unmanaged device counts | Derive from org nodeCount delta. Labelled ⚠ est. in UI. |
| L-04 | CPU/RAM require per-device call | GET /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-05 | Alert UIDs are numeric in API | Demo uses string IDs ("RMM-XXXX") for readability; real API returns numeric alertId | In live normalizer, cast alertId to string for display. |
| L-06 | Alert status fields differ | NinjaOne API returns ACTIVE / RESET; demo extends with investigating / mitigated / resolved | Map extended statuses to display badges in the normalizer layer. |
| L-07 | Ack state not persisted | No alert acknowledgment endpoint separate from DELETE /v2/alert/{uid} | Use DELETE to clear; state persists server-side in NinjaOne. |
| L-08 | No RBAC on action buttons | Static HTML — all buttons visible to all users | Enforce at proxy layer: restrict POST/DELETE endpoints by authenticated role. |
Proxy Activation Checklist
Step-by-step to move from demo mode to live NinjaOne API data.
monitoring management control.
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./api/ninja-rmm/* to https://app.ninjarmm.com/v2/*, injects Authorization: Bearer {token}, and adds CORS headers for the dashboard origin.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./api/ninja-rmm/devices fetch should return HTTP 200 with a real device array. KPI "Total Devices" count should match your NinjaOne portal count.systemName, nodeClass, organizationName, etc. If these differ from demo field names, add a normalizer function between ninjaFetch() and the data arrays in loadDashboard().POST /v2/device/{id}/script on a test device. Verify the job queues and completes before enabling on production fleet.GET /v2/ticketing/ticket?status=OPEN, or CW Manage GET /v4_6_release/apis/3.0/service/tickets. Source via the same proxy.- 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
Troubleshooting
Common issues during deployment and daily operation. Always open DevTools Console and Network tabs first.
/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.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.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.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().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.